#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
This program 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.
This program 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 <http://www.gnu.org/licenses/>.
@author: mkaay
"""
import sys
from uuid import uuid4 as uuid # should be above PyQt imports
from time import sleep, time
from base64 import b64decode
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import re
import gettext
from os.path import abspath
from os.path import join
from os.path import basename
from os.path import commonprefix
from module import InitHomeDir
from module.gui.ConnectionManager import *
from module.gui.Connector import Connector
from module.gui.MainWindow import *
from module.gui.Queue import *
from module.gui.Overview import *
from module.gui.Collector import *
from module.gui.XMLParser import *
from module.gui.CoreConfigParser import ConfigParser
from module.lib.rename_process import renameProcess
from module.remote.thriftbackend.thriftgen.pyload.ttypes import *
try:
import pynotify
except ImportError:
print "pynotify not installed, falling back to qt tray notification"
def formatSpeed(speed):
speed = int(speed)
steps = 0
sizes = ["B/s", "KiB/s", "MiB/s", "GiB/s"]
while speed > 1000:
speed /= 1024.0
steps += 1
return "%.2f %s" % (speed, sizes[steps])
def formatSize(size):
"""formats size of bytes"""
size = int(size)
steps = 0
sizes = ["B", "KiB", "MiB", "GiB", "TiB"]
while size > 1000:
size /= 1024.0
steps += 1
return "%.2f %s" % (size, sizes[steps])
class main(QObject):
def __init__(self):
"""
main setup
"""
QObject.__init__(self)
self.app = QApplication(sys.argv)
self.path = pypath
self.homedir = abspath("")
self.configdir = ""
self.init(True)
def init(self, first=False):
"""
set main things up
"""
self.parser = XMLParser(join(self.configdir, "gui.xml"), join(self.path, "module", "config", "gui_default.xml"))
lang = self.parser.xml.elementsByTagName("language").item(0).toElement().text()
if not lang:
parser = XMLParser(join(self.path, "module", "config", "gui_default.xml"))
lang = parser.xml.elementsByTagName("language").item(0).toElement().text()
translation = gettext.translation("pyLoadGui", join(pypath, "locale"), languages=["en", str(lang)])
try:
translation.install(unicode=(True if sys.stdout.encoding.lower().startswith("utf") else False))
except:
translation.install(unicode=False)
self.connector = Connector()
self.mainWindow = MainWindow(self.connector)
self.connWindow = ConnectionManager()
self.mainloop = self.Loop(self)
self.connectSignals()
self.checkClipboard = False
default = self.refreshConnections()
self.connData = None
self.captchaProcessing = False
self.serverStatus = {"freespace":0}
self.core = None # pyLoadCore if started
self.connectionLost = False
if True: # when used if first, minimizing not working correctly..
self.tray = TrayIcon()
self.tray.show()
self.notification = Notification(self.tray)
self.connect(self, SIGNAL("showMessage"), self.notification.showMessage)
self.connect(self.tray.exitAction, SIGNAL("triggered()"), self.slotQuit)
self.connect(self.tray.showAction, SIGNAL("toggled(bool)"), self.mainWindow.setVisible)
self.connect(self.mainWindow, SIGNAL("hidden"), self.tray.mainWindowHidden)
if not first:
self.connWindow.show()
else:
self.connWindow.edit.setData(default)
data = self.connWindow.edit.getData()
self.slotConnect(data)
def startMain(self):
"""
start all refresh threads and show main window
"""
if not self.connector.connectProxy():
self.init()
return
self.connect(self.connector, SIGNAL("connectionLost"), self.slotConnectionLost)
self.restoreMainWindow()
self.mainWindow.show()
self.initQueue()
self.initPackageCollector()
self.mainloop.start()
self.clipboard = self.app.clipboard()
self.connect(self.clipboard, SIGNAL('dataChanged()'), self.slotClipboardChange)
self.mainWindow.actions["clipboard"].setChecked(self.checkClipboard)
self.mainWindow.tabs["settings"]["w"].setConnector(self.connector)
self.mainWindow.tabs["settings"]["w"].loadConfig()
self.tray.showAction.setDisabled(False)
def stopMain(self):
"""
stop all refresh threads and hide main window
"""
self.tray.showAction.setDisabled(True)
self.disconnect(self.clipboard, SIGNAL('dataChanged()'), self.slotClipboardChange)
self.disconnect(self.connector, SIGNAL("connectionLost"), self.slotConnectionLost)
self.mainloop.stop()
self.mainWindow.saveWindow()
self.mainWindow.hide()
self.queue.stop()
def connectSignals(self):
"""
signal and slot stuff, yay!
"""
self.connect(self.connector, SIGNAL("errorBox"), self.slotErrorBox)
self.connect(self.connWindow, SIGNAL("saveConnection"), self.slotSaveConnection)
self.connect(self.connWindow, SIGNAL("removeConnection"), self.slotRemoveConnection)
self.connect(self.connWindow, SIGNAL("connect"), self.slotConnect)
self.connect(self.mainWindow, SIGNAL("connector"), self.slotShowConnector)
self.connect(self.mainWindow, SIGNAL("addPackage"), self.slotAddPackage)
self.connect(self.mainWindow, SIGNAL("setDownloadStatus"), self.slotSetDownloadStatus)
self.connect(self.mainWindow, SIGNAL("saveMainWindow"), self.slotSaveMainWindow)
self.connect(self.mainWindow, SIGNAL("pushPackageToQueue"), self.slotPushPackageToQueue)
self.connect(self.mainWindow, SIGNAL("restartDownload"), self.slotRestartDownload)
self.connect(self.mainWindow, SIGNAL("removeDownload"), self.slotRemoveDownload)
self.connect(self.mainWindow, SIGNAL("abortDownload"), self.slotAbortDownload)
self.connect(self.mainWindow, SIGNAL("addContainer"), self.slotAddContainer)
self.connect(self.mainWindow, SIGNAL("stopAllDownloads"), self.slotStopAllDownloads)
self.connect(self.mainWindow, SIGNAL("setClipboardStatus"), self.slotSetClipboardStatus)
self.connect(self.mainWindow, SIGNAL("changePackageName"), self.slotChangePackageName)
self.connect(self.mainWindow, SIGNAL("pullOutPackage"), self.slotPullOutPackage)
self.connect(self.mainWindow, SIGNAL("refreshStatus"), self.slotRefreshStatus)
self.connect(self.mainWindow, SIGNAL("setPriority"), self.slotSetPriority)
self.connect(self.mainWindow, SIGNAL("reloadAccounts"), self.slotReloadAccounts)
self.connect(self.mainWindow, SIGNAL("quit"), self.quit)
self.connect(self.mainWindow.mactions["exit"], SIGNAL("triggered()"), self.slotQuit)
self.connect(self.mainWindow.captchaDock, SIGNAL("done"), self.slotCaptchaDone)
def slotShowConnector(self):
"""
emitted from main window (menu)
hide the main window and show connection manager
(to switch to other core)
"""
self.quitInternal()
self.stopMain()
self.init()
def quit(self):
"""
quit gui
"""
self.app.quit()
def loop(self):
"""
start application loop
"""
sys.exit(self.app.exec_())
def slotErrorBox(self, msg):
"""
display a nice error box
"""
msgb = QMessageBox(QMessageBox.Warning, "Error", msg)
msgb.exec_()
def initPackageCollector(self):
"""
init the package collector view
* columns
* selection
* refresh thread
* drag'n'drop
"""
view = self.mainWindow.tabs["collector"]["package_view"]
view.setSelectionBehavior(QAbstractItemView.SelectRows)
view.setSelectionMode(QAbstractItemView.ExtendedSelection)
def dropEvent(klass, event):
event.setDropAction(Qt.CopyAction)
event.accept()
view = event.source()
if view == klass:
items = view.selectedItems()
for item in items:
if not hasattr(item.parent(), "getPackData"):
continue
target = view.itemAt(event.pos())
if not hasattr(target, "getPackData"):
target = target.parent()
klass.emit(SIGNAL("droppedToPack"), target.getPackData()["id"], item.getFileData()["id"])
event.ignore()
return
items = view.selectedItems()
for item in items:
row = view.indexOfTopLevelItem(item)
view.takeTopLevelItem(row)
def dragEvent(klass, event):
view = event.source()
#dragOkay = False
#items = view.selectedItems()
#for item in items:
# if hasattr(item, "_data"):
# if item._data["id"] == "fixed" or item.parent()._data["id"] == "fixed":
# dragOkay = True
# else:
# dragOkay = True
#if dragOkay:
event.accept()
#else:
# event.ignore()
view.dropEvent = dropEvent
view.dragEnterEvent = dragEvent
view.setDragEnabled(True)
view.setDragDropMode(QAbstractItemView.DragDrop)
view.setDropIndicatorShown(True)
view.setDragDropOverwriteMode(True)
view.connect(view, SIGNAL("droppedToPack"), self.slotAddFileToPackage)
#self.packageCollector = PackageCollector(view, self.connector)
self.packageCollector = view.model()
def initQueue(self):
"""
init the queue view
* columns
* progressbar
"""
view = self.mainWindow.tabs["queue"]["view"]
view.setSelectionBehavior(QAbstractItemView.SelectRows)
view.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.queue = view.model()
self.connect(self.queue, SIGNAL("updateCount"), self.slotUpdateCount)
overview = self.mainWindow.tabs["overview"]["view"].model()
overview.queue = self.queue
self.connect(self.queue, SIGNAL("updateCount"), overview.queueChanged)
self.queue.start()
def slotUpdateCount(self, pc, fc):
self.mainWindow.packageCount.setText("%i" % pc)
self.mainWindow.fileCount.setText("%i" % fc)
def refreshServerStatus(self):
"""
refresh server status and overall speed in the status bar
"""
s = self.connector.statusServer()
if s.pause:
self.mainWindow.status.setText(_("paused"))
else:
self.mainWindow.status.setText(_("running"))
self.mainWindow.speed.setText(formatSpeed(s.speed))
self.mainWindow.space.setText(formatSize(self.serverStatus["freespace"]))
self.mainWindow.actions["toggle_status"].setChecked(not s.pause)
def refreshLog(self):
"""
update log window
"""
offset = self.mainWindow.tabs["log"]["text"].logOffset
lines = self.connector.getLog(offset)
if not lines:
return
self.mainWindow.tabs["log"]["text"].logOffset += len(lines)
for line in lines:
self.mainWindow.tabs["log"]["text"].emit(SIGNAL("append(QString)"), line.strip("\n"))
cursor = self.mainWindow.tabs["log"]["text"].textCursor()
cursor.movePosition(QTextCursor.End, QTextCursor.MoveAnchor)
self.mainWindow.tabs["log"]["text"].setTextCursor(cursor)
def getConnections(self):
"""
parse all connections in the config file
"""
connectionsNode = self.parser.xml.elementsByTagName("connections").item(0)
if connectionsNode.isNull():
raise Exception("null")
connections = self.parser.parseNode(connectionsNode)
ret = []
for conn in connections:
data = {}
data["type"] = conn.attribute("type", "remote")
data["default"] = conn.attribute("default", "False")
data["id"] = conn.attribute("id", uuid().hex)
if data["default"] == "True":
data["default"] = True
else:
data["default"] = False
subs = self.parser.parseNode(conn, "dict")
if not subs.has_key("name"):
data["name"] = _("Unnamed")
else:
data["name"] = subs["name"].text()
if data["type"] == "remote":
if not subs.has_key("server"):
continue
else:
data["host"] = subs["server"].text()
data["user"] = subs["server"].attribute("user", "admin")
data["port"] = int(subs["server"].attribute("port", "7227"))
data["password"] = subs["server"].attribute("password", "")
ret.append(data)
return ret
def slotSaveConnection(self, data):
"""
save connection to config file
"""
connectionsNode = self.parser.xml.elementsByTagName("connections").item(0)
if connectionsNode.isNull():
raise Exception("null")
connections = self.parser.parseNode(connectionsNode)
connNode = self.parser.xml.createElement("connection")
connNode.setAttribute("default", str(data["default"]))
connNode.setAttribute("type", data["type"])
connNode.setAttribute("id", data["id"])
nameNode = self.parser.xml.createElement("name")
nameText = self.parser.xml.createTextNode(data["name"])
nameNode.appendChild(nameText)
connNode.appendChild(nameNode)
if data["type"] == "remote":
serverNode = self.parser.xml.createElement("server")
serverNode.setAttribute("user", data["user"])
serverNode.setAttribute("port", data["port"])
serverNode.setAttribute("password", data["password"])
hostText = self.parser.xml.createTextNode(data["host"])
serverNode.appendChild(hostText)
connNode.appendChild(serverNode)
found = False
for c in connections:
cid = c.attribute("id", "None")
if str(cid) == str(data["id"]):
found = c
break
if found:
connectionsNode.replaceChild(connNode, found)
else:
connectionsNode.appendChild(connNode)
self.parser.saveData()
self.refreshConnections()
def slotRemoveConnection(self, data):
"""
remove connection from config file
"""
connectionsNode = self.parser.xml.elementsByTagName("connections").item(0)
if connectionsNode.isNull():
raise Exception("null")
connections = self.parser.parseNode(connectionsNode)
found = False
for c in connections:
cid = c.attribute("id", "None")
if str(cid) == str(data["id"]):
found = c
break
if found:
connectionsNode.removeChild(found)
self.parser.saveData()
self.refreshConnections()
def slotConnect(self, data):
"""
connect to a core
if connection is local, parse the core config file for data
if internal, start pyLoadCore
set up connector, show main window
"""
self.connWindow.hide()
if data["type"] not in ("remote","internal"):
coreparser = ConfigParser(self.configdir)
if not coreparser.config:
self.connector.setConnectionData("127.0.0.1", 7227, "anonymous", "anonymous", False)
else:
self.connector.setConnectionData("127.0.0.1", coreparser.get("remote","port"), "anonymous", "anonymous")
elif data["type"] == "remote":
self.connector.setConnectionData(data["host"], data["port"], data["user"], data["password"])
elif data["type"] == "internal":
from pyLoadCore import Core
from module.ConfigParser import ConfigParser as CoreConfig
import thread
if not self.core:
config = CoreConfig() #create so at least default config exists
self.core = Core()
self.core.startedInGui = True
thread.start_new_thread(self.core.start, (True, False))
while not self.core.running:
sleep(0.5)
self.connector.setConnectionData("127.0.0.1", config.get("remote","port"), "anonymous", "anonymous")
self.startMain()
try:
host = data["host"]
except:
host = "127.0.0.1"
def refreshConnections(self):
"""
reload connetions and display them
"""
self.parser.loadData()
conns = self.getConnections()
self.connWindow.emit(SIGNAL("setConnections"), conns)
for conn in conns:
if conn["default"]:
return conn
return None
def slotSetDownloadStatus(self, status):
"""
toolbar start/pause slot
"""
if status:
self.connector.unpauseServer()
else:
self.connector.pauseServer()
def slotAddPackage(self, name, links, password=None):
"""
emitted from main window
add package to the collector
"""
pack = self.connector.addPackage(name, links, Destination.Collector)
if password:
data = {"password": password}
self.connector.setPackageData(pack, data)
def slotAddFileToPackage(self, pid, fid): #deprecated?
"""
emitted from collector view after a drop action
"""
#self.connector.addFileToPackage(fid, pid)
pass
def slotAddContainer(self, path):
"""
emitted from main window
add container
"""
filename = basename(path)
type = "".join(filename.split(".")[-1])
fh = open(path, "r")
content = fh.read()
fh.close()
self.connector.uploadContainer(filename, content)
def slotSaveMainWindow(self, state, geo):
"""
save the window geometry and toolbar/dock position to config file
"""
mainWindowNode = self.parser.xml.elementsByTagName("mainWindow").item(0)
if mainWindowNode.isNull():
mainWindowNode = self.parser.xml.createElement("mainWindow")
self.parser.root.appendChild(mainWindowNode)
stateNode = mainWindowNode.toElement().elementsByTagName("state").item(0)
geoNode = mainWindowNode.toElement().elementsByTagName("geometry").item(0)
newStateNode = self.parser.xml.createTextNode(state)
newGeoNode = self.parser.xml.createTextNode(geo)
stateNode.removeChild(stateNode.firstChild())
geoNode.removeChild(geoNode.firstChild())
stateNode.appendChild(newStateNode)
geoNode.appendChild(newGeoNode)
self.parser.saveData()
def restoreMainWindow(self):
"""
load and restore main window geometry and toolbar/dock position from config
"""
mainWindowNode = self.parser.xml.elementsByTagName("mainWindow").item(0)
if mainWindowNode.isNull():
return
nodes = self.parser.parseNode(mainWindowNode, "dict")
state = str(nodes["state"].text())
geo = str(nodes["geometry"].text())
self.mainWindow.restoreWindow(state, geo)
self.mainWindow.captchaDock.hide()
def slotPushPackageToQueue(self, id):
"""
emitted from main window
push the collector package to queue
"""
self.connector.pushToQueue(id)
def slotRestartDownload(self, id, isPack):
"""
emitted from main window
restart download
"""
if isPack:
self.connector.restartPackage(id)
else:
self.connector.restartFile(id)
def slotRefreshStatus(self, id):
"""
emitted from main window
refresh download status
"""
self.connector.recheckPackage(id)
def slotRemoveDownload(self, id, isPack):
"""
emitted from main window
remove download
"""
if isPack:
self.connector.deletePackages([id])
else:
self.connector.deleteFiles([id])
def slotAbortDownload(self, id, isPack):
"""
emitted from main window
remove download
"""
if isPack:
data = self.connector.getFileOrder(id) #less data to transmit
self.connector.stopDownloads(data.values())
else:
self.connector.stopDownloads([id])
def slotStopAllDownloads(self):
"""
emitted from main window
stop all running downloads
"""
self.connector.stopAllDownloads()
def slotClipboardChange(self):
"""
called if clipboard changes
"""
if self.checkClipboard:
text = self.clipboard.text()
pattern = re.compile(r"(http|https|ftp)://[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(([0-9]{1,5})?/.*)?")
matches = pattern.finditer(text)
# thanks to: jmansour //#139
links = [str(match.group(0)) for match in matches]
if len(links) == 0:
return
filenames = [link.rpartition("/")[2] for link in links]
packagename = commonprefix(filenames)
if len(packagename) == 0:
packagename = filenames[0]
self.slotAddPackage(packagename, links)
def slotSetClipboardStatus(self, status):
"""
set clipboard checking
"""
self.checkClipboard = status
def slotChangePackageName(self, pid, name):
"""
package name edit finished
"""
self.connector.setPackageName(pid, str(name))
def slotPullOutPackage(self, pid):
"""
pull package out of the queue
"""
self.connector.pullFromQueue(pid)
def slotSetPriority(self, pid, level):
"""
set package priority
"""
self.connector.setPriority(pid, level)
def checkCaptcha(self):
if self.connector.isCaptchaWaiting() and self.mainWindow.captchaDock.isFree():
t = self.connector.getCaptchaTask(False)
self.mainWindow.show()
self.mainWindow.raise_()
self.mainWindow.activateWindow()
self.mainWindow.captchaDock.emit(SIGNAL("setTask"), t.tid, b64decode(t.data), t.type)
elif not self.mainWindow.captchaDock.isFree():
status = self.connector.getCaptchaTaskStatus(self.mainWindow.captchaDock.currentID)
if not (status == "user" or status == "shared-user"):
self.mainWindow.captchaDock.hide()
self.mainWindow.captchaDock.processing = False
self.mainWindow.captchaDock.currentID = None
def slotCaptchaDone(self, cid, result):
self.connector.setCaptchaResult(cid, str(result))
def pullEvents(self):
events = self.connector.getEvents(self.connector.connectionID)
if not events:
return
for event in events:
if event.event == "account":
self.mainWindow.emit(SIGNAL("reloadAccounts"), False)
elif event.event == "config":
pass
elif event.destination == Destination.Queue:
self.queue.addEvent(event)
try:
if event.event == "update" and event.type == ElementType.File:
info = self.connector.getFileData(event.id)
if info.statusmsg == "finished":
self.emit(SIGNAL("showMessage"), _("Finished downloading of '%s'") % info.name)
elif info.statusmsg == "failed":
self.emit(SIGNAL("showMessage"), _("Failed downloading '%s'!") % info.name)
if event.event == "insert" and event.type == ElementType.File:
info = self.connector.getLinkInfo(event[3])
self.emit(SIGNAL("showMessage"), _("Added '%s' to queue") % info.name)
except:
print "can't send notification"
elif event.destination == Destination.Collector:
self.packageCollector.addEvent(event)
def slotReloadAccounts(self, force=False):
self.mainWindow.tabs["accounts"]["view"].model().reloadData(force)
def slotQuit(self):
self.tray.hide()
self.quitInternal()
self.app.quit()
def quitInternal(self):
if self.core:
self.core.api.kill()
for i in range(10):
if self.core.shuttedDown:
break
sleep(0.5)
def slotConnectionLost(self):
if not self.connectionLost:
self.connectionLost = True
m = QMessageBox(QMessageBox.Critical, _("Connection lost"), _("Lost connection to the core!"), QMessageBox.Ok)
m.exec_()
self.slotQuit()
class Loop():
def __init__(self, parent):
self.parent = parent
self.timer = QTimer()
self.timer.connect(self.timer, SIGNAL("timeout()"), self.update)
self.lastSpaceCheck = 0
def start(self):
self.update()
self.timer.start(1000)
def update(self):
"""
methods to call
"""
self.parent.refreshServerStatus()
if self.lastSpaceCheck + 5 < time():
self.lastSpaceCheck = time()
self.parent.serverStatus["freespace"] = self.parent.connector.freeSpace()
self.parent.refreshLog()
self.parent.checkCaptcha()
self.parent.pullEvents()
def stop(self):
self.timer.stop()
class TrayIcon(QSystemTrayIcon):
def __init__(self):
QSystemTrayIcon.__init__(self, QIcon(join(pypath, "icons", "logo-gui.png")))
self.contextMenu = QMenu()
self.showAction = QAction(_("Show"), self.contextMenu)
self.showAction.setCheckable(True)
self.showAction.setChecked(True)
self.showAction.setDisabled(True)
self.contextMenu.addAction(self.showAction)
self.exitAction = QAction(QIcon(join(pypath, "icons", "close.png")), _("Exit"), self.contextMenu)
self.contextMenu.addAction(self.exitAction)
self.setContextMenu(self.contextMenu)
self.connect(self, SIGNAL("activated(QSystemTrayIcon::ActivationReason)"), self.clicked)
def mainWindowHidden(self):
self.showAction.setChecked(False)
def clicked(self, reason):
if self.showAction.isEnabled():
if reason == QSystemTrayIcon.Trigger:
self.showAction.toggle()
class Notification(QObject):
def __init__(self, tray):
QObject.__init__(self)
self.tray = tray
self.usePynotify = False
try:
self.usePynotify = pynotify.init("icon-summary-body")
except:
print "init error"
def showMessage(self, body):
if self.usePynotify:
n = pynotify.Notification("pyload", body, join(pypath, "icons", "logo.png"))
try:
n.set_hint_string("x-canonical-append", "")
except:
pass
n.show()
else:
self.tray.showMessage("pyload", body)
if __name__ == "__main__":
renameProcess('pyLoadGui')
app = main()
app.loop()