Use dict-based format strings for numbers (bug 1165)
[gpodder.git] / bin / gpo
blobfd57ab62d3fa1ee5eb62ea93f41079161a039280
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
5 # gPodder - A media aggregator and podcast client
6 # Copyright (c) 2005-2010 Thomas Perl and the gPodder Team
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.
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.
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/>.
23 # gpo - A better command-line interface to gPodder using the gPodder API
24 # by Thomas Perl <thp@gpodder.org>; 2009-05-07
27 """
28 Usage: gpo [COMMAND] [params...]
30 - Subscription management -
32 subscribe URL [TITLE] Subscribe to a new feed at URL (as TITLE)
33 rename URL TITLE Rename feed at URL to TITLE
34 unsubscribe URL Unsubscribe from feed at URL
35 enable URL Enable feed updates for the feed at URL
36 disable URL Disable feed updates for the feed at URL
38 info URL Show information about feed at URL
39 list List all subscribed podcasts
40 update [URL] Check for new episodes (all or only at URL)
42 - Episode management -
44 download [URL] Download new episodes (all or only from URL)
45 pending [URL] List new episodes (all or only from URL)
46 episodes [URL] List episodes (all or only from URL)
48 - Other commands -
50 sync Synchronize downloaded episodes to device
51 youtube [URL] Resolve the YouTube URL to a download URL
53 """
55 import sys
56 import os
57 import re
58 import inspect
60 gpodder_script = sys.argv[0]
61 if os.path.islink(gpodder_script):
62 gpodder_script = os.readlink(gpodder_script)
63 gpodder_dir = os.path.join(os.path.dirname(gpodder_script), '..')
64 prefix = os.path.abspath(os.path.normpath(gpodder_dir))
66 src_dir = os.path.join(prefix, 'src')
67 data_dir = os.path.join(prefix, 'data')
69 if os.path.exists(src_dir) and os.path.exists(data_dir) and \
70 not prefix.startswith('/usr'):
71 # Run gPodder from local source folder (not installed)
72 sys.path.insert(0, src_dir)
75 import gpodder
76 _ = gpodder.gettext
78 # Use only the gPodder API here, so this serves both as an example
79 # and as a motivation to provide all functionality in the API :)
80 from gpodder import api
82 def inred(x):
83 return '\033[91m' + x + '\033[0m'
85 def ingreen(x):
86 return '\033[92m' + x + '\033[0m'
88 def inblue(x):
89 return '\033[94m' + x + '\033[0m'
91 class gPodderCli(object):
92 COLUMNS = 80
94 def __init__(self):
95 self.client = api.PodcastClient()
96 self._current_action = ''
98 def _start_action(self, msg, *args):
99 line = msg % args
100 if len(line) > self.COLUMNS-7:
101 line = line[:self.COLUMNS-7-3] + '...'
102 else:
103 line = line + (' '*(self.COLUMNS-7-len(line)))
104 self._current_action = line
105 sys.stdout.write(line)
106 sys.stdout.flush()
108 def _update_action(self, progress):
109 progress = '%3.0f%%' % (progress*100.,)
110 result = '['+inblue(progress)+']'
111 sys.stdout.write('\r' + self._current_action + result)
112 sys.stdout.flush()
114 def _finish_action(self, success=True):
115 result = '['+ingreen('DONE')+']' if success else '['+inred('FAIL')+']'
116 print '\r' + self._current_action + result
117 self._current_action = ''
119 # -------------------------------------------------------------------
121 def subscribe(self, url, title=None):
122 if self.client.get_podcast(url) is not None:
123 self._info(_('You are already subscribed to %s.' % url))
124 return True
126 if self.client.create_podcast(url, title) is None:
127 self._error(_('Cannot download feed for %s.') % url)
128 return True
130 self.client.finish()
132 self._info(_('Successfully added %s.' % url))
133 return True
135 def rename(self, url, title):
136 podcast = self.client.get_podcast(url)
138 if podcast is None:
139 self._error(_('You are not subscribed to %s.') % url)
140 else:
141 old_title = podcast.title
142 podcast.rename(title)
143 self.client.finish()
144 self._info(_('Renamed %s to %s.') % (old_title, title))
146 return True
148 def unsubscribe(self, url):
149 podcast = self.client.get_podcast(url)
151 if podcast is None:
152 self._error(_('You are not subscribed to %s.') % url)
153 else:
154 podcast.delete()
155 self.client.finish()
156 self._error(_('Unsubscribed from %s.') % url)
158 return True
160 def _episodesList(self, podcast):
161 def status_str(episode):
162 if episode.is_new:
163 return ' * '
164 if episode.is_downloaded:
165 return ' ▉ '
166 if episode.is_deleted:
167 return ' ░ '
169 return ' '
171 episodes = ('%3d. %s %s' % (i+1, status_str(e), e.title) for i, e in enumerate(podcast.get_episodes()))
172 return episodes
174 def info(self, url):
175 podcast = self.client.get_podcast(url)
177 if podcast is None:
178 self._error(_('You are not subscribed to %s.') % url)
179 else:
180 title, url, status = podcast.title, podcast.url, podcast.feed_update_status_msg()
181 episodes = self._episodesList(podcast)
182 episodes = '\n '.join(episodes)
183 print >>sys.stdout, """
184 Title: %(title)s
185 URL: %(url)s
186 Feed update is %(status)s
188 Episodes:
189 %(episodes)s
190 """ % locals()
192 return True
194 def episodes(self, url=None):
195 for podcast in self.client.get_podcasts():
196 podcast_printed = False
197 if url is None or podcast.url == url:
198 episodes = self._episodesList(podcast)
199 episodes = '\n '.join(episodes)
200 print >>sys.stdout, """
201 Episodes from %s:
203 """ % (podcast.url, episodes)
204 return True
206 def list(self):
207 for podcast in self.client.get_podcasts():
208 print podcast.url
210 return True
212 def update(self, url=None):
213 for podcast in self.client.get_podcasts():
214 if url is None and podcast.update_enabled():
215 self._start_action('Updating %s', podcast.title)
216 podcast.update()
217 self._finish_action()
218 elif podcast.url == url:
219 # Don't need to check for update_enabled()
220 self._start_action('Updating %s', podcast.title)
221 podcast.update()
222 self._finish_action()
224 return True
226 def pending(self, url=None):
227 count = 0
228 for podcast in self.client.get_podcasts():
229 podcast_printed = False
230 if url is None or podcast.url == url:
231 for episode in podcast.get_episodes():
232 if episode.is_new:
233 if not podcast_printed:
234 print podcast.title
235 podcast_printed = True
236 print ' ', episode.title
237 count += 1
239 print count, 'episodes pending.'
240 return True
242 def download(self, url=None):
243 count = 0
244 for podcast in self.client.get_podcasts():
245 podcast_printed = False
246 if url is None or podcast.url == url:
247 for episode in podcast.get_episodes():
248 if episode.is_new:
249 if not podcast_printed:
250 print inblue(podcast.title)
251 podcast_printed = True
252 self._start_action('Downloading %s', episode.title)
253 episode.download(self._update_action)
254 self._finish_action()
255 count += 1
257 print count, 'episodes downloaded.'
258 return True
260 def disable(self, url):
261 podcast = self.client.get_podcast(url)
263 if podcast is None:
264 self._error(_('You are not subscribed to %s.') % url)
265 else:
266 podcast.disable()
267 self.client.finish()
268 self._error(_('Disabling feed update from %s.') % url)
270 return True
272 def enable(self, url):
273 podcast = self.client.get_podcast(url)
275 if podcast is None:
276 self._error(_('You are not subscribed to %s.') % url)
277 else:
278 podcast.enable()
279 self.client.finish()
280 self._error(_('Enabling feed update from %s.') % url)
282 return True
284 def sync(self):
285 self.client.synchronize_device()
287 def youtube(self, url):
288 yurl = self.client.youtube_url_resolver(url)
289 print yurl
290 return True
292 # -------------------------------------------------------------------
294 def _error(self, *args):
295 print >>sys.stderr, inred(' '.join(args))
297 def _info(self, *args):
298 print >>sys.stdout, ' '.join(args)
300 def _checkargs(self, func, command_line):
301 args, varargs, keywords, defaults = inspect.getargspec(func)
302 args.pop(0) # Remove "self" from args
303 defaults = defaults or ()
304 minarg, maxarg = len(args)-len(defaults), len(args)
306 if len(command_line) < minarg or len(command_line) > maxarg:
307 self._error('Wrong argument count for %s.' % func.__name__)
308 return False
310 return func(*command_line)
313 def _parse(self, command_line):
314 if not command_line:
315 return False
317 command = command_line.pop(0)
318 if command.startswith('_'):
319 self._error(_('This command is not available.'))
320 return False
322 for name, func in inspect.getmembers(self):
323 if inspect.ismethod(func) and name == command:
324 return self._checkargs(func, command_line)
326 self._error(_('The requested function is not available.'))
327 return False
330 def stylize(s):
331 s = re.sub(r' .{27}', lambda m: inblue(m.group(0)), s)
332 s = re.sub(r' - .*', lambda m: ingreen(m.group(0)), s)
333 return s
335 if __name__ == '__main__':
336 cli = gPodderCli()
337 cli._parse(sys.argv[1:]) or sys.stderr.write(stylize(__doc__))