#!/usr/bin/env python2 import sys, os, json, tempfile, urllib2, urllib, json from PyQt4 import QtGui, QtCore, QtWebKit, QtNetwork from threading import Thread settings = QtCore.QSettings("jabs.nu", "feedthemonkey") class MainWindow(QtGui.QMainWindow): def __init__(self): QtGui.QMainWindow.__init__(self) self.setWindowIcon(QtGui.QIcon("feedmonkey")) self.addAction(QtGui.QAction("Full Screen", self, checkable=True, toggled=lambda v: self.showFullScreen() if v else self.showNormal(), shortcut="F11")) self.history = self.get("history", []) self.restoreGeometry(QtCore.QByteArray.fromRawData(settings.value("geometry").toByteArray())) self.restoreState(QtCore.QByteArray.fromRawData(settings.value("state").toByteArray())) self.initUI() session_id = self.get("session_id") server_url = self.get("server_url") if not (session_id and server_url): self.authenticate() else: self.initApp() def initUI(self): self.content = Content(self) self.setCentralWidget(self.content) def mkAction(name, connect, shortcut=None): action = QtGui.QAction(name, self) action.triggered.connect(connect) if shortcut: action.setShortcut(shortcut) return action mb = self.menuBar() fileMenu = mb.addMenu("&File") fileMenu.addAction(mkAction("&Close", self.close, "Ctrl+W")) fileMenu.addAction(mkAction("&Log Out", self.content.reload)) fileMenu.addSeparator() fileMenu.addAction(mkAction("&Exit", self.close, "Ctrl+Q")) actionMenu = mb.addMenu("&Action") actionMenu.addAction(mkAction("&Reload", self.content.reload, "R")) actionMenu.addAction(mkAction("&Next", self.content.showNext, "J")) actionMenu.addAction(mkAction("&Previous", self.content.showPrevious, "K")) actionMenu.addAction(mkAction("&Open in Browser", self.content.openCurrent, "Return")) viewMenu = mb.addMenu("&View") viewMenu.addAction(mkAction("Zoom &In", lambda: self.content.wb.setZoomFactor(self.content.wb.zoomFactor() + 0.2), "Ctrl++")) viewMenu.addAction(mkAction("Zoom &Out", lambda: self.content.wb.setZoomFactor(self.content.wb.zoomFactor() - 0.2), "Ctrl+-")) viewMenu.addAction(mkAction("&Reset", lambda: self.content.wb.setZoomFactor(1), "Ctrl+0")) helpMenu = mb.addMenu("&Help") helpMenu.addAction(mkAction("&About", lambda: QtGui.QDesktopServices.openUrl(QtCore.QUrl("http://jabs.nu/feedthemonkey", QtCore.QUrl.TolerantMode)) )) def initApp(self): session_id = self.get("session_id") server_url = self.get("server_url") self.tinyTinyRSS = TinyTinyRSS(self, server_url, session_id) self.content.evaluateJavaScript("setArticle('loading')") self.content.reload() def closeEvent(self, ev): settings.setValue("geometry", self.saveGeometry()) settings.setValue("state", self.saveState()) return QtGui.QMainWindow.closeEvent(self, ev) def put(self, key, value): "Persist an object somewhere under a given key" settings.setValue(key, json.dumps(value)) settings.sync() def get(self, key, default=None): "Get the object stored under 'key' in persistent storage, or the default value" v = settings.value(key) return json.loads(unicode(v.toString())) if v.isValid() else default def setWindowTitle(self, t): super(QtGui.QMainWindow, self).setWindowTitle("Feed the Monkey" + t) def authenticate(self): dialog = Login() def callback(): server_url = str(dialog.textServerUrl.text()) user = str(dialog.textName.text()) password = str(dialog.textPass.text()) session_id = TinyTinyRSS.login(server_url, user, password) if session_id: self.put("session_id", session_id) self.put("server_url", server_url) self.initApp() else: self.authenticate() dialog.accepted.connect(callback) dialog.exec_() def logOut(self): self.content.evaluateJavaScript("setArticle()") self.tinyTinyRSS.logOut() self.tinyTinyRSS = None self.put("session_id", None) self.put("server_url", None) self.authenticate() class Content(QtGui.QWidget): def __init__(self, container): QtGui.QWidget.__init__(self) self.app = container self.index = 0 self.wb = QtWebKit.QWebView(titleChanged=lambda t: container.setWindowTitle(t)) self.wb.page().setLinkDelegationPolicy(QtWebKit.QWebPage.DelegateAllLinks) self.wb.linkClicked.connect(lambda url: self.openLink(url)) self.setLayout(QtGui.QVBoxLayout(spacing=0)) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().addWidget(self.wb) self.do_show_next = QtGui.QShortcut(QtCore.Qt.Key_Right, self, activated=self.showNext) self.do_show_previous = QtGui.QShortcut(QtCore.Qt.Key_Left, self, activated=self.showPrevious) self.wb.settings().setAttribute(QtWebKit.QWebSettings.PluginsEnabled, True) self.wb.settings().setIconDatabasePath(tempfile.mkdtemp()) self.wb.setHtml(self.templateString()) self.unread_articles = [] def openLink(self, url): QtGui.QDesktopServices.openUrl(url) def reload(self): w = WorkerThread(self.app, self._reload) self.connect(w, QtCore.SIGNAL("reload_done()"), self.reload_done) w.start() def _reload(self): self.unread_articles = self.app.tinyTinyRSS.getUnreadFeeds() self.index = -1 def reload_done(self): self.setUnreadCount() if len(self.unread_articles) > 0: self.showNext() def showNext(self): if self.index >= 0 and self.index < len(self.unread_articles): previous = self.unread_articles[self.index] self.app.tinyTinyRSS.setArticleRead(previous["id"]) if len(self.unread_articles) > self.index + 1: self.index += 1 current = self.unread_articles[self.index] self.setArticle(current) else: if self.index < len(self.unread_articles): self.index += 1 self.setUnreadCount() def showPrevious(self): if self.index > 0: self.index -= 1 previous = self.unread_articles[self.index] self.setArticle(previous) self.setUnreadCount() def openCurrent(self): current = self.unread_articles[self.index] url = QtCore.QUrl(current["link"]) self.openLink(url) def setArticle(self, article): func = u"setArticle({});".format(json.dumps(article)) self.evaluateJavaScript(func) def evaluateJavaScript(self, func): return self.wb.page().mainFrame().evaluateJavaScript(func) def setUnreadCount(self): length = len(self.unread_articles) i = 0 if self.index > 0: i = self.index unread = length - i self.app.setWindowTitle(" (" + str(unread) + "/" + str(length) + ")") if unread < 1: self.evaluateJavaScript("setArticle('empty')") def templateString(self): return """ ttrssl

