From 24feaf4eea122239347266fdd1c6a9a7e800fa82 Mon Sep 17 00:00:00 2001 From: Thomas Perl Date: Sun, 25 Nov 2012 00:49:42 +0100 Subject: [PATCH] Pipe: Experimental pipe-based IPC interface --- bin/gpo | 6 ++ src/gpodder/pipe/__init__.py | 202 +++++++++++++++++++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 src/gpodder/pipe/__init__.py diff --git a/bin/gpo b/bin/gpo index 9409afac..bbd1dabb 100755 --- a/bin/gpo +++ b/bin/gpo @@ -61,6 +61,7 @@ rewrite OLDURL NEWURL Change the feed URL of [OLDURL] to [NEWURL] webui [public] Start gPodder's Web UI server (public = listen on all network interfaces) + pipe Start gPodder in pipe-based IPC server mode """ @@ -113,6 +114,7 @@ import gpodder _ = gpodder.gettext N_ = gpodder.ngettext +gpodder.images_folder = os.path.join(prefix, 'share', 'gpodder', 'images') gpodder.prefix = prefix # This is the command-line UI variant @@ -584,6 +586,10 @@ class gPodderCli(object): else: webui.main(core=self.core) + def pipe(self): + from gpodder import pipe + pipe.main(core=self.core) + def search(self, *terms): query = ' '.join(terms) if not query: diff --git a/src/gpodder/pipe/__init__.py b/src/gpodder/pipe/__init__.py new file mode 100644 index 00000000..79126b6d --- /dev/null +++ b/src/gpodder/pipe/__init__.py @@ -0,0 +1,202 @@ +# -*- coding: utf-8 -*- +# +# gPodder - A media aggregator and podcast client +# Copyright (c) 2005-2012 Thomas Perl and the gPodder Team +# +# gPodder is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# gPodder is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# gpodder.pipe - Socket/pipe-based backend for New UIs +# Thomas Perl ; 2012-11-24 + + +import gpodder + +from gpodder import core +from gpodder import model +from gpodder import util +from gpodder import coverart + +try: + # For Python < 2.6, we use the "simplejson" add-on module + import simplejson as json +except ImportError: + # Python 2.6 already ships with a nice "json" module + import json + +import os +import re +import sys +import threading +import Queue +import time +import fcntl + +def cmd(*signature): + def wrapper(f): + setattr(f, '_pipecmd', True) + return f + return wrapper + +def to_json(o): + keys = list(('id',) + o.__slots__) + values = list(getattr(o, key) for key in keys) + yield keys + yield values + +class PipeError(BaseException): pass + +class Pipe: + def __init__(self, core, reader, writer): + self.core = core + + self.model = self.core.model + self.cover_download = coverart.CoverDownloader() + + self.reader = reader + self.writer = writer + self.events_in = Queue.Queue() + self.events_out = Queue.Queue() + + def event_writer_proc(self): + while True: + item = self.events_out.get(True) + self.writer.write(item.encode('utf-8') + '\n') + self.writer.flush() + + def event_reader_proc(self): + while True: + args = self.events_in.get(True) + cmd = args.pop(0) + func = getattr(self, cmd, None) + if func is not None: + if getattr(func, '_pipecmd', False): + try: + result = func(*args) + if result: + self.event_out(result) + except PipeError, e: + self.event_out('! %s' % e) + except Exception, e: + print >>sys.stderr, 'FAIL:', e + self.event_out('! %s' % e) + raise + continue + + self.event_out('? %s' % line) + + def event_in(self, data): + self.events_in.put(data) + + def event_out(self, data): + self.events_out.put(data) + + def find_episode(self, id): + for podcast in self.model.get_podcasts(): + for episode in podcast.get_all_episodes(): + if episode.id == int(id): + return episode + raise PipeError('episode not found') + + def find_podcast(self, id): + for podcast in self.model.get_podcasts(): + if podcast.id == int(id): + return podcast + raise PipeError('podcast not found') + + def summarize_podcasts(self, podcasts): + yield ('id', 'title', 'downloads', 'cover') + for podcast in podcasts: + total, deleted, new, downloaded, unplayed = podcast.get_statistics() + cover_filename = self.cover_download.get_cover(podcast.cover_file, + podcast.cover_url, podcast.url, podcast.title, + podcast.auth_username, podcast.auth_password, True) + yield (podcast.id, podcast.title, downloaded, cover_filename) + + def summarize_episodes(self, episodes): + yield ('id', 'title', 'state') + for episode in episodes: + yield (episode.id, episode.title, episode.state) + + def serialize(self, data): + return json.dumps(list(data), ensure_ascii=False) + + @cmd() + def podcasts(self): + return 'podcasts ' + self.serialize(self.summarize_podcasts(self.model.get_podcasts())) + + @cmd(int) + def episodes(self, id): + podcast = self.find_podcast(id) + return 'episodes ' + id + ' ' + self.serialize(self.summarize_episodes(podcast.get_all_episodes())) + + @cmd(int) + def episode(self, id): + episode = self.find_episode(id) + return 'episode ' + id + ' ' + self.serialize(to_json(episode)) + + @cmd(int) + def podcast(self, id): + podcast = self.find_podcast(id) + return 'podcast ' + id + ' ' + self.serialize(to_json(podcast)) + + @cmd() + def update_all(self): + @util.run_in_background + def update_proc(): + for podcast in self.model.get_podcasts(): + self.event_out('updating %d' % podcast.id) + podcast.update() + self.event_out('updated %d' % podcast.id) + self.event_out('updated_all') + + @cmd(int) + def update(self, id): + @util.run_in_background + def update_proc(): + podcast = self.find_podcast(id) + self.event_out('updating %d' % podcast.id) + podcast.update() + self.event_out('updated %d' % podcast.id) + + def run(self): + def post_random_events(): + while True: + time.sleep(1) + self.event_out('hello') + time.sleep(2) + self.event_out('hello 10') + random_event_thread = threading.Thread(target=post_random_events) + random_event_thread.setDaemon(True) + #random_event_thread.start() + + reader_thread = threading.Thread(target=self.event_reader_proc) + writer_thread = threading.Thread(target=self.event_writer_proc) + reader_thread.setDaemon(True) + writer_thread.setDaemon(True) + reader_thread.start() + writer_thread.start() + + while True: + line = self.reader.readline() + if not line: + break + line = line.rstrip() + if line: + self.event_in(line.split()) + +def main(core): + pipe = Pipe(core, sys.stdin, sys.stdout) + pipe.run() +