home *** CD-ROM | disk | FTP | other *** search
/ Freelog 125 / Freelog_MarsAvril2015_No125.iso / Internet / gpodder / gpodder-portable.exe / gpodder-portable / bin / gpo next >
Text File  |  2014-10-30  |  28KB  |  862 lines

  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3.  
  4. #
  5. # gPodder - A media aggregator and podcast client
  6. # Copyright (c) 2005-2014 Thomas Perl and the gPodder Team
  7. #
  8. # gPodder is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation; either version 3 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # gPodder is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License
  19. # along with this program.  If not, see <http://www.gnu.org/licenses/>.
  20. #
  21.  
  22.  
  23. # gpo - A better command-line interface to gPodder using the gPodder API
  24. # by Thomas Perl <thp@gpodder.org>; 2009-05-07
  25.  
  26.  
  27. """
  28.   Usage: gpo [--verbose|-v] [COMMAND] [params...]
  29.  
  30.   - Subscription management -
  31.  
  32.     subscribe URL [TITLE]      Subscribe to a new feed at URL (as TITLE)
  33.     search QUERY               Search the gpodder.net directory for QUERY
  34.     toplist                    Show the gpodder.net top-subscribe podcasts
  35.  
  36.     import FILENAME|URL        Subscribe to all podcasts in an OPML file
  37.     export FILENAME            Export all subscriptions to an OPML file
  38.  
  39.     rename URL TITLE           Rename feed at URL to TITLE
  40.     unsubscribe URL            Unsubscribe from feed at URL
  41.     enable URL                 Enable feed updates for the feed at URL
  42.     disable URL                Disable feed updates for the feed at URL
  43.  
  44.     info URL                   Show information about feed at URL
  45.     list                       List all subscribed podcasts
  46.     update [URL]               Check for new episodes (all or only at URL)
  47.  
  48.   - Episode management -
  49.  
  50.     download [URL]             Download new episodes (all or only from URL)
  51.     pending [URL]              List new episodes (all or only from URL)
  52.     episodes [URL]             List episodes (all or only from URL)
  53.  
  54.   - Configuration -
  55.  
  56.     set [key] [value]          List one (all) keys or set to a new value
  57.  
  58.   - Other commands -
  59.  
  60.     youtube URL                Resolve the YouTube URL to a download URL
  61.     rewrite OLDURL NEWURL      Change the feed URL of [OLDURL] to [NEWURL]
  62.     webui [public]             Start gPodder's Web UI server
  63.                                (public = listen on all network interfaces)
  64.     pipe                       Start gPodder in pipe-based IPC server mode
  65.  
  66. """
  67.  
  68. from __future__ import print_function
  69.  
  70. import sys
  71. import collections
  72. import os
  73. import re
  74. import inspect
  75. import functools
  76. try:
  77.     import readline
  78. except ImportError:
  79.     readline = None
  80. import shlex
  81. import pydoc
  82. import logging
  83.  
  84. try:
  85.     import termios
  86.     import fcntl
  87.     import struct
  88. except ImportError:
  89.     termios = None
  90.     fcntl = None
  91.     struct = None
  92.  
  93. # A poor man's argparse/getopt - but it works for our use case :)
  94. verbose = False
  95. for flag in ('-v', '--verbose'):
  96.     if flag in sys.argv:
  97.         sys.argv.remove(flag)
  98.         verbose = True
  99.         break
  100.  
  101. gpodder_script = sys.argv[0]
  102. if os.path.islink(gpodder_script):
  103.     gpodder_script = os.readlink(gpodder_script)
  104. gpodder_dir = os.path.join(os.path.dirname(gpodder_script), '..')
  105. # TODO: Read parent directory links as well (/bin -> /usr/bin, like on Fedora, see Bug #1618)
  106. # This would allow /usr/share/gpodder/ (not /share/gpodder/) to be found from /bin/gpodder
  107. prefix = os.path.abspath(os.path.normpath(gpodder_dir))
  108.  
  109. src_dir = os.path.join(prefix, 'src')
  110.  
  111. if os.path.exists(os.path.join(src_dir, 'gpodder', '__init__.py')):
  112.     # Run gPodder from local source folder (not installed)
  113.     sys.path.insert(0, src_dir)
  114.  
  115. import gpodder
  116. _ = gpodder.gettext
  117. N_ = gpodder.ngettext
  118.  
  119. gpodder.images_folder = os.path.join(prefix, 'share', 'gpodder', 'images')
  120. gpodder.prefix = prefix
  121.  
  122. # This is the command-line UI variant
  123. gpodder.ui.cli = True
  124.  
  125. # Platform detection (i.e. MeeGo 1.2 Harmattan, etc..)
  126. gpodder.detect_platform()
  127.  
  128. have_ansi = sys.stdout.isatty() and not gpodder.ui.win32
  129. interactive_console = sys.stdin.isatty() and sys.stdout.isatty()
  130. is_single_command = False
  131.  
  132. from gpodder import log
  133. log.setup(verbose)
  134.  
  135. from gpodder import core
  136. from gpodder import download
  137. from gpodder import my
  138. from gpodder import opml
  139. from gpodder import util
  140. from gpodder import youtube
  141. from gpodder import model
  142. from gpodder.config import config_value_to_string
  143.  
  144. def incolor(color_id, s):
  145.     if have_ansi and cli._config.ui.cli.colors:
  146.         return '\033[9%dm%s\033[0m' % (color_id, s)
  147.     return s
  148.  
  149. def safe_print(*args, **kwargs):
  150.     def convert(arg):
  151.         return unicode(util.convert_bytes(arg))
  152.  
  153.     ofile = kwargs.get('file', sys.stdout)
  154.     output = u' '.join(map(convert, args))
  155.     if ofile.encoding is None:
  156.         output = util.sanitize_encoding(output)
  157.     else:
  158.         output = output.encode(ofile.encoding, 'replace')
  159.  
  160.     try:
  161.         ofile.write(output)
  162.     except Exception, e:
  163.         print("""
  164.         *** ENCODING FAIL ***
  165.  
  166.         Please report this to http://bugs.gpodder.org/:
  167.  
  168.         args = %s
  169.         map(convert, args) = %s
  170.  
  171.         Exception = %s
  172.         """ % (repr(args), repr(map(convert, args)), e))
  173.  
  174.     ofile.write(kwargs.get('end', os.linesep))
  175.     ofile.flush()
  176.  
  177. # On Python 3 the encoding insanity is gone, so our safe_print()
  178. # function simply becomes the normal print() function. Good stuff!
  179. if sys.version_info >= (3,):
  180.     safe_print = print
  181.  
  182. # ANSI Colors: red = 1, green = 2, yellow = 3, blue = 4
  183. inred, ingreen, inyellow, inblue = (functools.partial(incolor, x)
  184.         for x in range(1, 5))
  185.  
  186. def FirstArgumentIsPodcastURL(function):
  187.     """Decorator for functions that take a podcast URL as first arg"""
  188.     setattr(function, '_first_arg_is_podcast', True)
  189.     return function
  190.  
  191. def get_terminal_size():
  192.     if None in (termios, fcntl, struct):
  193.         return (80, 24)
  194.  
  195.     s = struct.pack('HHHH', 0, 0, 0, 0)
  196.     stdout = sys.stdout.fileno()
  197.     x = fcntl.ioctl(stdout, termios.TIOCGWINSZ, s)
  198.     rows, cols, xp, yp = struct.unpack('HHHH', x)
  199.     return rows, cols
  200.  
  201. class gPodderCli(object):
  202.     COLUMNS = 80
  203.     EXIT_COMMANDS = ('quit', 'exit', 'bye')
  204.  
  205.     def __init__(self):
  206.         self.core = core.Core()
  207.         self._db = self.core.db
  208.         self._config = self.core.config
  209.         self._model = self.core.model
  210.  
  211.         self._current_action = ''
  212.         self._commands = dict((name.rstrip('_'), func)
  213.             for name, func in inspect.getmembers(self)
  214.             if inspect.ismethod(func) and not name.startswith('_'))
  215.         self._prefixes, self._expansions = self._build_prefixes_expansions()
  216.         self._prefixes.update({'?': 'help'})
  217.         self._valid_commands = sorted(self._prefixes.values())
  218.         gpodder.user_extensions.on_ui_initialized(self.core.model,
  219.                 self._extensions_podcast_update_cb,
  220.                 self._extensions_episode_download_cb)
  221.  
  222.     def _build_prefixes_expansions(self):
  223.         prefixes = {}
  224.         expansions = collections.defaultdict(list)
  225.         names = sorted(self._commands.keys())
  226.         names.extend(self.EXIT_COMMANDS)
  227.  
  228.         # Generator for all prefixes of a given string (longest first)
  229.         # e.g. ['gpodder', 'gpodde', 'gpodd', 'gpod', 'gpo', 'gp', 'g']
  230.         mkprefixes = lambda n: (n[:x] for x in xrange(len(n), 0, -1))
  231.  
  232.         # Return True if the given prefix is unique in "names"
  233.         is_unique = lambda p: len([n for n in names if n.startswith(p)]) == 1
  234.  
  235.         for name in names:
  236.             is_still_unique = True
  237.             unique_expansion = None
  238.             for prefix in mkprefixes(name):
  239.                 if is_unique(prefix):
  240.                     unique_expansion = '[%s]%s' % (prefix, name[len(prefix):])
  241.                     prefixes[prefix] = name
  242.                     continue
  243.  
  244.                 if unique_expansion is not None:
  245.                     expansions[prefix].append(unique_expansion)
  246.                     continue
  247.  
  248.         return prefixes, expansions
  249.  
  250.     def _extensions_podcast_update_cb(self, podcast):
  251.         self._info(_('Podcast update requested by extensions.'))
  252.         self._update_podcast(podcast)
  253.  
  254.     def _extensions_episode_download_cb(self, episode):
  255.         self._info(_('Episode download requested by extensions.'))
  256.         self._download_episode(episode)
  257.  
  258.     def _start_action(self, msg, *args):
  259.         line = util.convert_bytes(msg % args)
  260.         if len(line) > self.COLUMNS-7:
  261.             line = line[:self.COLUMNS-7-3] + '...'
  262.         else:
  263.             line = line + (' '*(self.COLUMNS-7-len(line)))
  264.         self._current_action = line
  265.         safe_print(self._current_action, end='')
  266.  
  267.     def _update_action(self, progress):
  268.         if have_ansi:
  269.             progress = '%3.0f%%' % (progress*100.,)
  270.             result = '['+inblue(progress)+']'
  271.             safe_print('\r' + self._current_action + result, end='')
  272.  
  273.     def _finish_action(self, success=True, skip=False):
  274.         if skip:
  275.             result = '['+inyellow('SKIP')+']'
  276.         elif success:
  277.             result = '['+ingreen('DONE')+']'
  278.         else:
  279.             result = '['+inred('FAIL')+']'
  280.  
  281.         if have_ansi:
  282.             safe_print('\r' + self._current_action + result)
  283.         else:
  284.             safe_print(result)
  285.         self._current_action = ''
  286.  
  287.     def _atexit(self):
  288.         self.core.shutdown()
  289.  
  290.     # -------------------------------------------------------------------
  291.  
  292.     def import_(self, url):
  293.         for channel in opml.Importer(url).items:
  294.             self.subscribe(channel['url'], channel.get('title'))
  295.  
  296.     def export(self, filename):
  297.         podcasts = self._model.get_podcasts()
  298.         opml.Exporter(filename).write(podcasts)
  299.  
  300.     def get_podcast(self, url, create=False, check_only=False):
  301.         """Get a specific podcast by URL
  302.  
  303.         Returns a podcast object for the URL or None if
  304.         the podcast has not been subscribed to.
  305.         """
  306.         url = util.normalize_feed_url(url)
  307.         if url is None:
  308.             self._error(_('Invalid url: %s') % url)
  309.             return None
  310.  
  311.         # Subscribe to new podcast
  312.         if create:
  313.             return self._model.load_podcast(url, create=True,
  314.                     max_episodes=self._config.max_episodes_per_feed)
  315.  
  316.         # Load existing podcast
  317.         for podcast in self._model.get_podcasts():
  318.             if podcast.url == url:
  319.                 return podcast
  320.  
  321.         if not check_only:
  322.             self._error(_('You are not subscribed to %s.') % url)
  323.         return None
  324.  
  325.     def subscribe(self, url, title=None):
  326.         existing = self.get_podcast(url, check_only=True)
  327.         if existing is not None:
  328.             self._error(_('Already subscribed to %s.') % existing.url)
  329.             return True
  330.  
  331.         try:
  332.             podcast = self.get_podcast(url, create=True)
  333.             if podcast is None:
  334.                 self._error(_('Cannot subscribe to %s.') % url)
  335.                 return True
  336.                 
  337.             if title is not None:
  338.                 podcast.rename(title)
  339.             podcast.save()
  340.         except Exception, e:
  341.             logger.warn('Cannot subscribe: %s', e, exc_info=True)
  342.             if hasattr(e, 'strerror'):
  343.                 self._error(e.strerror)
  344.             else:
  345.                 self._error(str(e))
  346.             return True
  347.  
  348.         self._db.commit()
  349.  
  350.         self._info(_('Successfully added %s.' % url))
  351.         return True
  352.  
  353.     def _print_config(self, search_for):
  354.         for key in self._config.all_keys():
  355.             if search_for is None or search_for.lower() in key.lower():
  356.                 value = config_value_to_string(self._config._lookup(key))
  357.                 safe_print(key, '=', value)
  358.  
  359.     def set(self, key=None, value=None):
  360.         if value is None:
  361.             self._print_config(key)
  362.             return
  363.  
  364.         try:
  365.             current_value = self._config._lookup(key)
  366.             current_type = type(current_value)
  367.         except KeyError:
  368.             self._error(_('This configuration option does not exist.'))
  369.             return
  370.  
  371.         if current_type == dict:
  372.             self._error(_('Can only set leaf configuration nodes.'))
  373.             return
  374.  
  375.         self._config.update_field(key, value)
  376.         self.set(key)
  377.  
  378.     @FirstArgumentIsPodcastURL
  379.     def rename(self, url, title):
  380.         podcast = self.get_podcast(url)
  381.  
  382.         if podcast is not None:
  383.             old_title = podcast.title
  384.             podcast.rename(title)
  385.             self._db.commit()
  386.             self._info(_('Renamed %(old_title)s to %(new_title)s.') % {
  387.                 'old_title': util.convert_bytes(old_title),
  388.                 'new_title': util.convert_bytes(title),
  389.             })
  390.  
  391.         return True
  392.  
  393.     @FirstArgumentIsPodcastURL
  394.     def unsubscribe(self, url):
  395.         podcast = self.get_podcast(url)
  396.  
  397.         if podcast is None:
  398.             self._error(_('You are not subscribed to %s.') % url)
  399.         else:
  400.             podcast.delete()
  401.             self._db.commit()
  402.             self._error(_('Unsubscribed from %s.') % url)
  403.  
  404.         return True
  405.         
  406.     def is_episode_new(self, episode):
  407.         return (episode.state == gpodder.STATE_NORMAL and episode.is_new)
  408.  
  409.     def _episodesList(self, podcast):
  410.         def status_str(episode):
  411.             # is new
  412.             if self.is_episode_new(episode):
  413.                 return u' * '
  414.             # is downloaded
  415.             if (episode.state == gpodder.STATE_DOWNLOADED):
  416.                 return u' Γûë '
  417.             # is deleted
  418.             if (episode.state == gpodder.STATE_DELETED):
  419.                 return u' Γûæ '
  420.     
  421.             return u'   '
  422.  
  423.         episodes = (u'%3d. %s %s' % (i+1, status_str(e), e.title)
  424.                 for i, e in enumerate(podcast.get_all_episodes()))
  425.         return episodes
  426.  
  427.     @FirstArgumentIsPodcastURL
  428.     def info(self, url):
  429.         podcast = self.get_podcast(url)
  430.  
  431.         if podcast is None:
  432.             self._error(_('You are not subscribed to %s.') % url)
  433.         else:
  434.             def feed_update_status_msg(podcast):
  435.                 if podcast.pause_subscription:
  436.                     return "disabled"
  437.                 return "enabled"
  438.             
  439.             title, url, status = podcast.title, podcast.url, \
  440.                 feed_update_status_msg(podcast)
  441.             episodes = self._episodesList(podcast)
  442.             episodes = u'\n      '.join(episodes)
  443.             self._pager(u"""
  444.     Title: %(title)s
  445.     URL: %(url)s
  446.     Feed update is %(status)s
  447.  
  448.     Episodes:
  449.       %(episodes)s
  450.             """ % locals())
  451.  
  452.         return True
  453.  
  454.     @FirstArgumentIsPodcastURL
  455.     def episodes(self, url=None):
  456.         output = []
  457.         for podcast in self._model.get_podcasts():
  458.             podcast_printed = False
  459.             if url is None or podcast.url == url:
  460.                 episodes = self._episodesList(podcast)
  461.                 episodes = u'\n      '.join(episodes)
  462.                 output.append(u"""
  463.     Episodes from %s:
  464.       %s
  465. """ % (podcast.url, episodes))
  466.  
  467.         self._pager(u'\n'.join(output))
  468.         return True
  469.  
  470.     def list(self):
  471.         for podcast in self._model.get_podcasts():
  472.             if not podcast.pause_subscription:
  473.                 safe_print('#', ingreen(podcast.title))
  474.             else:
  475.                 safe_print('#', inred(podcast.title),
  476.                         '-', _('Updates disabled'))
  477.  
  478.             safe_print(podcast.url)
  479.  
  480.         return True
  481.  
  482.     def _update_podcast(self, podcast):
  483.         self._start_action(' %s', podcast.title)
  484.         try:
  485.             podcast.update()
  486.             self._finish_action()
  487.         except Exception, e:
  488.             self._finish_action(False)
  489.  
  490.     def _pending_message(self, count):
  491.         return N_('%(count)d new episode', '%(count)d new episodes',
  492.                 count) % {'count': count}
  493.  
  494.     @FirstArgumentIsPodcastURL
  495.     def update(self, url=None):
  496.         count = 0
  497.         safe_print(_('Checking for new episodes'))
  498.         for podcast in self._model.get_podcasts():
  499.             if url is not None and podcast.url != url:
  500.                 continue
  501.  
  502.             if not podcast.pause_subscription:
  503.                 self._update_podcast(podcast)
  504.                 count += sum(1 for e in podcast.get_all_episodes() if self.is_episode_new(e))
  505.             else:
  506.                 self._start_action(_('Skipping %(podcast)s') % {
  507.                     'podcast': podcast.title})
  508.                 self._finish_action(skip=True)
  509.  
  510.         util.delete_empty_folders(gpodder.downloads)
  511.         safe_print(inblue(self._pending_message(count)))
  512.         return True
  513.  
  514.     @FirstArgumentIsPodcastURL
  515.     def pending(self, url=None):
  516.         count = 0
  517.         for podcast in self._model.get_podcasts():
  518.             podcast_printed = False
  519.             if url is None or podcast.url == url:
  520.                 for episode in podcast.get_all_episodes():
  521.                     if self.is_episode_new(episode):
  522.                         if not podcast_printed:
  523.                             safe_print('#', ingreen(podcast.title))
  524.                             podcast_printed = True
  525.                         safe_print(' ', episode.title)
  526.                         count += 1
  527.  
  528.         util.delete_empty_folders(gpodder.downloads)
  529.         safe_print(inblue(self._pending_message(count)))
  530.         return True
  531.  
  532.     def _download_episode(self, episode):
  533.         self._start_action('Downloading %s', episode.title)
  534.         
  535.         task = download.DownloadTask(episode, self._config)
  536.         task.add_progress_callback(self._update_action)
  537.         task.status = download.DownloadTask.QUEUED
  538.         task.run()
  539.         
  540.         self._finish_action()
  541.  
  542.     @FirstArgumentIsPodcastURL
  543.     def download(self, url=None):
  544.         episodes = []
  545.         for podcast in self._model.get_podcasts():
  546.             if url is None or podcast.url == url:
  547.                 for episode in podcast.get_all_episodes():
  548.                     if self.is_episode_new(episode):
  549.                         episodes.append(episode)
  550.  
  551.         if self._config.downloads.chronological_order:
  552.             # download older episodes first
  553.             episodes = list(model.Model.sort_episodes_by_pubdate(episodes))
  554.  
  555.         last_podcast = None
  556.         for episode in episodes:
  557.             if episode.channel != last_podcast:
  558.                 safe_print(inblue(episode.channel.title))
  559.                 last_podcast = episode.channel
  560.             self._download_episode(episode)
  561.  
  562.         util.delete_empty_folders(gpodder.downloads)
  563.         safe_print(len(episodes), 'episodes downloaded.')
  564.         return True
  565.  
  566.     @FirstArgumentIsPodcastURL
  567.     def disable(self, url):
  568.         podcast = self.get_podcast(url)
  569.  
  570.         if podcast is None:
  571.             self._error(_('You are not subscribed to %s.') % url)
  572.         else:
  573.             if not podcast.pause_subscription:
  574.                 podcast.pause_subscription = True
  575.                 podcast.save()
  576.             self._db.commit()
  577.             self._error(_('Disabling feed update from %s.') % url)
  578.  
  579.         return True
  580.  
  581.     @FirstArgumentIsPodcastURL
  582.     def enable(self, url):
  583.         podcast = self.get_podcast(url)
  584.  
  585.         if podcast is None:
  586.             self._error(_('You are not subscribed to %s.') % url)
  587.         else:
  588.             if podcast.pause_subscription:
  589.                 podcast.pause_subscription = False
  590.                 podcast.save()
  591.             self._db.commit()
  592.             self._error(_('Enabling feed update from %s.') % url)
  593.  
  594.         return True
  595.  
  596.     def youtube(self, url):
  597.         fmt_ids = youtube.get_fmt_ids(self._config.youtube)
  598.         yurl = youtube.get_real_download_url(url, fmt_ids)
  599.         safe_print(yurl)
  600.  
  601.         return True
  602.  
  603.     def webui(self, public=None):
  604.         from gpodder import webui
  605.         if public == 'public':
  606.             # Warn the user that the web UI is listening on all network
  607.             # interfaces, which could lead to problems.
  608.             # Only use this on a trusted, private network!
  609.             self._warn(_('Listening on ALL network interfaces.'))
  610.             webui.main(only_localhost=False, core=self.core)
  611.         else:
  612.             webui.main(core=self.core)
  613.  
  614.     def pipe(self):
  615.         from gpodder import pipe
  616.         pipe.main(core=self.core)
  617.  
  618.     def search(self, *terms):
  619.         query = ' '.join(terms)
  620.         if not query:
  621.             return
  622.  
  623.         directory = my.Directory()
  624.         results = directory.search(query)
  625.         self._show_directory_results(results)
  626.  
  627.     def toplist(self):
  628.         directory = my.Directory()
  629.         results = directory.toplist()
  630.         self._show_directory_results(results, True)
  631.  
  632.     def _show_directory_results(self, results, multiple=False):
  633.         if not results:
  634.             self._error(_('No podcasts found.'))
  635.             return
  636.  
  637.         if not interactive_console or is_single_command:
  638.             safe_print('\n'.join(url for title, url in results))
  639.             return
  640.  
  641.         def show_list():
  642.             self._pager('\n'.join(u'%3d: %s\n     %s' %
  643.                 (index+1, title, url if title != url else '')
  644.                 for index, (title, url) in enumerate(results)))
  645.  
  646.         show_list()
  647.  
  648.         msg = _('Enter index to subscribe, ? for list')
  649.         while True:
  650.             index = raw_input(msg + ': ')
  651.  
  652.             if not index:
  653.                 return
  654.  
  655.             if index == '?':
  656.                 show_list()
  657.                 continue
  658.  
  659.             try:
  660.                 index = int(index)
  661.             except ValueError:
  662.                 self._error(_('Invalid value.'))
  663.                 continue
  664.  
  665.             if not (1 <= index <= len(results)):
  666.                 self._error(_('Invalid value.'))
  667.                 continue
  668.  
  669.             title, url = results[index-1]
  670.             self._info(_('Adding %s...') % title)
  671.             self.subscribe(url)
  672.             if not multiple:
  673.                 break
  674.  
  675.     @FirstArgumentIsPodcastURL
  676.     def rewrite(self, old_url, new_url):
  677.         podcast = self.get_podcast(old_url)
  678.         if podcast is None:
  679.             self._error(_('You are not subscribed to %s.') % old_url)
  680.         else:
  681.             result = podcast.rewrite_url(new_url)
  682.             if result is None:
  683.                 self._error(_('Invalid URL: %s') % new_url)
  684.             else:
  685.                 new_url = result
  686.                 self._error(_('Changed URL from %(old_url)s to %(new_url)s.') %
  687.                 {
  688.                     'old_url': old_url,
  689.                     'new_url': new_url,
  690.                 })
  691.         return True
  692.  
  693.     def help(self):
  694.         safe_print(stylize(__doc__), file=sys.stderr, end='')
  695.         return True
  696.  
  697.     # -------------------------------------------------------------------
  698.  
  699.     def _pager(self, output):
  700.         if have_ansi:
  701.             # Need two additional rows for command prompt
  702.             rows_needed = len(output.splitlines()) + 2
  703.             rows, cols = get_terminal_size()
  704.             if rows_needed < rows:
  705.                 safe_print(output)
  706.             else:
  707.                 pydoc.pager(util.sanitize_encoding(output))
  708.         else:
  709.             safe_print(output)
  710.  
  711.     def _shell(self):
  712.         safe_print(os.linesep.join(x.strip() for x in ("""
  713.         gPodder %(__version__)s "%(__relname__)s" (%(__date__)s) - %(__url__)s
  714.         %(__copyright__)s
  715.         License: %(__license__)s
  716.  
  717.         Entering interactive shell. Type 'help' for help.
  718.         Press Ctrl+D (EOF) or type 'quit' to quit.
  719.         """ % gpodder.__dict__).splitlines()))
  720.  
  721.         if readline is not None:
  722.             readline.parse_and_bind('tab: complete')
  723.             readline.set_completer(self._tab_completion)
  724.             readline.set_completer_delims(' ')
  725.  
  726.         while True:
  727.             try:
  728.                 line = raw_input('gpo> ')
  729.             except EOFError:
  730.                 safe_print('')
  731.                 break
  732.             except KeyboardInterrupt:
  733.                 safe_print('')
  734.                 continue
  735.  
  736.             if self._prefixes.get(line, line) in self.EXIT_COMMANDS:
  737.                 break
  738.  
  739.             try:
  740.                 args = shlex.split(line)
  741.             except ValueError, value_error:
  742.                 self._error(_('Syntax error: %(error)s') %
  743.                         {'error': value_error})
  744.                 continue
  745.  
  746.             try:
  747.                 self._parse(args)
  748.             except KeyboardInterrupt:
  749.                 self._error('Keyboard interrupt.')
  750.             except EOFError:
  751.                 self._error('EOF.')
  752.  
  753.         self._atexit()
  754.  
  755.     def _error(self, *args):
  756.         safe_print(inred(' '.join(args)), file=sys.stderr)
  757.  
  758.     # Warnings look like error messages for now
  759.     _warn = _error
  760.  
  761.     def _info(self, *args):
  762.         safe_print(*args)
  763.  
  764.     def _checkargs(self, func, command_line):
  765.         args, varargs, keywords, defaults = inspect.getargspec(func)
  766.         args.pop(0) # Remove "self" from args
  767.         defaults = defaults or ()
  768.         minarg, maxarg = len(args)-len(defaults), len(args)
  769.  
  770.         if len(command_line) < minarg or (len(command_line) > maxarg and \
  771.                 varargs is None):
  772.             self._error('Wrong argument count for %s.' % func.__name__)
  773.             return False
  774.  
  775.         return func(*command_line)
  776.  
  777.     def _tab_completion_podcast(self, text, count):
  778.         """Tab completion for podcast URLs"""
  779.         urls = [p.url for p in self._model.get_podcasts() if text in p.url]
  780.         if count < len(urls):
  781.             return urls[count]
  782.  
  783.         return None
  784.  
  785.  
  786.     def _tab_completion(self, text, count):
  787.         """Tab completion function for readline"""
  788.         if readline is None:
  789.             return None
  790.  
  791.         current_line = readline.get_line_buffer()
  792.         if text == current_line:
  793.             for name in self._valid_commands:
  794.                 if name.startswith(text):
  795.                     if count == 0:
  796.                         return name
  797.                     else:
  798.                         count -= 1
  799.         else:
  800.             args = current_line.split()
  801.             command = args.pop(0)
  802.             command_function = getattr(self, command, None)
  803.             if not command_function:
  804.                 return None
  805.             if getattr(command_function, '_first_arg_is_podcast', False):
  806.                 if not args or (len(args) == 1 and not current_line.endswith(' ')):
  807.                     return self._tab_completion_podcast(text, count)
  808.  
  809.         return None
  810.  
  811.  
  812.     def _parse_single(self, command_line):
  813.         try:
  814.             result = self._parse(command_line)
  815.         except KeyboardInterrupt:
  816.             self._error('Keyboard interrupt.')
  817.             result = -1
  818.         self._atexit()
  819.         return result
  820.  
  821.     def _parse(self, command_line):
  822.         if not command_line:
  823.             return False
  824.  
  825.         command = command_line.pop(0)
  826.  
  827.         # Resolve command aliases
  828.         command = self._prefixes.get(command, command)
  829.  
  830.         if command in self._commands:
  831.             func = self._commands[command]
  832.             if inspect.ismethod(func):
  833.                 return self._checkargs(func, command_line)
  834.  
  835.         if command in self._expansions:
  836.             safe_print(_('Ambiguous command. Did you mean..'))
  837.             for cmd in self._expansions[command]:
  838.                 safe_print('   ', inblue(cmd))
  839.         else:
  840.             self._error(_('The requested function is not available.'))
  841.  
  842.         return False
  843.  
  844.  
  845. def stylize(s):
  846.     s = re.sub(r'    .{27}', lambda m: inblue(m.group(0)), s)
  847.     s = re.sub(r'  - .*', lambda m: ingreen(m.group(0)), s)
  848.     return s
  849.  
  850. if __name__ == '__main__':
  851.     logger = logging.getLogger(__name__)
  852.     cli = gPodderCli()
  853.     args = sys.argv[1:]
  854.     if args:
  855.         is_single_command = True
  856.         cli._parse_single(args)
  857.     elif interactive_console:
  858.         cli._shell()
  859.     else:
  860.         safe_print(__doc__, end='')
  861.  
  862.