""" class TinyTinyRSS: def __init__(self, app, server_url, session_id): self.app = app if server_url and session_id: self.server_url = server_url self.session_id = session_id else: self.app.authenticate() def doOperation(self, operation, options=None): url = self.server_url + "/api/" default_options = {'sid': self.session_id, 'op': operation} if options: options = dict(default_options.items() + options.items()) else: options = default_options json_string = json.dumps(options) req = urllib2.Request(url) fd = urllib2.urlopen(req, json_string) body = "" while True: data = fd.read(1024) if not len(data): break body += data return json.loads(body)["content"] def getUnreadFeeds(self): unread_articles = [] def more(skip): return self.doOperation("getHeadlines", {"show_excerpt": False, "view_mode": "unread", "show_content": True, "feed_id": -4, "skip": skip}) skip = 0 while True: new = more( skip) unread_articles += new length = len(new) if length < 1: break skip += length return unread_articles def setArticleRead(self, article_id): l = lambda: self.doOperation("updateArticle", {'article_ids':article_id, 'mode': 0, 'field': 2}) t = Thread(target=l) t.start() def logOut(self): self.doOperation("logout") @classmethod def login(self, server_url, user, password): url = server_url + "/api/" options = {"op": "login", "user": user, "password": password} json_string = json.dumps(options) req = urllib2.Request(url) fd = urllib2.urlopen(req, json_string) body = "" while 1: data = fd.read(1024) if not len(data): break body += data body = json.loads(body)["content"] if body.has_key("error"): msgBox = QtGui.QMessageBox() msgBox.setText(body["error"]) msgBox.exec_() return None return body["session_id"] class Login(QtGui.QDialog): def __init__(self): QtGui.QDialog.__init__(self) self.setWindowIcon(QtGui.QIcon("feedmonkey.png")) self.setWindowTitle("Feed the Monkey - Login") self.label = QtGui.QLabel(self) self.label.setText("Please specify a server url, a username and a password.") self.textServerUrl = QtGui.QLineEdit(self) self.textServerUrl.setPlaceholderText("http://example.com/ttrss/") self.textServerUrl.setText("http://") self.textName = QtGui.QLineEdit(self) self.textName.setPlaceholderText("username") self.textPass = QtGui.QLineEdit(self) self.textPass.setEchoMode(QtGui.QLineEdit.Password); self.textPass.setPlaceholderText("password") self.buttons = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok) self.buttons.accepted.connect(self.accept) layout = QtGui.QVBoxLayout(self) layout.addWidget(self.label) layout.addWidget(self.textServerUrl) layout.addWidget(self.textName) layout.addWidget(self.textPass) layout.addWidget(self.buttons) class WorkerThread(QtCore.QThread): def __init__(self, parent, do_reload): super(WorkerThread, self).__init__(parent) self.do_reload = do_reload self.isRunning = True def run(self): self.do_reload() self.emit(QtCore.SIGNAL("reload_done()")) self.isRunning = False if __name__ == "__main__": app = QtGui.QApplication(sys.argv) wb = MainWindow() wb.show() sys.exit(app.exec_())