diff --git a/.gitignore b/.gitignore
index 7e07ab4..844b308 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,10 @@
.idea/workspace.xml
.vs/*
*.pyproj
+*.exe
+src/dist/*
+src/build/*
+*.log
+avbin64.dll
+avbin32.dll
+avbin.dll
diff --git a/README.md b/README.md
index db79e05..4de6d70 100644
--- a/README.md
+++ b/README.md
@@ -6,16 +6,16 @@
Visual intel chat analysis, planning and notification application for [EVE Online](http://www.eveonline.com). Gathers status through in-game intelligence channels on all known hostiles and presents all the data on a [dotlan](http://evemaps.dotlan.net/map/Cache#npc24) generated regional map. The map is annotated in real-time as players report intel in monitored chat channels.
-Vintel is written with Python 2.7, using PyQt4 for the application presentation layer, BeautifulSoup4 for SVG parsing, and Pyglet for audio playback.
+Vintel is written with Python 3.6, using PyQt5 for the application presentation layer, BeautifulSoup4 for SVG parsing, and Pyglet+pyttsx3 for audio playback.
### News
-_The current release version of Vintel [can be found here](https://github.com/Xanthos-Eve/vintel/releases). Both Mac and Windows distributions are now available for download with this release._
+_The current release version of Vintel [can be found here](https://github.com/bperian/vintel/releases). Windows distributions are now available for download with this release._
-Keep up on the latest at the [wiki](https://github.com/Xanthos-Eve/vintel/wiki) or visit our [issues](https://github.com/Xanthos-Eve/vintel/issues) page to see what bugs and features are in the queue.
+Keep up on the latest at the [wiki](https://github.com/bperian/vintel/wiki) or visit our [issues](https://github.com/bperian/vintel/issues) page to see what bugs and features are in the queue.
## Screenshot
-
+
## Features
@@ -55,47 +55,29 @@ To use this feature: click on a pilot in the local pilot list and then type the
## Running Vintel from Source
-To run or build from the source you need the following packages installed on your machine. Most, if not all, can be installed from the command line using package management software such as "pip". Mac and Linux both come with pip installed, Windows users may need to install [cygwin](https://www.cygwin.com) to get pip. Of course all the requirements also have downoad links.
-
-The packages required are:
-- Python 2.7.x
-https://www.python.org/downloads/
-Vintel is not compatible with Python 3!
-- PyQt4x
-http://www.riverbankcomputing.com/software/pyqt/download
-Please use the PyQt Binary Package for Py2.7
-Vintel is not compatible with PyQt5!
-- BeautifulSoup 4
-https://pypi.python.org/pypi/beautifulsoup4
-- Pyglet 1.2.4 (for python 2.7)
-https://bitbucket.org/pyglet/pyglet/wiki/Download
-pyglet is used to play the sound – If it is not available the sound option will be disabled.
-- Requests 2
-https://pypi.python.org/pypi/requests
-- Six for python 3 compatibility https://pypi.python.org/pypi/six
+- The packages requirements are defined in `setup.py` file
+- Pyglet may require https://github.com/AVbin/AVbin installed; this can be easily confirmed by checking the `vintel.log` file for errors regarding `avbin` not loaded.
+
## Building the Vintel Standalone Package
- - The standalone is created using pyinstaller. All media files and the .spec-file with the configuration for pyinstaller are included in the source repo. Pyinstaller can be found here: https://github.com/pyinstaller/pyinstaller/wiki.
- - Edit the .spec file to match your src path in the "a = Analysis" section and execute "pyinstaller vintel.spec vintel.py". If everything went correctly you should get a dist folder that contains the standalone executable.
+ - The standalone is created using pyinstaller. All media files and the setup.py file with the configuration for pyinstaller are included in the source repo.
+ pyinstaller can be installed through pip like this `pip install pyinstaller`
+ - execute "pyinstaller vintel.spec vintel.py". If everything went correctly you should get a dist folder that contains the standalone executable + required files.
## FAQ
-**License?**
+**License**
Vintel is licensed under the [GPLv3](http://www.gnu.org/licenses/gpl-3.0.html).
-**Vintel does not play sounds - is there a remedy for this?**
-
-The most likely cause of this is that pyglet is not installed.
-
**A litte bit to big for such a little tool.**
-The .exe ships with the complete environment and needed libs. You could save some space using the the source code instead.
+- The .exe ships with the complete environment and needed libs. You could save some space using the the source code instead.
**What platforms are supported?**
-Vintel runs on Mac (OS X), Windows and Linux. Mac and Windows standalone packages are provided with each release. Linux users are advised to install all the requirements listed above then download and run from source.
+Vintel runs on Mac (OS X), Windows and Linux. Windows standalone packages are provided with each release. Linux and Mac users are advised to install all the requirements listed above then download and run from source.
**What file system permissions does Vintel need?**
@@ -110,31 +92,23 @@ Vintel looks for a new version at startup and loads dynamic infomation (i.e., ju
**Vintel does not find my chatlogs or is not showing changes to chat when it should. What can I do?**
-Vintel looks for your chat logs in ~\EVE\logs\chatlogs and ~\DOCUMENTS\EVE\logs\chatlogs. Logging must be enabled in the EVE client options. You can set this path on your own by giving it to Vintel at startup. For this you have to start it on the command line and call the program with the path to the logs.
+Vintel looks for your chat logs in ~\EVE\logs\chatlogs and ~\DOCUMENTS\EVE\logs\chatlogs , game logs in ~\EVE\logs\gamelogs and ~\DOCUMENTS\EVE\logs\gamelogs
+Logging must be enabled in the EVE client options. You can set this path on your own by giving it to Vintel at startup. For this you have to start it on the command line and call the program with the path to the logs.
Examples:
-`win> vintel-1.0.exe "d:\strange\path\EVE\logs\chatlogs"`
+`win> vintel.exe "d:\strange\path\EVE\logs\chatlogs" "d:\strange\path\EVE\logs\gamelogs"`
– or –
-`linux and mac> python vintel.py "/home/user/myverypecialpath/EVE/logs/chatlogs"`
-
-**Vintel does not start! What can I do?**
+`linux and mac> python vintel.py "/home/user/myverypecialpath/EVE/logs/chatlogs" "/home/user/myverypecialpath/EVE/logs/gamelogs"`
-Please try to delete Vintel's Cache. It is located in the EVE-directory where the chatlogs are in. If your chatlogs are in \Documents\EVE\logs\chatlogs Vintel writes the cachte to \Documents\EVE\vintel
**Vintel takes many seconds to start up; what are some of the causes and what can I do about it?**
Vintel asks the operating system to notifiy when a change has been made to the ChatLogs directory - this will happen when a new log is created or an existing one is updated. In response to this notification, Vintel examines all of the files in the directory to analysze the changes. If you have a lot of chat logs this can make Vintel slow to scan for file changes. Try perodically moving all the chatlogs out of the ChatLogs directory (zip them up and save them somewhere else if you think you may need them some day).
-**Vintel complains about missing dll files on Windows at app launch, is there a workaround for this?**
-
-Yes there is! There is a bit of a mix up going on with the latest pyinstaller and the Microsoft developer dlls. Here is a link to help illuminate the issue https://github.com/pyinstaller/pyinstaller/issues/1974
-
-You can visit Microsoft's web site to download the developer dlls https://www.microsoft.com/en-in/download/details.aspx?id=5555.
-You can also read a more technical treatment of the issue here http://www.tomshardware.com/answers/id-2417960/msvcr100-dll-32bit-64bit.html
**How can I resolve the "empty certificate data" error?**
@@ -142,12 +116,13 @@ Do not use the standalone EXE, install the environment and use the sourcecode di
**Vintel is misbehaving and I dont know why - how can I easily help diagnose problems with Vintel**
-Vintel writes its own set of logs to the \Documents\EVE\vintel\vintel directory. A new log is created as the old one fills up to its maximum size setting. Each entry inside the log file is time-stamped. These logs are emitted in real-time so you can watch the changes to the file as you use the app.
+Vintel writes its own set of logs to the \Documents\EVE\vintel\vintel directory and in the application directory vintel.log . A new log is created as the old one fills up to its maximum size setting. Each entry inside the log file is time-stamped. These logs are emitted in real-time so you can watch the changes to the file as you use the app.
**I love Vintel - how can I help?**
-If you are technically inclined and have a solid grasp of Python, [contact the project maintainer via email](mailto:xanthos.eve@gmail.com) to see how you can best help out. Alternatively you can find something you want to change and create a pull request to have your changes reviewed and potentially added to the codebase. There have been several great contributions made this way!
+If you are technically inclined and have a solid grasp of Python, [contact the project maintainer via email](mailto:bperian@faxtorial.com) to see how you can best help out. Alternatively you can find something you want to change and create a pull request to have your changes reviewed and potentially added to the codebase. There have been several great contributions made this way!
**I'm not a coder, how can I help?**
-Your feedback is needed! Use the program for a while, then come back [here and create issues](https://github.com/Xanthos-Eve/vintel/issues). Record anything you think about Vintel - bugs, frustrations, and ideas to make it better.
+Your feedback is needed! Use the program for a while, then come back [here and create issues](https://github.com/bperian/vintel/issues). Record anything you think about Vintel - bugs, frustrations, and ideas to make it better.
+ISK donations to [Blitz Arkaral](https://zkillboard.com/character/95517727/) are of course welcome.
\ No newline at end of file
diff --git a/src/setup.py b/src/setup.py
new file mode 100644
index 0000000..33a81b4
--- /dev/null
+++ b/src/setup.py
@@ -0,0 +1,40 @@
+from setuptools import find_packages, setup, Executable
+
+
+
+
+import sys
+base = None
+
+if sys.platform == "win32":
+ base = "Win32GUI"
+
+import os.path
+PYTHON_INSTALL_DIR = os.path.dirname(os.path.dirname(os.__file__))
+
+
+
+executables = [Executable("vintel.py", base=base,icon="icon.ico")]
+
+packages = ["appdirs","packaging.version","packaging.specifiers","packaging","pyglet","pyqt5","pyttsx3"]
+
+package_data = {'ui' : ['vi/ui/*']}
+
+options = {
+ 'build_exe': {
+ 'packages':packages,
+ 'optimize':2,
+
+ },
+}
+
+setup(
+ name = "VINTEL",
+ options = options,
+ version = "1.2.4",
+ description = 'Intel chat analyzer',
+ executables = executables,
+ include_package_data=True,
+ package_data = {'' : ['*.ui','*.png','*.svg','*.wav']},
+
+)
\ No newline at end of file
diff --git a/src/tools/addmessage.py b/src/tools/addmessage.py
deleted file mode 100644
index 58f9f10..0000000
--- a/src/tools/addmessage.py
+++ /dev/null
@@ -1,24 +0,0 @@
-import sys
-import six
-if six.PY2:
- from io import open
-
-PATH = "/home/sparrow/EVE/logs/Chatlogs/TheCitadel_20401229_065150.txt"
-
-def main():
- line = ""
- with open(PATH, "r", encoding="utf-16") as f:
- content = f.read()
- lines = content.split("\n")
- line = lines[-2].strip()
- line = line[:line.find(">")+1]
- line = line + " " + sys.argv[1] + "\n"
- with open(PATH, "a") as f:
- f.write(line.encode("utf-16"))
-
-
-
-if __name__ == "__main__":
- main()
-
-
diff --git a/src/vi/PanningWebView.py b/src/vi/PanningWebView.py
index cba19e0..fc4cafa 100644
--- a/src/vi/PanningWebView.py
+++ b/src/vi/PanningWebView.py
@@ -1,12 +1,22 @@
-
-from PyQt4.QtWebKit import QWebView
-from PyQt4.QtGui import *
-from PyQt4 import QtCore
-from PyQt4.QtCore import QPoint
-from PyQt4.QtCore import QString
-from PyQt4.QtCore import QEvent
-
-class PanningWebView(QWebView):
+from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
+from PyQt5.QtCore import QPoint,pyqtSignal, QUrl, QEvent
+
+class WebEnginePage(QWebEnginePage):
+ mapLinkClicked = pyqtSignal(QUrl)
+ def acceptNavigationRequest(self, url, type, isMainFrame):
+ if type == QWebEnginePage.NavigationTypeLinkClicked:
+ self.mapLinkClicked.emit(url)
+ return False
+ return True
+ def javaScriptConsoleMessage(self, level, msg, line, source):
+ # supress Javascript warnings and uncaught ReferenceError exceptions for JS functions
+ pass
+
+class PanningWebView(QWebEngineView):
+
+ def setPage(self, newPage):
+ super(PanningWebView, self).setPage(newPage)
+ self.widget = self.page()
def __init__(self, parent=None):
super(PanningWebView, self).__init__()
@@ -17,7 +27,7 @@ def __init__(self, parent=None):
self.offset = 0
self.handIsClosed = False
self.clickedInScrollBar = False
-
+ self.setPage(WebEnginePage(self))
def mousePressEvent(self, mouseEvent):
pos = mouseEvent.pos()
@@ -27,7 +37,7 @@ def mousePressEvent(self, mouseEvent):
else:
if self.ignored.count(mouseEvent):
self.ignored.remove(mouseEvent)
- return QWebView.mousePressEvent(self, mouseEvent)
+ return QWebEngineView.mousePressEvent(self, mouseEvent)
if not self.pressed and not self.scrolling and mouseEvent.modifiers() == QtCore.Qt.NoModifier:
if mouseEvent.buttons() == QtCore.Qt.LeftButton:
@@ -43,7 +53,7 @@ def mousePressEvent(self, mouseEvent):
self.offset = QPoint(xTuple[0], yTuple[0])
return
- return QWebView.mousePressEvent(self, mouseEvent)
+ return QWebEngineView.mousePressEvent(self, mouseEvent)
def mouseReleaseEvent(self, mouseEvent):
@@ -52,7 +62,7 @@ def mouseReleaseEvent(self, mouseEvent):
else:
if self.ignored.count(mouseEvent):
self.ignored.remove(mouseEvent)
- return QWebView.mousePressEvent(self, mouseEvent)
+ return QWebEngineView.mousePressEvent(self, mouseEvent)
if self.scrolling:
self.pressed = False
@@ -74,7 +84,7 @@ def mouseReleaseEvent(self, mouseEvent):
QApplication.postEvent(self, event1)
QApplication.postEvent(self, event2)
return
- return QWebView.mouseReleaseEvent(self, mouseEvent)
+ return QWebEngineView.mouseReleaseEvent(self, mouseEvent)
def mouseMoveEvent(self, mouseEvent):
@@ -94,7 +104,7 @@ def mouseMoveEvent(self, mouseEvent):
self.pressed = False
self.scrolling = True
return
- return QWebView.mouseMoveEvent(self, mouseEvent)
+ return QWebEngineView.mouseMoveEvent(self, mouseEvent)
def pointInScroller(self, position, orientation):
diff --git a/src/vi/amazon_s3.py b/src/vi/amazon_s3.py
index 560c84d..55daa34 100644
--- a/src/vi/amazon_s3.py
+++ b/src/vi/amazon_s3.py
@@ -21,8 +21,8 @@
import requests
import logging
-from PyQt4 import Qt
-from PyQt4.QtCore import QThread, SIGNAL
+from PyQt5 import Qt
+from PyQt5.QtCore import QThread, pyqtSignal
from vi import version
from vi.cache.cache import Cache
from distutils.version import LooseVersion, StrictVersion
@@ -62,6 +62,9 @@ def getNewestVersion():
class NotifyNewVersionThread(QThread):
+
+ newer_version = pyqtSignal()
+
def __init__(self):
QThread.__init__(self)
self.alerted = False
diff --git a/src/vi/cache/cache.py b/src/vi/cache/cache.py
index 3d0837b..96f0b31 100644
--- a/src/vi/cache/cache.py
+++ b/src/vi/cache/cache.py
@@ -21,6 +21,8 @@
import threading
import time
import six
+import PyQt5
+
if six.PY2:
def to_blob(x):
return buffer(str(x))
diff --git a/src/vi/chatparser/chatparser.py b/src/vi/chatparser/chatparser.py
index 8d7ac21..7c8cdd0 100644
--- a/src/vi/chatparser/chatparser.py
+++ b/src/vi/chatparser/chatparser.py
@@ -21,12 +21,14 @@
import os
import time
import six
+import logging
+
if six.PY2:
from io import open
from bs4 import BeautifulSoup
from vi import states
-from PyQt4.QtGui import QMessageBox
+from PyQt5.QtWidgets import QMessageBox
from .parser_functions import parseStatus
@@ -40,7 +42,7 @@ class ChatParser(object):
""" ChatParser will analyze every new line that was found inside the Chatlogs.
"""
- def __init__(self, path, rooms, systems):
+ def __init__(self, path, rooms, systems, logging):
""" path = the path with the logs
rooms = the rooms to parse"""
self.path = path # the path with the chatlog
@@ -50,6 +52,7 @@ def __init__(self, path, rooms, systems):
self.knownMessages = [] # message we allready analyzed
self.locations = {} # informations about the location of a char
self.ignoredPaths = []
+ self.logging = logging
self._collectInitFileData(path)
def _collectInitFileData(self, path):
@@ -58,50 +61,57 @@ def _collectInitFileData(self, path):
for filename in os.listdir(path):
fullPath = os.path.join(path, filename)
fileTime = os.path.getmtime(fullPath)
+ encoding = 'utf-16-le'
+ if len(filename)<20:
+ encoding = 'utf-8'
if currentTime - fileTime < maxDiff:
- self.addFile(fullPath)
+ self.addFile(fullPath,encoding)
- def addFile(self, path):
+ def addFile(self, path, encod):
lines = None
content = ""
filename = os.path.basename(path)
roomname = filename[:-20]
try:
- with open(path, "r", encoding='utf-16-le') as f:
+ with open(path, "r", encoding=encod, errors='ignore') as f:
content = f.read()
except Exception as e:
self.ignoredPaths.append(path)
- QMessageBox.warning(None, "Read a log file failed!", "File: {0} - problem: {1}".format(path, six.text_type(e)), "OK")
+ QMessageBox.warning(None, "Read a log file failed!", "File: {0} - problem: {1}".format(path, six.text_type(e)), QMessageBox.Ok)
return None
-
+ charname = None
+ try:
+ charname = self.fileData[path]["charname"]
+ except:
+ pass
lines = content.split("\n")
- if path not in self.fileData or (roomname in LOCAL_NAMES and "charname" not in self.fileData.get(path, [])):
+ if path not in self.fileData or "charname" not in self.fileData.get(path, []):
self.fileData[path] = {}
- if roomname in LOCAL_NAMES:
- charname = None
- sessionStart = None
- # for local-chats we need more infos
- for line in lines:
- if "Listener:" in line:
- charname = line[line.find(":") + 1:].strip()
- elif "Session started:" in line:
- sessionStr = line[line.find(":") + 1:].strip()
- sessionStart = datetime.datetime.strptime(sessionStr, "%Y.%m.%d %H:%M:%S")
- if charname and sessionStart:
- self.fileData[path]["charname"] = charname
- self.fileData[path]["sessionstart"] = sessionStart
- break
+ sessionStart = None
+ # for local-chats we need more infos
+ for line in lines:
+ if "listener:" in line.lower():
+ charname = line[line.find(":") + 1:].strip()
+ elif "session started:" in line.lower():
+ sessionStr = line[line.find(":") + 1:].strip()
+ sessionStart = datetime.datetime.strptime(sessionStr, "%Y.%m.%d %H:%M:%S")
+ if charname and sessionStart:
+ self.fileData[path]["charname"] = charname
+ self.fileData[path]["sessionstart"] = sessionStart
+ break
self.fileData[path]["lines"] = len(lines)
return lines
def _lineToMessage(self, line, roomname):
# finding the timestamp
+ line = line.decode('utf-8')
timeStart = line.find("[") + 1
timeEnds = line.find("]")
timeStr = line[timeStart:timeEnds].strip()
try:
timestamp = datetime.datetime.strptime(timeStr, "%Y.%m.%d %H:%M:%S")
- except ValueError:
+ except ValueError as e:
+ logging.critical(e)
return None
# finding the username of the poster
userEnds = line.find(">")
@@ -168,15 +178,22 @@ def _parseLocal(self, path, line):
self.locations[charname] = {"system": "?", "timestamp": datetime.datetime(1970, 1, 1, 0, 0, 0, 0)}
# Finding the timestamp
- timeStart = line.find("[") + 1
- timeEnds = line.find("]")
+ timeStart = line.decode().find("[") + 1
+ timeEnds = line.decode().find("]")
timeStr = line[timeStart:timeEnds].strip()
- timestamp = datetime.datetime.strptime(timeStr, "%Y.%m.%d %H:%M:%S")
-
+ timestamp = None
+ try:
+ timestamp = datetime.datetime.strptime(timeStr.decode(), "%Y.%m.%d %H:%M:%S")
+ except Exception as e:
+ logging.critical(e)
+ return
# Finding the username of the poster
- userEnds = line.find(">")
+ userEnds = line.decode().find(">")
+ if userEnds == -1:
+ userEnds = len(line)-1
username = line[timeEnds + 1:userEnds].strip()
+ status = states.IGNORE
# Finding the pure message
text = line[userEnds + 1:].strip() # text will the text to work an
if username in ("EVE-System", "EVE System"):
@@ -191,6 +208,18 @@ def _parseLocal(self, path, line):
self.locations[charname]["system"] = system
self.locations[charname]["timestamp"] = timestamp
message = Message("", "", timestamp, charname, [system, ], "", "", status)
+
+ # Solving new game logs user location where case
+ if message == [] and status != states.LOCATION:
+ text = line[timeEnds:].decode().strip().replace("*", "").lower()
+ if "(none)" in text and "jumping" in text and "to" in text:
+ system = text.split("to")[1].strip().upper()
+ status = states.LOCATION
+ if self.locations[charname]["timestamp"] is None or timestamp > self.locations[charname]["timestamp"]:
+ self.locations[charname]["system"] = system
+ self.locations[charname]["timestamp"] = timestamp
+ message = Message("", "", timestamp, charname, [system, ], "", "", status)
+
return message
def fileModified(self, path):
@@ -200,26 +229,52 @@ def fileModified(self, path):
# Checking if we must do anything with the changed file.
# We only need those which name is in the rooms-list
# EvE names the file like room_20140913_200737.txt, so we don't need
- # the last 20 chars
+ # the last 20 chars. if file name is under 20 char it's most likely a game log
filename = os.path.basename(path)
- roomname = filename[:-20]
- if path not in self.fileData:
- # seems eve created a new file. New Files have 12 lines header
- self.fileData[path] = {"lines": 13}
- oldLength = self.fileData[path]["lines"]
- lines = self.addFile(path)
- if path in self.ignoredPaths:
- return []
- for line in lines[oldLength - 1:]:
- line = line.strip()
- if len(line) > 2:
- message = None
- if roomname in LOCAL_NAMES:
+
+ if len(filename) > 20:
+ roomname = str(filename[:-20])
+ if roomname.find('[') > -1:
+ roomname = roomname[0:roomname.find('[')-1]
+
+ if path not in self.fileData:
+ # seems eve created a new file. New Files have 12 lines header
+ self.fileData[path] = {"lines": 13}
+ oldLength = self.fileData[path]["lines"]
+ lines = self.addFile(path,'utf-16-le')
+
+ if path in self.ignoredPaths:
+ return []
+
+ for line in lines[oldLength - 1:]:
+ line = line.strip()
+ line = line.encode('utf-8', 'ignore')
+ if len(line) > 2:
+ message = None
+ if roomname == "Local":
+ message = self._parseLocal(path, line)
+ else:
+ message = self._lineToMessage(line, roomname)
+ if message:
+ messages.append(message)
+ else:
+ # Game log parsing
+ if path not in self.fileData:
+ self.fileData[path] = { "lines": 6} # Game logs have 6 lines header so we'll skip that
+ oldLength = self.fileData[path]["lines"]
+ lines = self.addFile(path,'utf-8')
+
+ if path in self.ignoredPaths:
+ return []
+ for line in lines[oldLength - 1:]:
+ line = line.strip()
+ line = line.encode('ascii','ignore')
+ if len(line) > 2:
+ message = None
+
message = self._parseLocal(path, line)
- else:
- message = self._lineToMessage(line, roomname)
- if message:
- messages.append(message)
+ if message:
+ messages.append(message)
return messages
diff --git a/src/vi/dotlan.py b/src/vi/dotlan.py
index 182b7bd..15c1d85 100644
--- a/src/vi/dotlan.py
+++ b/src/vi/dotlan.py
@@ -88,7 +88,7 @@ def __init__(self, region, svgFile=None):
"temporary problem (like dotlan is not reachable), or " \
"everythig went to hell. Sorry. This makes no sense " \
"without the map.\n\nRemember the site for possible " \
- "updates: https://github.com/Xanthos-Eve/vintel".format(type(e), six.text_type(e))
+ "updates: https://github.com/bperian/vintel".format(type(e), six.text_type(e))
raise DotlanException(t)
# Create soup from the svg
self.soup = BeautifulSoup(svg, 'html.parser')
@@ -119,6 +119,15 @@ def _extractSystemsFromSoup(self, soup):
for element in symbol.select(".sys"):
name = element.select("text")[0].text.strip().upper()
mapCoordinates = {}
+ sov = {}
+ sovData = element.select("text")[1].text.strip().upper()
+ try:
+ sov["name"] = sovData[0:(sovData.index('(')-1)]
+ sov["sov_level"] = sovData[sovData.index('(')+1:len(sovData)-1]
+ except:
+ # If system SOV is NPC index isn't set
+ sov["name"] = sovData
+
for keyname in ("x", "y", "width", "height"):
mapCoordinates[keyname] = float(uses[symbolId][keyname])
mapCoordinates["center_x"] = (mapCoordinates["x"] + (mapCoordinates["width"] / 2))
@@ -127,8 +136,9 @@ def _extractSystemsFromSoup(self, soup):
transform = uses[symbolId]["transform"]
except KeyError:
transform = "translate(0,0)"
- systems[name] = System(name, element, self.soup, mapCoordinates, transform, systemId)
+ systems[name] = System(name, element, self.soup, mapCoordinates, transform, systemId, sov)
return systems
+
def _prepareSvg(self, soup, systems):
svg = soup.select("svg")[0]
@@ -267,19 +277,8 @@ def changeJumpbridgesVisibility(self):
for line in self.soup.select(".jumpbridge"):
line["visibility"] = value
self._jumpMapsVisible = newStatus
- # self.debugWriteSoup()
return newStatus
- def debugWriteSoup(self):
- svgData = self.soup.prettify("utf-8")
- try:
- with open("/Users/mark/Desktop/output.svg", "wb") as svgFile:
- svgFile.write(svgData)
- svgFile.close()
- except Exception as e:
- logging.error(e)
-
-
class System(object):
"""
A System on the Map
@@ -291,7 +290,7 @@ class System(object):
UNKNOWN_COLOR = "#FFFFFF"
CLEAR_COLOR = "#59FF6C"
- def __init__(self, name, svgElement, mapSoup, mapCoordinates, transform, systemId):
+ def __init__(self, name, svgElement, mapSoup, mapCoordinates, transform, systemId, sovData):
self.status = states.UNKNOWN
self.name = name
self.svgElement = svgElement
@@ -301,6 +300,8 @@ def __init__(self, name, svgElement, mapSoup, mapCoordinates, transform, systemI
self.secondLine = svgElement.select("text")[1]
self.lastAlarmTime = 0
self.messages = []
+ self.sovData = sovData
+ self.sovShown = False
self.setStatus(states.UNKNOWN)
self.__locatedCharacters = []
self.backgroundColor = "#FFFFFF"
@@ -440,8 +441,15 @@ def setStatus(self, newStatus):
self.secondLine["style"] = "fill: #000000;"
elif newStatus == states.UNKNOWN:
self.setBackgroundColor(self.UNKNOWN_COLOR)
- # second line in the rects is reserved for the clock
- self.secondLine.string = "?"
+ # second line in the rects is reserved for the clock and SOV
+ if self.sovShown is False:
+ self.SovShown = True
+ self.secondLine.string = self.sovData["name"]
+ try:
+ self.secondLine.string += " (" + self.sovData["sov_level"] + ")"
+ except KeyError:
+ pass # System is NPC owned
+
self.secondLine["style"] = "fill: #000000;"
if newStatus not in (states.NOT_CHANGE, states.REQUEST): # unknown not affect system status
self.status = newStatus
@@ -473,15 +481,26 @@ def update(self):
seconds = int(diff - minutes * 60)
string = "{m:02d}:{s:02d}".format(m=minutes, s=seconds)
if self.status == states.CLEAR:
+
secondsUntilWhite = 10 * 60
calcValue = int(diff / (secondsUntilWhite / 255.0))
if calcValue > 255:
calcValue = 255
self.secondLine["style"] = "fill: #008100;"
- string = "clr: {m:02d}:{s:02d}".format(m=minutes, s=seconds)
- self.setBackgroundColor("rgb({r},{g},{b})".format(r=calcValue, g=255, b=calcValue))
- self.secondLine.string = string
-
+ if minutes > 30 and self.sovShown is False:
+ self.SovShown = True
+ self.secondLine.string = self.sovData["name"]
+ try:
+ self.secondLine.string += " (" + self.sovData["sov_level"] + ")"
+ except KeyError:
+ pass # System is NPC owned
+ else:
+ self.sovShown = False
+ string = "clr: {m:02d}:{s:02d}".format(m=minutes, s=seconds)
+ self.setBackgroundColor("rgb({r},{g},{b})".format(r=calcValue, g=255, b=calcValue))
+ self.secondLine.string = string
+
+
def convertRegionName(name):
"""
@@ -505,11 +524,3 @@ def convertRegionName(name):
nextUpper = False
converted.append(char)
return u"".join(converted)
-
-
-# this is for testing:
-if __name__ == "__main__":
- map = Map("Providence", "Providence.svg")
- s = map.systems["I7S-1S"]
- s.setStatus(states.ALARM)
- logging.error(map.svg)
diff --git a/src/vi/filewatcher.py b/src/vi/filewatcher.py
index 75032a1..6c6eabc 100644
--- a/src/vi/filewatcher.py
+++ b/src/vi/filewatcher.py
@@ -22,8 +22,8 @@
import time
import logging
-from PyQt4 import QtCore
-from PyQt4.QtCore import SIGNAL
+from PyQt5 import QtCore
+from PyQt5.QtCore import pyqtSignal, QObject, QModelIndex
"""
There is a problem with the QFIleWatcher on Windows and the log
@@ -40,6 +40,9 @@
DEFAULT_MAX_AGE = 60 * 60 * 24
class FileWatcher(QtCore.QThread):
+
+ data_changed = pyqtSignal(str)
+
def __init__(self, path, maxAge=DEFAULT_MAX_AGE):
QtCore.QThread.__init__(self)
self.path = path
@@ -52,7 +55,7 @@ def __init__(self, path, maxAge=DEFAULT_MAX_AGE):
self.paused = True
self.active = True
-
+
def directoryChanged(self):
self.updateWatchedFiles()
@@ -69,7 +72,7 @@ def run(self):
if not stat.S_ISREG(pathStat.st_mode):
continue
if modified < pathStat.st_size:
- self.emit(SIGNAL("file_change"), path)
+ self.data_changed.emit(path)
self.files[path] = pathStat.st_size
@@ -92,4 +95,5 @@ def updateWatchedFiles(self):
if self.maxAge and ((now - pathStat.st_mtime) > self.maxAge):
continue
filesInDir[fullPath] = self.files.get(fullPath, 0)
+
self.files = filesInDir
diff --git a/src/vi/resources.py b/src/vi/resources.py
deleted file mode 100644
index fa94cba..0000000
--- a/src/vi/resources.py
+++ /dev/null
@@ -1,33 +0,0 @@
-###########################################################################
-# Vintel - Visual Intel Chat Analyzer #
-# Copyright (C) 2014-15 Sebastian Meyer (sparrow.242.de+eve@gmail.com ) #
-# #
-# 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 . #
-###########################################################################
-
-import os
-import sys
-
-
-def resourcePath(relativePath):
- """ Get absolute path to resource, works for dev and for PyInstaller
- """
- if getattr(sys, 'frozen', False):
- # PyInstaller creates a temp folder and stores path in _MEIPASS
- basePath = sys._MEIPASS
- else:
- basePath = os.path.abspath(".")
- returnpath = os.path.join(basePath, relativePath)
- return returnpath
diff --git a/src/vi/soundmanager.py b/src/vi/soundmanager.py
index ff8bbfd..2011406 100644
--- a/src/vi/soundmanager.py
+++ b/src/vi/soundmanager.py
@@ -8,7 +8,7 @@
# (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 #
+# but WITHOUT ANY WARRANTYf without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
@@ -24,10 +24,13 @@
import requests
import time
import six
+import logging
+import pyttsx3
+from win32com.server.util import wrap
from collections import namedtuple
-from PyQt4.QtCore import QThread
-from .resources import resourcePath
+from PyQt5.QtCore import QThread
+from pkg_resources import resource_filename
from six.moves import queue
import logging
@@ -35,6 +38,7 @@
global gPygletAvailable
+
try:
import pyglet
from pyglet import media
@@ -68,9 +72,9 @@ def platformSupportsAudio(self):
return self.platformSupportsSpeech() or gPygletAvailable
def platformSupportsSpeech(self):
- if self._soundThread.isDarwin:
+ if self._soundThread.isDarwin or self._soundThread.usePyTTS:
return True
- return False
+ return True
def setUseSpokenNotifications(self, newValue):
if newValue is not None:
@@ -82,14 +86,18 @@ def setSoundVolume(self, newValue):
self.soundVolume = max(0, min(100, newValue))
self._soundThread.setVolume(self.soundVolume)
- def playSound(self, name="alarm", message="", abbreviatedMessage=""):
+ def playSound(self, name="alarm", message="Incoming enemies", abbreviatedMessage="red alert"):
""" Schedules the work, which is picked up by SoundThread.run()
"""
+ if name is False:
+ name = "alarm"
+ message = "this is a test"
+ abbreviatedMessage = "test"
if self.soundAvailable and self.soundActive:
if self.useSpokenNotifications:
audioFile = None
else:
- audioFile = resourcePath("vi/ui/res/{0}".format(self.SOUNDS[name]))
+ audioFile = resource_filename(__name__,"ui/res/{0}".format(self.SOUNDS[name]))
self._soundThread.queue.put((audioFile, message, abbreviatedMessage))
def quit(self):
@@ -100,12 +108,9 @@ def quit(self):
# Inner class handle audio playback without blocking the UI
#
- class SoundThread(QThread):
+ class SoundThread(QThread):
+ usePyTTS = True
queue = None
- useGoogleTTS = False
- useVoiceRss = False
- VOICE_RSS_API_KEY = '896a7f61ec5e478cba856a78babab79c'
- GOOGLE_TTS_API_KEY = ''
isDarwin = sys.platform.startswith("darwin")
volume = 25
@@ -116,8 +121,11 @@ def __init__(self):
if gPygletAvailable:
self.player = media.Player()
else:
- self.player = None
+ self.player = None
+ if self.usePyTTS:
+ self.speech_engine = pyttsx3.init()
self.active = True
+
def setVolume(self, volume):
@@ -148,10 +156,11 @@ def quit(self):
def speak(self, message):
- if self.useGoogleTTS:
- self.audioExtractToMp3(inputText=message) # experimental
- elif self.useVoiceRss:
- self.playTTS(message) # experimental
+ if self.usePyTTS:
+ self.speech_engine.say(message) # experimental
+ self.speech_engine.setProperty('volume',self.volume/100.0)
+ self.speech_engine.setProperty('rate',130)
+ self.speech_engine.runAndWait()
elif self.isDarwin:
self.darwinSpeak(message)
else:
@@ -184,54 +193,6 @@ def darwinSpeak(self, message):
except Exception as e:
logging.error("SoundThread.darwinSpeak exception: %s", e)
- #
- # Experimental text-to-speech stuff below
- #
-
- # VoiceRss
-
- def playTTS(self, inputText=''):
- try:
- mp3url = 'http://api.voicerss.org/?c=WAV&key={self.VOICE_RSS_API_KEY}&src={inputText}&hl=en-us'.format(
- **locals())
- self.playAudioFile(requests.get(mp3url, stream=True).raw)
- time.sleep(.5)
- except requests.exceptions.RequestException as e:
- logging.error('playTTS error: %s', str(e))
-
- # google_tts
-
- def audioExtractToMp3(self, inputText='', args=None):
- # This accepts :
- # a dict,
- # an audio_args named tuple
- # or arg parse object
- audioArgs = namedtuple('audio_args', ['language', 'output'])
- if args is None:
- args = audioArgs(language='en', output=open('output.mp3', 'w'))
- if type(args) is dict:
- args = audioArgs(language=args.get('language', 'en'), output=open(args.get('output', 'output.mp3'), 'w'))
- # Process inputText into chunks
- # Google TTS only accepts up to (and including) 100 characters long texts.
- # Split the text in segments of maximum 100 characters long.
- combinedText = self.splitText(inputText)
-
- # Download chunks and write them to the output file
- for idx, val in enumerate(combinedText):
- mp3url = "http://translate.google.com/translate_tts?tl=%s&q=%s&total=%s&idx=%s&ie=UTF-8&client=t&key=%s" % (
- args.language, requests.utils.quote(val), len(combinedText), idx, self.GOOGLE_TTS_API_KEY)
- headers = {"Host": "translate.google.com", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1)"}
- sys.stdout.write('.')
- sys.stdout.flush()
- if len(val) > 0:
- try:
- args.timeout.write(requests.get(mp3url, headers=headers).content)
- time.sleep(.5)
- except requests.exceptions.RequestException as e:
- logging.error('audioExtractToMp3 error: %s', e)
- args.output.close()
- return args.output.name
-
def splitText(self, inputText, maxLength=100):
"""
Try to split between sentences to avoid interruptions mid-sentence.
diff --git a/src/vi/threads.py b/src/vi/threads.py
index f42c9e9..5b9247e 100755
--- a/src/vi/threads.py
+++ b/src/vi/threads.py
@@ -22,16 +22,20 @@
import six
from six.moves import queue
-from PyQt4.QtCore import QThread, SIGNAL, QTimer
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QThread, pyqtSignal, QTimer
from vi import evegate
from vi import koschecker
from vi.cache.cache import Cache
-from vi.resources import resourcePath
+
+from vi.ui import ChatEntryWidget
+from pkg_resources import resource_filename
STATISTICS_UPDATE_INTERVAL_MSECS = 1 * 60 * 1000
class AvatarFindThread(QThread):
+ avatar_update = pyqtSignal(QtWidgets.QWidget, bytes)
def __init__(self):
QThread.__init__(self)
self.queue = queue.Queue()
@@ -64,7 +68,7 @@ def run(self):
logging.debug("AvatarFindThread getting avatar for %s" % charname)
avatar = None
if charname == "VINTEL":
- with open(resourcePath("vi/ui/res/logo_small.png"), "rb") as f:
+ with open(resource_filename(__name__,"ui/res/logo_small.png"), "rb") as f:
avatar = f.read()
if not avatar:
avatar = cache.getAvatar(charname)
@@ -80,7 +84,7 @@ def run(self):
cache.putAvatar(charname, avatar)
if avatar:
logging.debug("AvatarFindThread emit avatar_update for %s" % charname)
- self.emit(SIGNAL("avatar_update"), chatEntry, avatar)
+ self.avatar_update.emit( chatEntry, avatar)
except Exception as e:
logging.error("Error in AvatarFindThread : %s", e)
@@ -93,6 +97,8 @@ def quit(self):
class KOSCheckerThread(QThread):
+ kos_result = pyqtSignal()
+
def __init__(self):
QThread.__init__(self)
self.queue = queue.Queue()
@@ -151,6 +157,7 @@ def quit(self):
class MapStatisticsThread(QThread):
+ statisticDataUpdate = pyqtSignal(dict)
def __init__(self):
QThread.__init__(self)
self.queue = queue.Queue(maxsize=1)
@@ -166,7 +173,7 @@ def requestStatistics(self):
def run(self):
self.refreshTimer = QTimer()
- self.connect(self.refreshTimer, SIGNAL("timeout()"), self.requestStatistics)
+ self.refreshTimer.timeout.connect(self.requestStatistics)
while True:
# Block waiting for requestStatistics() to enqueue a token
self.queue.get()
@@ -183,7 +190,7 @@ def run(self):
requestData = {"result": "error", "text": six.text_type(e)}
self.lastStatisticsUpdate = time.time()
self.refreshTimer.start(self.pollRate)
- self.emit(SIGNAL("statistic_data_update"), requestData)
+ self.statisticDataUpdate.emit(requestData)
logging.debug("MapStatisticsThread emitted statistic_data_update")
diff --git a/src/vi/ui/ChatEntryWidget.py b/src/vi/ui/ChatEntryWidget.py
new file mode 100644
index 0000000..f437847
--- /dev/null
+++ b/src/vi/ui/ChatEntryWidget.py
@@ -0,0 +1,62 @@
+import datetime
+import sys
+import six
+from PyQt5 import QtWidgets, QtGui, uic
+from PyQt5.QtCore import pyqtSignal
+from PyQt5.QtGui import QImage,QPixmap
+from pkg_resources import resource_stream, resource_filename
+
+class ChatEntryWidget(QtWidgets.QWidget):
+ mark_system = pyqtSignal(str)
+ TEXT_SIZE = 11
+ SHOW_AVATAR = True
+ questionMarkPixmap = None
+
+ def __init__(self, message):
+ QtWidgets.QWidget.__init__(self)
+ if not self.questionMarkPixmap:
+ self.questionMarkPixmap = QtGui.QPixmap(resource_filename(__name__,"res/qmark.png")).scaledToHeight(32)
+ uic.loadUi(resource_stream(__name__,"ChatEntry.ui"), self)
+ self.avatarLabel.setPixmap(self.questionMarkPixmap)
+ self.message = message
+ self.updateText()
+ self.textLabel.linkActivated.connect(self.linkClicked)
+ if sys.platform.startswith("win32") or sys.platform.startswith("cygwin"):
+ ChatEntryWidget.TEXT_SIZE = 8
+ self.changeFontSize(self.TEXT_SIZE)
+ if not ChatEntryWidget.SHOW_AVATAR:
+ self.avatarLabel.setVisible(False)
+
+
+ def linkClicked(self, link):
+ link = six.text_type(link)
+ function, parameter = link.split("/", 1)
+ if function == "mark_system":
+ self.mark_system.emit(parameter)
+ elif function == "link":
+ webbrowser.open(parameter)
+
+
+ def updateText(self):
+ time = datetime.datetime.strftime(self.message.timestamp, "%H:%M:%S")
+ text = u"{time} - {user} - {room}
{text}".format(user=self.message.user,
+ room=self.message.room,
+ time=time,
+ text=self.message.message)
+ self.textLabel.setText(text)
+
+
+ def updateAvatar(self, avatarData):
+ image = QImage.fromData(avatarData)
+ pixmap = QPixmap.fromImage(image)
+ if pixmap.isNull():
+ return False
+ scaledAvatar = pixmap.scaled(32, 32)
+ self.avatarLabel.setPixmap(scaledAvatar)
+ return True
+
+
+ def changeFontSize(self, newSize):
+ font = self.textLabel.font()
+ font.setPointSize(newSize)
+ self.textLabel.setFont(font)
diff --git a/src/vi/ui/Info.ui b/src/vi/ui/Info.ui
index 6864006..6b69751 100644
--- a/src/vi/ui/Info.ui
+++ b/src/vi/ui/Info.ui
@@ -750,7 +750,7 @@ Public License instead of this License. But first, please read
- <html><head/><body><p align="center"><span style=" font-weight:600;">Vintel</span> - Visual Intel Chat Analyzer<br/><br/><br/>Dev Team: <a href="mailto:xanthos.eve@gmail.com"><span style=" text-decoration: underline; color:#0000ff;">Xanthos</span></a>, <a href="mailto:lightlazer@gmail.com"><span style=" text-decoration: underline; color:#0000ff;">Parrion Hukondo</span></a>, <a href="mailto:cberg@3wsolutions.se"><span style=" text-decoration: underline; color:#0000ff;">chriz</span></a><br/><br/>This software is based on original work by Sebastian Meyer <br/>and is open source licensed under GPL v3.<br/>Find current sources and documentation at<br/><a href="https://github.com/XanthosX/vintel"><span style=" text-decoration: underline; color:#0000ff;">https://github.com/xanthos-eve/vintel</span></a></p><p align="center">Donations of ISK are appreciated.</p><p align="center">NRDS!</p></body></html>
+ <html><head/><body><p align="center"><span style=" font-weight:600;">Vintel</span> - Visual Intel Chat Analyzer<br/><br/><br/> Dev Team: <ul style="text-align:left;" > <li> <a href="https://github.com/bperian">Blitz Arkaral<a> </li> <li> <a href="mailto:xanthos.eve@gmail.com"><span style=" text-decoration: underline; color:#0000ff;">Xanthos</span></a> </li> <li> <a href="mailto:lightlazer@gmail.com"><span style=" text-decoration: underline; color:#0000ff;">Parrion Hukondo</span></a> </li> <li> <a href="mailto:cberg@3wsolutions.se"><span style=" text-decoration: underline; color:#0000ff;">chriz</span></a> </li> </ul> <br/><br/>This software is based on original work by Sebastian Meyer <br/>and is open source licensed under GPL v3.<br/>Find current sources and documentation at<br/><a href="https://github.com/bperian/vintel"><span style=" text-decoration: underline; color:#0000ff;">https://github.com/xanthos-eve/vintel</span></a></p><p align="center">Donations of ISK are appreciated.</p><p align="center"></p></body></html>
Qt::AlignHCenter|Qt::AlignTop
diff --git a/src/vi/ui/MainWindow.ui b/src/vi/ui/MainWindow.ui
index d3a9ccd..944c7a4 100644
--- a/src/vi/ui/MainWindow.ui
+++ b/src/vi/ui/MainWindow.ui
@@ -384,7 +384,7 @@
-
+