Compare commits
203 Commits
1.0.2a-bet
...
1.1.7-beta
Author | SHA1 | Date | |
---|---|---|---|
8e9fccdbbc | |||
39990fc2b4 | |||
e8c315d834 | |||
f8a06a8746 | |||
9415087da7 | |||
9aee5c32eb | |||
fcdb4a3889 | |||
534a326258 | |||
0390ff5919 | |||
b800ae1751 | |||
a2c17982d3 | |||
0347befae6 | |||
af54b79790 | |||
dd04ae98a0 | |||
31b76fba92 | |||
9f4a4b0eb0 | |||
575a23c6bf | |||
5d84f09359 | |||
3072583482 | |||
8d867cf78a | |||
36c79b5a2a | |||
dfdaf731b4 | |||
67bff8586c | |||
9e4cbea6e4 | |||
d150b2ce54 | |||
a20949cc4d | |||
e3fceb20a2 | |||
f4e00d9ef3 | |||
1980bd5988 | |||
db54affc74 | |||
0edb9444ef | |||
b22c25f53f | |||
76e6666a79 | |||
a804a10e0e | |||
fe413b12c1 | |||
e38dc2f063 | |||
5e5418090b | |||
56c1f8582a | |||
00f8c0a280 | |||
1d915eb155 | |||
b7b8060ef2 | |||
2d190b076a | |||
cd92b1afea | |||
4d21a001d6 | |||
4af59d2315 | |||
c9c98b6c11 | |||
1ff43db2ce | |||
822f6b4729 | |||
44a8dc6815 | |||
a35576895c | |||
631662b30c | |||
cbe3f5a2dc | |||
73f8bd426b | |||
0642604480 | |||
1d95f5076e | |||
53b0c2e8f9 | |||
f59f5fe981 | |||
67545d8a13 | |||
ab3e3b40c4 | |||
188024c2db | |||
324b56a623 | |||
782d424392 | |||
cf63bfda9d | |||
903d4c647c | |||
407b83fe90 | |||
27edc80d2b | |||
01f48f8b91 | |||
527e690170 | |||
d100572aa4 | |||
42640c4ad5 | |||
a61972e503 | |||
464e147223 | |||
8759784561 | |||
ee5b4a689e | |||
71ccf1eea8 | |||
a9ee7c463b | |||
6f683a71c7 | |||
24b192b22c | |||
b6b1a4737f | |||
00202cc865 | |||
235524b06d | |||
8a7f822970 | |||
ff3f048bb4 | |||
abda202f32 | |||
2d4ac84de0 | |||
86732e7827 | |||
693b5b1978 | |||
e3d3ecfd31 | |||
ce6b81ab73 | |||
501365b5a3 | |||
c6741d4392 | |||
42feae53dd | |||
c65695b8dc | |||
4da71e262b | |||
c519fd33d5 | |||
07ef0211b9 | |||
c45b56a5b6 | |||
6f27fc7669 | |||
4530ac017c | |||
400fe6efa3 | |||
ac7a12d18d | |||
c2ff11fab7 | |||
34019ff338 | |||
176bc43888 | |||
2e290c4c74 | |||
74a374d46b | |||
58f5f10c78 | |||
7d8ed954a9 | |||
078b3cef3c | |||
22ef0250ca | |||
cc53162dcc | |||
fa309cfcef | |||
4d57b0cf79 | |||
6ea5d28609 | |||
9d56a2ce9a | |||
811759478a | |||
28e2d93314 | |||
93b3117699 | |||
10e6a1019e | |||
2024555780 | |||
e15c3fa3e6 | |||
8aa6403f51 | |||
fb5fca1dc4 | |||
75d5b1a695 | |||
e56d9bddbf | |||
7d9aa70dc0 | |||
6d72ed2a69 | |||
9b584f78a0 | |||
dfe0e74f9c | |||
a11c08a2ee | |||
9159204883 | |||
605e27ce99 | |||
2dc08b36ea | |||
60dae4f1fb | |||
85728d33bb | |||
2ade08aa89 | |||
50909962d3 | |||
cc02023730 | |||
5bdc40b9f5 | |||
4f3e63db07 | |||
b8893b853f | |||
6da6f38673 | |||
369dcbb5a1 | |||
ec010f29e8 | |||
22867bc9e6 | |||
dde1913e07 | |||
5b5842a5f8 | |||
fbf086886f | |||
c1ff6c4b26 | |||
99b110d052 | |||
3df498eed4 | |||
b5ab2a6ac9 | |||
5c91960f04 | |||
3b52fd3213 | |||
9366457b88 | |||
1cb7ef66db | |||
ee6a05deae | |||
c978883584 | |||
9b5508ecba | |||
8e1c6fae7c | |||
59e662f5a7 | |||
6486d97ee3 | |||
8c088440c5 | |||
320ee1c5d1 | |||
e123720354 | |||
d39d4e79ad | |||
8d7eeece30 | |||
3b64e1a3ec | |||
81ae9bd635 | |||
27846772e9 | |||
baf697b919 | |||
59ede8d446 | |||
8b748a3343 | |||
75471aaddc | |||
7225f261f1 | |||
c466264d43 | |||
14e801b717 | |||
af4b467814 | |||
1b3feaa167 | |||
2526fa0ca8 | |||
a878d36dcf | |||
90de6433b6 | |||
d9abc364f1 | |||
e542b6df1f | |||
a7a6b085f1 | |||
0078f76e8c | |||
1a01cb60d9 | |||
0f81ce4c24 | |||
c4ef4137d0 | |||
cdc6d71356 | |||
2357a6378e | |||
9503d0fef4 | |||
c46dda4540 | |||
894c23f64f | |||
9360fa954c | |||
74408e56fd | |||
dd8e54fa6b | |||
b378840878 | |||
c0a6406dc9 | |||
df3544e734 | |||
d40de5b67e | |||
25b63dfc65 | |||
70f50c8595 |
4
MANIFEST.in
Normal file
@ -0,0 +1,4 @@
|
||||
include README.txt
|
||||
include release_notes.txt
|
||||
include requirements.txt
|
||||
recursive-include scripts *.py *.txt
|
61
Makefile
@ -1,25 +1,60 @@
|
||||
TAGGER_BASE := $(HOME)/Dropbox/tagger/comictagger
|
||||
VERSION_STR := $(shell grep version $(TAGGER_BASE)/ctversion.py| cut -d= -f2 | sed 's/\"//g')
|
||||
|
||||
TAGGER_SRC := $(TAGGER_BASE)/comictaggerlib
|
||||
VERSION_STR := $(shell grep version $(TAGGER_SRC)/ctversion.py| cut -d= -f2 | sed 's/\"//g')
|
||||
PASSWORD := $(shell cat $(TAGGER_BASE)/project_password.txt)
|
||||
UPLOAD_TOOL := $(TAGGER_BASE)/google/googlecode_upload.py
|
||||
all: clean
|
||||
|
||||
clean:
|
||||
rm -f *~ *.pyc *.pyo
|
||||
rm -f logdict*.log
|
||||
rm -rf *~ *.pyc *.pyo
|
||||
rm -rf scripts/*.pyc
|
||||
cd comictaggerlib; rm -f *~ *.pyc *.pyo
|
||||
rm -rf dist MANIFEST
|
||||
rm -rf *.deb
|
||||
rm -rf logdict*.log
|
||||
make -C mac clean
|
||||
make -C windows clean
|
||||
rm -rf build
|
||||
|
||||
zip:
|
||||
cd release; \
|
||||
rm -rf *zip comictagger-src-$(VERSION_STR) ; \
|
||||
svn checkout https://comictagger.googlecode.com/svn/trunk/ comictagger-src-$(VERSION_STR); \
|
||||
zip -r comictagger-src-$(VERSION_STR).zip comictagger-src-$(VERSION_STR); \
|
||||
rm -rf comictagger-src-$(VERSION_STR)
|
||||
|
||||
pydist:
|
||||
mkdir -p release
|
||||
rm -f release/*.zip
|
||||
python setup.py sdist --formats=zip #,gztar
|
||||
mv dist/comictagger-$(VERSION_STR).zip release
|
||||
@echo When satisfied with release, do this:
|
||||
@echo make svn_tag
|
||||
|
||||
remove_test_install:
|
||||
sudo rm -rf /usr/local/bin/comictagger.py
|
||||
sudo rm -rf /usr/local/lib/python2.7/dist-packages/comictagger*
|
||||
|
||||
#deb:
|
||||
# fpm -s python -t deb \
|
||||
# -n 'comictagger' \
|
||||
# --category 'utilities' \
|
||||
# --maintainer 'comictagger@gmail.com' \
|
||||
# --after-install debian_scripts/after_install.sh \
|
||||
# --before-remove debian_scripts/before_remove.sh \
|
||||
# -d 'python >= 2.6' \
|
||||
# -d 'python < 2.8' \
|
||||
# -d 'python-imaging' \
|
||||
# -d 'python-bs4' \
|
||||
# --deb-suggests 'rar' \
|
||||
# --deb-suggests 'unrar-free' \
|
||||
# --python-install-bin /usr/share/comictagger \
|
||||
# --python-install-lib /usr/share/comictagger \
|
||||
# setup.py
|
||||
#
|
||||
# # For now, don't require PyQt, since command-line is available without it
|
||||
# #-d 'python-qt4 >= 4.8'
|
||||
|
||||
upload:
|
||||
$(UPLOAD_TOOL) -p comictagger -s "ComicTagger $(VERSION_STR) Source" -l Featured,Type-Source -u beville -w $(PASSWORD) "release/comictagger-$(VERSION_STR).zip"
|
||||
$(UPLOAD_TOOL) -p comictagger -s "ComicTagger $(VERSION_STR) Mac OS X" -l Featured,Type-Archive -u beville -w $(PASSWORD) "release/ComicTagger-$(VERSION_STR).dmg"
|
||||
$(UPLOAD_TOOL) -p comictagger -s "ComicTagger $(VERSION_STR) Windows" -l Featured,Type-Installer -u beville -w $(PASSWORD) "release/ComicTagger v$(VERSION_STR).exe"
|
||||
python setup.py register
|
||||
|
||||
svn_tag:
|
||||
svn copy https://comictagger.googlecode.com/svn/trunk \
|
||||
https://comictagger.googlecode.com/svn/tags/$(VERSION_STR) -m "Release $(VERSION_STR)"
|
||||
|
||||
https://comictagger.googlecode.com/svn/tags/$(VERSION_STR) -m "Release $(VERSION_STR)"
|
||||
|
||||
|
31
README.txt
Normal file
@ -0,0 +1,31 @@
|
||||
ComicTagger is a multi-platform app for writing metadata to comic archives, written in Python and PyQt.
|
||||
|
||||
Features:
|
||||
|
||||
* Runs on Mac OSX, Microsoft Windows, and Linux systems
|
||||
* Communicates with an online database (Comic Vine) for acquiring metadata
|
||||
* Uses image processing to automatically match a given archive with the correct issue data
|
||||
* Batch processing in the GUI for tagging hundreds or more comics at a time
|
||||
* Reads and writes multiple tagging schemes ( ComicBookLover and ComicRack, with more planned).
|
||||
* Reads and writes RAR and Zip archives (external tools needed for writing RAR)
|
||||
* Command line interface (CLI) on all platforms (including Windows), which supports batch operations, and which can be used in native scripts for complex operations. For example, to recusrively scrape and tag all archives in a folder
|
||||
comictagger.py -R -s -o -f -t cr -v -i --nooverwrite /path/to/comics/
|
||||
|
||||
For details, screenshots, release notes, and more, visit http://code.google.com/p/comictagger/
|
||||
|
||||
Requires:
|
||||
|
||||
* python 2.6 or 2.7
|
||||
* configparser
|
||||
* python imaging (PIL) >= 1.1.6
|
||||
* beautifulsoup > 4.1
|
||||
|
||||
Optional requirement (for GUI):
|
||||
|
||||
* pyqt4
|
||||
|
||||
Install and run:
|
||||
|
||||
* ComicTagger can be run directly from this directory, using the launcher script "comictagger.py"
|
||||
|
||||
* To install on your system use: "python setup.py install". Take note in the output where comictagger.py goes!
|
@ -1,161 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>dialogMatchSelect</class>
|
||||
<widget class="QDialog" name="dialogMatchSelect">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>831</width>
|
||||
<height>506</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Select Match</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="1">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="labelCover">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>300</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>9</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="rowCount">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="columnCount">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Series</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Publisher</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Date</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelThumbnail">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>300</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Panel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Sunken</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>dialogMatchSelect</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>dialogMatchSelect</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
@ -1,55 +0,0 @@
|
||||
"""
|
||||
A PyQT4 dialog to confirm and set options for auto-tag
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
from settings import ComicTaggerSettings
|
||||
from settingswindow import SettingsWindow
|
||||
from filerenamer import FileRenamer
|
||||
import os
|
||||
import utils
|
||||
|
||||
|
||||
class AutoTagStartWindow(QtGui.QDialog):
|
||||
|
||||
def __init__( self, parent, settings, msg ):
|
||||
super(AutoTagStartWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'autotagstartwindow.ui' ), self)
|
||||
self.label.setText( msg )
|
||||
|
||||
self.settings = settings
|
||||
|
||||
self.cbxSaveOnLowConfidence.setCheckState( QtCore.Qt.Unchecked )
|
||||
self.cbxDontUseYear.setCheckState( QtCore.Qt.Unchecked )
|
||||
self.cbxAssumeIssueOne.setCheckState( QtCore.Qt.Unchecked )
|
||||
|
||||
self.autoSaveOnLow = False
|
||||
self.dontUseYear = False
|
||||
self.assumeIssueOne = False
|
||||
|
||||
|
||||
def accept( self ):
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
self.autoSaveOnLow = self.cbxSaveOnLowConfidence.isChecked()
|
||||
self.dontUseYear = self.cbxDontUseYear.isChecked()
|
||||
self.assumeIssueOne = self.cbxAssumeIssueOne.isChecked()
|
||||
|
@ -1,124 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>dialogExport</class>
|
||||
<widget class="QDialog" name="dialogExport">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::NonModal</enum>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>524</width>
|
||||
<height>248</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Auto-Tag</string>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="1" column="1">
|
||||
<widget class="QCheckBox" name="cbxDontUseYear">
|
||||
<property name="text">
|
||||
<string>Don't use publication year in indentification process</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QCheckBox" name="cbxSaveOnLowConfidence">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Save on low confidence match</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QCheckBox" name="cbxAssumeIssueOne">
|
||||
<property name="text">
|
||||
<string>If no issue number, assume "1"</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>dialogExport</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>346</x>
|
||||
<y>187</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>277</x>
|
||||
<y>104</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>dialogExport</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>346</x>
|
||||
<y>187</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>277</x>
|
||||
<y>104</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
510
comictagger.py
@ -1,507 +1,5 @@
|
||||
#!/usr/bin/python
|
||||
#!/usr/bin/env python
|
||||
from comictaggerlib.main import ctmain
|
||||
|
||||
"""
|
||||
A python script to tag comic archives
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import signal
|
||||
import os
|
||||
import traceback
|
||||
import time
|
||||
from pprint import pprint
|
||||
import json
|
||||
import platform
|
||||
import locale
|
||||
|
||||
filename_encoding = sys.getfilesystemencoding()
|
||||
|
||||
try:
|
||||
qt_available = True
|
||||
from PyQt4 import QtCore, QtGui
|
||||
from taggerwindow import TaggerWindow
|
||||
except ImportError as e:
|
||||
qt_available = False
|
||||
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
from options import Options, MetaDataStyle
|
||||
from comicarchive import ComicArchive
|
||||
from issueidentifier import IssueIdentifier
|
||||
from genericmetadata import GenericMetadata
|
||||
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from filerenamer import FileRenamer
|
||||
from cbltransformer import CBLTransformer
|
||||
|
||||
import utils
|
||||
import codecs
|
||||
|
||||
class MultipleMatch():
|
||||
def __init__( self, filename, match_list):
|
||||
self.filename = filename
|
||||
self.matches = match_list
|
||||
|
||||
class OnlineMatchResults():
|
||||
def __init__(self):
|
||||
self.goodMatches = []
|
||||
self.noMatches = []
|
||||
self.multipleMatches = []
|
||||
self.writeFailures = []
|
||||
|
||||
#-----------------------------
|
||||
|
||||
def actual_issue_data_fetch( match, settings ):
|
||||
|
||||
# now get the particular issue data
|
||||
try:
|
||||
cv_md = ComicVineTalker().fetchIssueData( match['volume_id'], match['issue_number'], settings )
|
||||
except ComicVineTalkerException:
|
||||
print "Network error while getting issue details. Save aborted"
|
||||
return None
|
||||
|
||||
if settings.apply_cbl_transform_on_cv_import:
|
||||
cv_md = CBLTransformer( cv_md, settings ).apply()
|
||||
|
||||
return cv_md
|
||||
|
||||
def actual_metadata_save( ca, opts, md ):
|
||||
|
||||
if not opts.dryrun:
|
||||
# write out the new data
|
||||
if not ca.writeMetadata( md, opts.data_style ):
|
||||
print "The tag save seemed to fail!"
|
||||
return False
|
||||
else:
|
||||
print "Save complete."
|
||||
else:
|
||||
if opts.terse:
|
||||
print "dry-run option was set, so nothing was written"
|
||||
else:
|
||||
print "dry-run option was set, so nothing was written, but here is the final set of tags:"
|
||||
print u"{0}".format(md)
|
||||
return True
|
||||
|
||||
|
||||
def post_process_matches( match_results, opts, settings ):
|
||||
# now go through the match results
|
||||
if opts.show_save_summary:
|
||||
if len( match_results.goodMatches ) > 0:
|
||||
print "\nSuccessful matches:"
|
||||
print "------------------"
|
||||
for f in match_results.goodMatches:
|
||||
print f
|
||||
|
||||
if len( match_results.noMatches ) > 0:
|
||||
print "\nNo matches:"
|
||||
print "------------------"
|
||||
for f in match_results.noMatches:
|
||||
print f
|
||||
|
||||
if len( match_results.writeFailures ) > 0:
|
||||
print "\nFile Write Failures:"
|
||||
print "------------------"
|
||||
for f in match_results.writeFailures:
|
||||
print f
|
||||
|
||||
if not opts.show_save_summary and not opts.interactive:
|
||||
#jusr quit if we're not interactive or showing the summary
|
||||
return
|
||||
|
||||
if len( match_results.multipleMatches ) > 0:
|
||||
print "\nMultiple matches:"
|
||||
print "------------------"
|
||||
for mm in match_results.multipleMatches:
|
||||
print mm.filename
|
||||
for (counter,m) in enumerate(mm.matches):
|
||||
print u" {0}. {1} #{2} [{3}] ({4}/{5}) - {6}".format(counter,
|
||||
m['series'],
|
||||
m['issue_number'],
|
||||
m['publisher'],
|
||||
m['month'],
|
||||
m['year'],
|
||||
m['issue_title'])
|
||||
if opts.interactive:
|
||||
while True:
|
||||
i = raw_input("Choose a match #, or 's' to skip: ")
|
||||
if (i.isdigit() and int(i) in range(len(mm.matches))) or i == 's':
|
||||
break
|
||||
if i != 's':
|
||||
# save the data!
|
||||
# we know at this point, that the file is all good to go
|
||||
ca = ComicArchive( mm.filename )
|
||||
md = create_local_metadata( opts, ca, ca.hasMetadata(opts.data_style) )
|
||||
cv_md = actual_issue_data_fetch(mm.matches[int(i)], settings)
|
||||
md.overlay( cv_md )
|
||||
actual_metadata_save( ca, opts, md )
|
||||
|
||||
|
||||
print
|
||||
|
||||
|
||||
def cli_mode( opts, settings ):
|
||||
if len( opts.file_list ) < 1:
|
||||
print "You must specify at least one filename. Use the -h option for more info"
|
||||
return
|
||||
|
||||
match_results = OnlineMatchResults()
|
||||
|
||||
for f in opts.file_list:
|
||||
f = f.decode(filename_encoding, 'replace')
|
||||
process_file_cli( f, opts, settings, match_results )
|
||||
sys.stdout.flush()
|
||||
|
||||
post_process_matches( match_results, opts, settings )
|
||||
|
||||
|
||||
def create_local_metadata( opts, ca, has_desired_tags ):
|
||||
|
||||
md = GenericMetadata()
|
||||
md.setDefaultPageList( ca.getNumberOfPages() )
|
||||
|
||||
if has_desired_tags:
|
||||
md = ca.readMetadata( opts.data_style )
|
||||
|
||||
# now, overlay the parsed filename info
|
||||
if opts.parse_filename:
|
||||
md.overlay( ca.metadataFromFilename() )
|
||||
|
||||
# finally, use explicit stuff
|
||||
if opts.metadata is not None:
|
||||
md.overlay( opts.metadata )
|
||||
|
||||
return md
|
||||
|
||||
def process_file_cli( filename, opts, settings, match_results ):
|
||||
|
||||
batch_mode = len( opts.file_list ) > 1
|
||||
|
||||
ca = ComicArchive(filename)
|
||||
if settings.rar_exe_path != "":
|
||||
ca.setExternalRarProgram( settings.rar_exe_path )
|
||||
|
||||
if not ca.seemsToBeAComicArchive():
|
||||
print "Sorry, but "+ filename + " is not a comic archive!"
|
||||
return
|
||||
|
||||
#if not ca.isWritableForStyle( opts.data_style ) and ( opts.delete_tags or opts.save_tags or opts.rename_file ):
|
||||
if not ca.isWritable( ) and ( opts.delete_tags or opts.copy_tags or opts.save_tags or opts.rename_file ):
|
||||
print "This archive is not writable for that tag type"
|
||||
return
|
||||
|
||||
has = [ False, False, False ]
|
||||
if ca.hasCIX(): has[ MetaDataStyle.CIX ] = True
|
||||
if ca.hasCBI(): has[ MetaDataStyle.CBI ] = True
|
||||
if ca.hasCoMet(): has[ MetaDataStyle.COMET ] = True
|
||||
|
||||
if opts.print_tags:
|
||||
|
||||
|
||||
if opts.data_style is None:
|
||||
page_count = ca.getNumberOfPages()
|
||||
|
||||
brief = ""
|
||||
|
||||
if batch_mode:
|
||||
brief = "{0}: ".format(filename)
|
||||
|
||||
if ca.isZip(): brief += "ZIP archive "
|
||||
elif ca.isRar(): brief += "RAR archive "
|
||||
elif ca.isFolder(): brief += "Folder archive "
|
||||
|
||||
brief += "({0: >3} pages)".format(page_count)
|
||||
brief += " tags:[ "
|
||||
|
||||
if not ( has[ MetaDataStyle.CBI ] or has[ MetaDataStyle.CIX ] or has[ MetaDataStyle.COMET ] ):
|
||||
brief += "none "
|
||||
else:
|
||||
if has[ MetaDataStyle.CBI ]: brief += "CBL "
|
||||
if has[ MetaDataStyle.CIX ]: brief += "CR "
|
||||
if has[ MetaDataStyle.COMET ]: brief += "CoMet "
|
||||
brief += "]"
|
||||
|
||||
print brief
|
||||
|
||||
if opts.terse:
|
||||
return
|
||||
|
||||
print
|
||||
|
||||
if opts.data_style is None or opts.data_style == MetaDataStyle.CIX:
|
||||
if has[ MetaDataStyle.CIX ]:
|
||||
print "------ComicRack tags--------"
|
||||
if opts.raw:
|
||||
print u"{0}".format(unicode(ca.readRawCIX(), errors='ignore'))
|
||||
else:
|
||||
print u"{0}".format(ca.readCIX())
|
||||
|
||||
if opts.data_style is None or opts.data_style == MetaDataStyle.CBI:
|
||||
if has[ MetaDataStyle.CBI ]:
|
||||
print "------ComicBookLover tags--------"
|
||||
if opts.raw:
|
||||
pprint(json.loads(ca.readRawCBI()))
|
||||
else:
|
||||
print u"{0}".format(ca.readCBI())
|
||||
|
||||
if opts.data_style is None or opts.data_style == MetaDataStyle.COMET:
|
||||
if has[ MetaDataStyle.COMET ]:
|
||||
print "------CoMet tags--------"
|
||||
if opts.raw:
|
||||
print u"{0}".format(ca.readRawCoMet())
|
||||
else:
|
||||
print u"{0}".format(ca.readCoMet())
|
||||
|
||||
|
||||
elif opts.delete_tags:
|
||||
style_name = MetaDataStyle.name[ opts.data_style ]
|
||||
if has[ opts.data_style ]:
|
||||
if not opts.dryrun:
|
||||
if not ca.removeMetadata( opts.data_style ):
|
||||
print "{0}: Tag removal seemed to fail!".format( filename )
|
||||
else:
|
||||
print "{0}: Removed {1} tags.".format( filename, style_name )
|
||||
else:
|
||||
print "{0}: dry-run. {1} tags not removed".format( filename, style_name )
|
||||
else:
|
||||
print "{0}: This archive doesn't have {1} tags to remove.".format( filename, style_name )
|
||||
|
||||
elif opts.copy_tags:
|
||||
dst_style_name = MetaDataStyle.name[ opts.data_style ]
|
||||
if opts.no_overwrite and has[ opts.data_style ]:
|
||||
print "{0}: Already has {1} tags. Not overwriting.".format(filename, dst_style_name)
|
||||
return
|
||||
if opts.copy_source == opts.data_style:
|
||||
print "{0}: Destination and source are same: {1}. Nothing to do.".format(filename, dst_style_name)
|
||||
return
|
||||
|
||||
src_style_name = MetaDataStyle.name[ opts.copy_source ]
|
||||
if has[ opts.copy_source ]:
|
||||
if not opts.dryrun:
|
||||
md = ca.readMetadata( opts.copy_source )
|
||||
|
||||
if settings.apply_cbl_transform_on_bulk_operation and opts.data_style == MetaDataStyle.CBI:
|
||||
md = CBLTransformer( md, settings ).apply()
|
||||
|
||||
if not ca.writeMetadata( md, opts.data_style ):
|
||||
print u"{0}: Tag copy seemed to fail!".format( filename )
|
||||
else:
|
||||
print u"{0}: Copied {1} tags to {2} .".format( filename, src_style_name, dst_style_name )
|
||||
else:
|
||||
print u"{0}: dry-run. {1} tags not copied".format( filename, src_style_name )
|
||||
else:
|
||||
print u"{0}: This archive doesn't have {1} tags to copy.".format( filename, src_style_name )
|
||||
|
||||
|
||||
elif opts.save_tags:
|
||||
|
||||
if opts.no_overwrite and has[ opts.data_style ]:
|
||||
print u"{0}: Already has {1} tags. Not overwriting.".format(filename, MetaDataStyle.name[ opts.data_style ])
|
||||
return
|
||||
|
||||
if batch_mode:
|
||||
print u"Processing {0}: ".format(filename)
|
||||
|
||||
md = create_local_metadata( opts, ca, has[ opts.data_style ] )
|
||||
|
||||
# now, search online
|
||||
if opts.search_online:
|
||||
if opts.issue_id is not None:
|
||||
# we were given the actual ID to search with
|
||||
try:
|
||||
cv_md = ComicVineTalker().fetchIssueDataByIssueID( opts.issue_id, settings )
|
||||
except ComicVineTalkerException:
|
||||
print "Network error while getting issue details. Save aborted"
|
||||
return None
|
||||
|
||||
if cv_md is None:
|
||||
print "No match for ID {0} was found.".format(opts.issue_id)
|
||||
return None
|
||||
|
||||
if settings.apply_cbl_transform_on_cv_import:
|
||||
cv_md = CBLTransformer( cv_md, settings ).apply()
|
||||
else:
|
||||
ii = IssueIdentifier( ca, settings )
|
||||
|
||||
if md is None or md.isEmpty:
|
||||
print "No metadata given to search online with!"
|
||||
return
|
||||
|
||||
def myoutput( text ):
|
||||
if opts.verbose:
|
||||
IssueIdentifier.defaultWriteOutput( text )
|
||||
|
||||
# use our overlayed MD struct to search
|
||||
ii.setAdditionalMetadata( md )
|
||||
ii.onlyUseAdditionalMetaData = True
|
||||
ii.setOutputFunction( myoutput )
|
||||
ii.cover_page_index = md.getCoverPageIndexList()[0]
|
||||
matches = ii.search()
|
||||
|
||||
result = ii.search_result
|
||||
|
||||
found_match = False
|
||||
choices = False
|
||||
low_confidence = False
|
||||
|
||||
if result == ii.ResultNoMatches:
|
||||
pass
|
||||
elif result == ii.ResultFoundMatchButBadCoverScore:
|
||||
low_confidence = True
|
||||
found_match = True
|
||||
elif result == ii.ResultFoundMatchButNotFirstPage :
|
||||
found_match = True
|
||||
elif result == ii.ResultMultipleMatchesWithBadImageScores:
|
||||
low_confidence = True
|
||||
choices = True
|
||||
elif result == ii.ResultOneGoodMatch:
|
||||
found_match = True
|
||||
elif result == ii.ResultMultipleGoodMatches:
|
||||
choices = True
|
||||
|
||||
if choices:
|
||||
print "Online search: Multiple matches. Save aborted"
|
||||
match_results.multipleMatches.append(MultipleMatch(filename,matches))
|
||||
return
|
||||
if low_confidence and opts.abortOnLowConfidence:
|
||||
print "Online search: Low confidence match. Save aborted"
|
||||
match_results.noMatches.append(filename)
|
||||
return
|
||||
if not found_match:
|
||||
print "Online search: No match found. Save aborted"
|
||||
match_results.noMatches.append(filename)
|
||||
return
|
||||
|
||||
|
||||
# we got here, so we have a single match
|
||||
|
||||
# now get the particular issue data
|
||||
cv_md = actual_issue_data_fetch(matches[0], settings)
|
||||
if cv_md is None:
|
||||
return
|
||||
|
||||
md.overlay( cv_md )
|
||||
|
||||
# ok, done building our metadata. time to save
|
||||
if not actual_metadata_save( ca, opts, md ):
|
||||
match_results.writeFailures.append(filename)
|
||||
else:
|
||||
match_results.goodMatches.append(filename)
|
||||
|
||||
elif opts.rename_file:
|
||||
|
||||
msg_hdr = ""
|
||||
if batch_mode:
|
||||
msg_hdr = u"{0}: ".format(filename)
|
||||
|
||||
if opts.data_style is not None:
|
||||
use_tags = has[ opts.data_style ]
|
||||
else:
|
||||
use_tags = False
|
||||
|
||||
md = create_local_metadata( opts, ca, use_tags )
|
||||
|
||||
if md.series is None:
|
||||
print msg_hdr + "Can't rename without series name"
|
||||
return
|
||||
|
||||
new_ext = None # default
|
||||
if settings.rename_extension_based_on_archive:
|
||||
if ca.isZip():
|
||||
new_ext = ".cbz"
|
||||
elif ca.isRar():
|
||||
new_ext = ".cbr"
|
||||
|
||||
renamer = FileRenamer( md )
|
||||
renamer.setTemplate( settings.rename_template )
|
||||
renamer.setIssueZeroPadding( settings.rename_issue_number_padding )
|
||||
renamer.setSmartCleanup( settings.rename_use_smart_string_cleanup )
|
||||
|
||||
new_name = renamer.determineName( filename, ext=new_ext )
|
||||
|
||||
if new_name == os.path.basename(filename):
|
||||
print msg_hdr + "Filename is already good!"
|
||||
return
|
||||
|
||||
folder = os.path.dirname( os.path.abspath( filename ) )
|
||||
new_abs_path = utils.unique_file( os.path.join( folder, new_name ) )
|
||||
|
||||
suffix = ""
|
||||
if not opts.dryrun:
|
||||
# rename the file
|
||||
os.rename( filename, new_abs_path )
|
||||
else:
|
||||
suffix = " (dry-run, no change)"
|
||||
|
||||
print u"renamed '{0}' -> '{1}' {2}".format(os.path.basename(filename), new_name, suffix)
|
||||
|
||||
#-----------------------------
|
||||
|
||||
def main():
|
||||
|
||||
# try to make stdout encodings happy for unicode
|
||||
if platform.system() == "Darwin":
|
||||
preferred_encoding = "utf-8"
|
||||
else:
|
||||
preferred_encoding = locale.getpreferredencoding()
|
||||
sys.stdout = codecs.getwriter(preferred_encoding)(sys.stdout)
|
||||
|
||||
opts = Options()
|
||||
opts.parseCmdLineArgs()
|
||||
|
||||
settings = ComicTaggerSettings()
|
||||
# make sure unrar program is in the path for the UnRAR class
|
||||
utils.addtopath(os.path.dirname(settings.unrar_exe_path))
|
||||
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
|
||||
if not qt_available and not opts.no_gui:
|
||||
opts.no_gui = True
|
||||
print "QT is not available."
|
||||
|
||||
if opts.no_gui:
|
||||
cli_mode( opts, settings )
|
||||
|
||||
else:
|
||||
|
||||
app = QtGui.QApplication(sys.argv)
|
||||
|
||||
if platform.system() != "Linux":
|
||||
img = QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/tags.png' ))
|
||||
splash = QtGui.QSplashScreen(img)
|
||||
splash.show()
|
||||
splash.raise_()
|
||||
app.processEvents()
|
||||
|
||||
try:
|
||||
tagger_window = TaggerWindow( opts.file_list, settings )
|
||||
tagger_window.show()
|
||||
|
||||
if platform.system() != "Linux":
|
||||
splash.finish( tagger_window )
|
||||
|
||||
sys.exit(app.exec_())
|
||||
except Exception, e:
|
||||
QtGui.QMessageBox.critical(QtGui.QMainWindow(), "Error", "Unhandled exception in app:\n" + traceback.format_exc() )
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
ctmain()
|
@ -120,7 +120,9 @@ class RarFileImplementation(object):
|
||||
if len(accum)==2:
|
||||
data = {}
|
||||
data['index'] = i
|
||||
data['filename'] = accum[0].strip()
|
||||
#!!!ATB - changed this because it was choking when a folder or file started with a space.
|
||||
#!!! now, just strip off the first char in the string
|
||||
data['filename'] = accum[0].rstrip()[1:]
|
||||
info = re_spaces.split(accum[1].strip())
|
||||
data['size'] = int(info[0])
|
||||
attr = info[5]
|
0
comictaggerlib/__init__.py
Normal file
@ -26,6 +26,10 @@ from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
|
||||
|
||||
from imagefetcher import ImageFetcher
|
||||
from settings import ComicTaggerSettings
|
||||
from comicarchive import MetaDataStyle
|
||||
from coverimagewidget import CoverImageWidget
|
||||
from comicvinetalker import ComicVineTalker
|
||||
import utils
|
||||
|
||||
class AutoTagMatchWindow(QtGui.QDialog):
|
||||
|
||||
@ -34,11 +38,28 @@ class AutoTagMatchWindow(QtGui.QDialog):
|
||||
def __init__(self, parent, match_set_list, style, fetch_func):
|
||||
super(AutoTagMatchWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'autotagmatchwindow.ui' ), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('matchselectionwindow.ui' ), self)
|
||||
|
||||
self.skipButton = QtGui.QPushButton(self.tr("Skip"))
|
||||
self.altCoverWidget = CoverImageWidget( self.altCoverContainer, CoverImageWidget.AltCoverMode )
|
||||
gridlayout = QtGui.QGridLayout( self.altCoverContainer )
|
||||
gridlayout.addWidget( self.altCoverWidget )
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
|
||||
self.archiveCoverWidget = CoverImageWidget( self.archiveCoverContainer, CoverImageWidget.ArchiveMode )
|
||||
gridlayout = QtGui.QGridLayout( self.archiveCoverContainer )
|
||||
gridlayout.addWidget( self.archiveCoverWidget )
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
|
||||
utils.reduceWidgetFontSize( self.twList )
|
||||
utils.reduceWidgetFontSize( self.teDescription, 1 )
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
self.skipButton = QtGui.QPushButton(self.tr("Skip to Next"))
|
||||
self.buttonBox.addButton(self.skipButton, QtGui.QDialogButtonBox.ActionRole)
|
||||
self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText("Accept and Next")
|
||||
self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText("Accept and Write Tags")
|
||||
|
||||
self.match_set_list = match_set_list
|
||||
self.style = style
|
||||
@ -56,18 +77,18 @@ class AutoTagMatchWindow(QtGui.QDialog):
|
||||
|
||||
self.current_match_set = self.match_set_list[ self.current_match_set_idx ]
|
||||
|
||||
|
||||
if self.current_match_set_idx + 1 == len( self.match_set_list ):
|
||||
self.skipButton.setDisabled(True)
|
||||
self.buttonBox.button(QtGui.QDialogButtonBox.Cancel).setDisabled(True)
|
||||
#self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText("Accept")
|
||||
self.skipButton.setText(self.tr("Skip"))
|
||||
|
||||
self.setCoverImage()
|
||||
self.populateTable()
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.current_row = 0
|
||||
self.twList.selectRow( 0 )
|
||||
|
||||
path = self.current_match_set.ca.path
|
||||
self.setWindowTitle( "Select correct match ({0} of {1}): {2}".format(
|
||||
self.setWindowTitle( u"Select correct match or skip ({0} of {1}): {2}".format(
|
||||
self.current_match_set_idx+1,
|
||||
len( self.match_set_list ),
|
||||
os.path.split(path)[1] ))
|
||||
@ -85,6 +106,8 @@ class AutoTagMatchWindow(QtGui.QDialog):
|
||||
|
||||
item_text = match['series']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setData( QtCore.Qt.UserRole, (match,))
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, item)
|
||||
|
||||
@ -93,21 +116,39 @@ class AutoTagMatchWindow(QtGui.QDialog):
|
||||
else:
|
||||
item_text = u"Unknown"
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 1, item)
|
||||
|
||||
item_text = ""
|
||||
month_str = u""
|
||||
year_str = u"????"
|
||||
if match['month'] is not None:
|
||||
item_text = u"{0}/".format(match['month'])
|
||||
month_str = u"-{0:02d}".format(int(match['month']))
|
||||
if match['year'] is not None:
|
||||
item_text += u"{0}".format(match['year'])
|
||||
else:
|
||||
item_text += u"????"
|
||||
year_str = u"{0}".format(match['year'])
|
||||
|
||||
item_text = year_str + month_str
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 2, item)
|
||||
|
||||
item_text = match['issue_title']
|
||||
if item_text is None:
|
||||
item_text = ""
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 3, item)
|
||||
|
||||
row += 1
|
||||
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.sortItems( 2 , QtCore.Qt.AscendingOrder )
|
||||
self.twList.selectRow(0)
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.horizontalHeader().setStretchLastSection(True)
|
||||
|
||||
|
||||
def cellDoubleClicked( self, r, c ):
|
||||
@ -119,34 +160,22 @@ class AutoTagMatchWindow(QtGui.QDialog):
|
||||
return
|
||||
if prev is not None and prev.row() == curr.row():
|
||||
return
|
||||
|
||||
self.current_row = curr.row()
|
||||
|
||||
# list selection was changed, update the the issue cover
|
||||
self.labelThumbnail.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
|
||||
self.cover_fetcher = ImageFetcher( )
|
||||
self.cover_fetcher.fetchComplete.connect(self.coverFetchComplete)
|
||||
self.cover_fetcher.fetch( self.current_match_set.matches[self.current_row]['img_url'] )
|
||||
|
||||
# called when the image is done loading
|
||||
def coverFetchComplete( self, image_data, issue_id ):
|
||||
img = QtGui.QImage()
|
||||
img.loadFromData( image_data )
|
||||
self.labelThumbnail.setPixmap(QtGui.QPixmap(img))
|
||||
self.altCoverWidget.setIssueID( self.currentMatch()['issue_id'] )
|
||||
if self.currentMatch()['description'] is None:
|
||||
self.teDescription.setText ( "" )
|
||||
else:
|
||||
self.teDescription.setText ( self.currentMatch()['description'] )
|
||||
|
||||
def setCoverImage( self ):
|
||||
ca = self.current_match_set.ca
|
||||
cover_idx = ca.readMetadata(self.style).getCoverPageIndexList()[0]
|
||||
image_data = ca.getPage( cover_idx )
|
||||
self.labelCover.setScaledContents(True)
|
||||
if image_data is not None:
|
||||
img = QtGui.QImage()
|
||||
img.loadFromData( image_data )
|
||||
self.labelCover.setPixmap(QtGui.QPixmap(img))
|
||||
else:
|
||||
self.labelCover.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
self.archiveCoverWidget.setArchive(ca)
|
||||
|
||||
def currentMatch( self ):
|
||||
row = self.twList.currentRow()
|
||||
match = self.twList.item(row, 0).data( QtCore.Qt.UserRole ).toPyObject()[0]
|
||||
return match
|
||||
|
||||
def accept(self):
|
||||
|
||||
self.saveMatch()
|
||||
@ -180,7 +209,7 @@ class AutoTagMatchWindow(QtGui.QDialog):
|
||||
|
||||
def saveMatch( self ):
|
||||
|
||||
match = self.current_match_set.matches[self.current_row]
|
||||
match = self.currentMatch()
|
||||
ca = self.current_match_set.ca
|
||||
|
||||
md = ca.readMetadata( self.style )
|
||||
@ -196,6 +225,8 @@ class AutoTagMatchWindow(QtGui.QDialog):
|
||||
QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
|
||||
md.overlay( cv_md )
|
||||
success = ca.writeMetadata( md, self.style )
|
||||
ca.loadCache( [ MetaDataStyle.CBI, MetaDataStyle.CIX ] )
|
||||
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
|
||||
if not success:
|
@ -22,7 +22,7 @@ import sys
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
import os
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
import utils
|
||||
|
||||
class AutoTagProgressWindow(QtGui.QDialog):
|
||||
|
||||
@ -30,17 +30,16 @@ class AutoTagProgressWindow(QtGui.QDialog):
|
||||
def __init__(self, parent):
|
||||
super(AutoTagProgressWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'autotagprogresswindow.ui' ), self)
|
||||
self.lblTest.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
self.lblArchive.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('autotagprogresswindow.ui' ), self)
|
||||
self.lblTest.setPixmap(QtGui.QPixmap(ComicTaggerSettings.getGraphic('nocover.png')))
|
||||
self.lblArchive.setPixmap(QtGui.QPixmap(ComicTaggerSettings.getGraphic('nocover.png')))
|
||||
self.isdone = False
|
||||
|
||||
# we can't specify relative font sizes in the UI designer, so
|
||||
# make font for scroll window a smidge smaller
|
||||
f = self.textEdit.font()
|
||||
if f.pointSize() > 10:
|
||||
f.setPointSize( f.pointSize() - 2 )
|
||||
self.textEdit.setFont( f )
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
utils.reduceWidgetFontSize( self.textEdit )
|
||||
|
||||
def setArchiveImage( self, img_data):
|
||||
self.setCoverImage( img_data, self.lblArchive )
|
||||
@ -55,7 +54,7 @@ class AutoTagProgressWindow(QtGui.QDialog):
|
||||
label.setPixmap(QtGui.QPixmap(img))
|
||||
label.setScaledContents(True)
|
||||
else:
|
||||
label.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
label.setPixmap(QtGui.QPixmap(ComicTaggerSettings.getGraphic('nocover.png')))
|
||||
label.setScaledContents(True)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtCore.QCoreApplication.processEvents()
|
104
comictaggerlib/autotagstartwindow.py
Normal file
@ -0,0 +1,104 @@
|
||||
"""
|
||||
A PyQT4 dialog to confirm and set options for auto-tag
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
from settings import ComicTaggerSettings
|
||||
from settingswindow import SettingsWindow
|
||||
from filerenamer import FileRenamer
|
||||
import os
|
||||
import utils
|
||||
|
||||
|
||||
class AutoTagStartWindow(QtGui.QDialog):
|
||||
|
||||
def __init__( self, parent, settings, msg ):
|
||||
super(AutoTagStartWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('autotagstartwindow.ui' ), self)
|
||||
self.label.setText( msg )
|
||||
|
||||
self.setWindowFlags(self.windowFlags() &
|
||||
~QtCore.Qt.WindowContextHelpButtonHint )
|
||||
|
||||
self.settings = settings
|
||||
|
||||
self.cbxSaveOnLowConfidence.setCheckState( QtCore.Qt.Unchecked )
|
||||
self.cbxDontUseYear.setCheckState( QtCore.Qt.Unchecked )
|
||||
self.cbxAssumeIssueOne.setCheckState( QtCore.Qt.Unchecked )
|
||||
self.cbxIgnoreLeadingDigitsInFilename.setCheckState( QtCore.Qt.Unchecked )
|
||||
self.cbxRemoveAfterSuccess.setCheckState( QtCore.Qt.Unchecked )
|
||||
self.cbxSpecifySearchString.setCheckState( QtCore.Qt.Unchecked )
|
||||
self.leNameLengthMatchTolerance.setText( str(self.settings.id_length_delta_thresh) )
|
||||
self.leSearchString.setEnabled( False )
|
||||
|
||||
nlmtTip = (
|
||||
""" <html>The <b>Name Length Match Tolerance</b> is for eliminating automatic
|
||||
search matches that are too long compared to your series name search. The higher
|
||||
it is, the more likely to have a good match, but each search will take longer and
|
||||
use more bandwidth. Too low, and only the very closest lexical matches will be
|
||||
explored.</html>""" )
|
||||
|
||||
self.leNameLengthMatchTolerance.setToolTip(nlmtTip)
|
||||
|
||||
ssTip = (
|
||||
"""<html>
|
||||
The <b>series search string</b> specifies the search string to be used for all selected archives.
|
||||
Use this when trying to match archives with hard-to-parse or incorrect filenames. All archives selected
|
||||
should be from the same series.
|
||||
</html>"""
|
||||
)
|
||||
self.leSearchString.setToolTip(ssTip)
|
||||
self.cbxSpecifySearchString.setToolTip(ssTip)
|
||||
|
||||
|
||||
validator = QtGui.QIntValidator(0, 99, self)
|
||||
self.leNameLengthMatchTolerance.setValidator(validator)
|
||||
|
||||
self.cbxSpecifySearchString.stateChanged.connect(self.searchStringToggle)
|
||||
|
||||
self.autoSaveOnLow = False
|
||||
self.dontUseYear = False
|
||||
self.assumeIssueOne = False
|
||||
self.ignoreLeadingDigitsInFilename = False
|
||||
self.removeAfterSuccess = False
|
||||
self.searchString = None
|
||||
self.nameLengthMatchTolerance = self.settings.id_length_delta_thresh
|
||||
|
||||
def searchStringToggle(self):
|
||||
enable = self.cbxSpecifySearchString.isChecked()
|
||||
self.leSearchString.setEnabled( enable )
|
||||
|
||||
|
||||
def accept( self ):
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
self.autoSaveOnLow = self.cbxSaveOnLowConfidence.isChecked()
|
||||
self.dontUseYear = self.cbxDontUseYear.isChecked()
|
||||
self.assumeIssueOne = self.cbxAssumeIssueOne.isChecked()
|
||||
self.ignoreLeadingDigitsInFilename = self.cbxIgnoreLeadingDigitsInFilename.isChecked()
|
||||
self.removeAfterSuccess = self.cbxRemoveAfterSuccess.isChecked()
|
||||
self.nameLengthMatchTolerance = int(self.leNameLengthMatchTolerance.text())
|
||||
|
||||
if self.cbxSpecifySearchString.isChecked():
|
||||
self.searchString = unicode(self.leSearchString.text())
|
||||
if len(self.searchString) == 0:
|
||||
self.searchString = None
|
||||
|
539
comictaggerlib/cli.py
Normal file
@ -0,0 +1,539 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
"""
|
||||
Comic tagger CLI functions
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2013 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import signal
|
||||
import os
|
||||
import traceback
|
||||
import time
|
||||
from pprint import pprint
|
||||
import json
|
||||
import platform
|
||||
import locale
|
||||
|
||||
filename_encoding = sys.getfilesystemencoding()
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
from options import Options
|
||||
from comicarchive import ComicArchive, MetaDataStyle
|
||||
from issueidentifier import IssueIdentifier
|
||||
from genericmetadata import GenericMetadata
|
||||
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from filerenamer import FileRenamer
|
||||
from cbltransformer import CBLTransformer
|
||||
|
||||
import utils
|
||||
import codecs
|
||||
|
||||
class MultipleMatch():
|
||||
def __init__( self, filename, match_list):
|
||||
self.filename = filename
|
||||
self.matches = match_list
|
||||
|
||||
class OnlineMatchResults():
|
||||
def __init__(self):
|
||||
self.goodMatches = []
|
||||
self.noMatches = []
|
||||
self.multipleMatches = []
|
||||
self.lowConfidenceMatches = []
|
||||
self.writeFailures = []
|
||||
self.fetchDataFailures = []
|
||||
|
||||
#-----------------------------
|
||||
|
||||
def actual_issue_data_fetch( match, settings ):
|
||||
|
||||
# now get the particular issue data
|
||||
try:
|
||||
cv_md = ComicVineTalker().fetchIssueData( match['volume_id'], match['issue_number'], settings )
|
||||
except ComicVineTalkerException:
|
||||
print >> sys.stderr, "Network error while getting issue details. Save aborted"
|
||||
return None
|
||||
|
||||
if settings.apply_cbl_transform_on_cv_import:
|
||||
cv_md = CBLTransformer( cv_md, settings ).apply()
|
||||
|
||||
return cv_md
|
||||
|
||||
def actual_metadata_save( ca, opts, md ):
|
||||
|
||||
if not opts.dryrun:
|
||||
# write out the new data
|
||||
if not ca.writeMetadata( md, opts.data_style ):
|
||||
print >> sys.stderr,"The tag save seemed to fail!"
|
||||
return False
|
||||
else:
|
||||
print >> sys.stderr,"Save complete."
|
||||
else:
|
||||
if opts.terse:
|
||||
print >> sys.stderr,"dry-run option was set, so nothing was written"
|
||||
else:
|
||||
print >> sys.stderr,"dry-run option was set, so nothing was written, but here is the final set of tags:"
|
||||
print u"{0}".format(md)
|
||||
return True
|
||||
|
||||
def display_match_set_for_choice( label, match_set, opts, settings ):
|
||||
print u"{0} -- {1}:".format(match_set.filename, label )
|
||||
|
||||
# sort match list by year
|
||||
match_set.matches.sort(key=lambda k: k['year'])
|
||||
|
||||
for (counter,m) in enumerate(match_set.matches):
|
||||
counter += 1
|
||||
print u" {0}. {1} #{2} [{3}] ({4}/{5}) - {6}".format(counter,
|
||||
m['series'],
|
||||
m['issue_number'],
|
||||
m['publisher'],
|
||||
m['month'],
|
||||
m['year'],
|
||||
m['issue_title'])
|
||||
if opts.interactive:
|
||||
while True:
|
||||
i = raw_input("Choose a match #, or 's' to skip: ")
|
||||
if (i.isdigit() and int(i) in range(1,len(match_set.matches)+1)) or i == 's':
|
||||
break
|
||||
if i != 's':
|
||||
i = int(i) - 1
|
||||
# save the data!
|
||||
# we know at this point, that the file is all good to go
|
||||
ca = ComicArchive( match_set.filename, settings )
|
||||
md = create_local_metadata( opts, ca, ca.hasMetadata(opts.data_style) )
|
||||
cv_md = actual_issue_data_fetch(match_set.matches[int(i)], settings)
|
||||
md.overlay( cv_md )
|
||||
actual_metadata_save( ca, opts, md )
|
||||
|
||||
|
||||
def post_process_matches( match_results, opts, settings ):
|
||||
# now go through the match results
|
||||
if opts.show_save_summary:
|
||||
if len( match_results.goodMatches ) > 0:
|
||||
print "\nSuccessful matches:"
|
||||
print "------------------"
|
||||
for f in match_results.goodMatches:
|
||||
print f
|
||||
|
||||
if len( match_results.noMatches ) > 0:
|
||||
print "\nNo matches:"
|
||||
print "------------------"
|
||||
for f in match_results.noMatches:
|
||||
print f
|
||||
|
||||
if len( match_results.writeFailures ) > 0:
|
||||
print "\nFile Write Failures:"
|
||||
print "------------------"
|
||||
for f in match_results.writeFailures:
|
||||
print f
|
||||
|
||||
if len( match_results.fetchDataFailures ) > 0:
|
||||
print "\nNetwork Data Fetch Failures:"
|
||||
print "------------------"
|
||||
for f in match_results.fetchDataFailures:
|
||||
print f
|
||||
|
||||
if not opts.show_save_summary and not opts.interactive:
|
||||
#just quit if we're not interactive or showing the summary
|
||||
return
|
||||
|
||||
if len( match_results.multipleMatches ) > 0:
|
||||
print "\nArchives with multiple high-confidence matches:"
|
||||
print "------------------"
|
||||
for match_set in match_results.multipleMatches:
|
||||
display_match_set_for_choice( "Multiple high-confidence matches", match_set, opts, settings )
|
||||
|
||||
if len( match_results.lowConfidenceMatches ) > 0:
|
||||
print "\nArchives with low-confidence matches:"
|
||||
print "------------------"
|
||||
for match_set in match_results.lowConfidenceMatches:
|
||||
if len( match_set.matches) == 1:
|
||||
label = "Single low-confidence match"
|
||||
else:
|
||||
label = "Multiple low-confidence matches"
|
||||
|
||||
display_match_set_for_choice( label, match_set, opts, settings )
|
||||
|
||||
|
||||
def cli_mode( opts, settings ):
|
||||
if len( opts.file_list ) < 1:
|
||||
print >> sys.stderr,"You must specify at least one filename. Use the -h option for more info"
|
||||
return
|
||||
|
||||
match_results = OnlineMatchResults()
|
||||
|
||||
for f in opts.file_list:
|
||||
if type(f) == str:
|
||||
f = f.decode(filename_encoding, 'replace')
|
||||
process_file_cli( f, opts, settings, match_results )
|
||||
sys.stdout.flush()
|
||||
|
||||
post_process_matches( match_results, opts, settings )
|
||||
|
||||
|
||||
def create_local_metadata( opts, ca, has_desired_tags ):
|
||||
|
||||
md = GenericMetadata()
|
||||
md.setDefaultPageList( ca.getNumberOfPages() )
|
||||
|
||||
if has_desired_tags:
|
||||
md = ca.readMetadata( opts.data_style )
|
||||
|
||||
# now, overlay the parsed filename info
|
||||
if opts.parse_filename:
|
||||
md.overlay( ca.metadataFromFilename() )
|
||||
|
||||
# finally, use explicit stuff
|
||||
if opts.metadata is not None:
|
||||
md.overlay( opts.metadata )
|
||||
|
||||
return md
|
||||
|
||||
def process_file_cli( filename, opts, settings, match_results ):
|
||||
|
||||
batch_mode = len( opts.file_list ) > 1
|
||||
|
||||
ca = ComicArchive(filename, settings)
|
||||
|
||||
if not os.path.lexists( filename ):
|
||||
print >> sys.stderr,"Cannot find "+ filename
|
||||
return
|
||||
|
||||
if not ca.seemsToBeAComicArchive():
|
||||
print >> sys.stderr,"Sorry, but "+ filename + " is not a comic archive!"
|
||||
return
|
||||
|
||||
#if not ca.isWritableForStyle( opts.data_style ) and ( opts.delete_tags or opts.save_tags or opts.rename_file ):
|
||||
if not ca.isWritable( ) and ( opts.delete_tags or opts.copy_tags or opts.save_tags or opts.rename_file ):
|
||||
print >> sys.stderr,"This archive is not writable for that tag type"
|
||||
return
|
||||
|
||||
has = [ False, False, False ]
|
||||
if ca.hasCIX(): has[ MetaDataStyle.CIX ] = True
|
||||
if ca.hasCBI(): has[ MetaDataStyle.CBI ] = True
|
||||
if ca.hasCoMet(): has[ MetaDataStyle.COMET ] = True
|
||||
|
||||
if opts.print_tags:
|
||||
|
||||
|
||||
if opts.data_style is None:
|
||||
page_count = ca.getNumberOfPages()
|
||||
|
||||
brief = ""
|
||||
|
||||
if batch_mode:
|
||||
brief = u"{0}: ".format(filename)
|
||||
|
||||
if ca.isZip(): brief += "ZIP archive "
|
||||
elif ca.isRar(): brief += "RAR archive "
|
||||
elif ca.isFolder(): brief += "Folder archive "
|
||||
|
||||
brief += "({0: >3} pages)".format(page_count)
|
||||
brief += " tags:[ "
|
||||
|
||||
if not ( has[ MetaDataStyle.CBI ] or has[ MetaDataStyle.CIX ] or has[ MetaDataStyle.COMET ] ):
|
||||
brief += "none "
|
||||
else:
|
||||
if has[ MetaDataStyle.CBI ]: brief += "CBL "
|
||||
if has[ MetaDataStyle.CIX ]: brief += "CR "
|
||||
if has[ MetaDataStyle.COMET ]: brief += "CoMet "
|
||||
brief += "]"
|
||||
|
||||
print brief
|
||||
|
||||
if opts.terse:
|
||||
return
|
||||
|
||||
print
|
||||
|
||||
if opts.data_style is None or opts.data_style == MetaDataStyle.CIX:
|
||||
if has[ MetaDataStyle.CIX ]:
|
||||
print "------ComicRack tags--------"
|
||||
if opts.raw:
|
||||
print u"{0}".format(unicode(ca.readRawCIX(), errors='ignore'))
|
||||
else:
|
||||
print u"{0}".format(ca.readCIX())
|
||||
|
||||
if opts.data_style is None or opts.data_style == MetaDataStyle.CBI:
|
||||
if has[ MetaDataStyle.CBI ]:
|
||||
print "------ComicBookLover tags--------"
|
||||
if opts.raw:
|
||||
pprint(json.loads(ca.readRawCBI()))
|
||||
else:
|
||||
print u"{0}".format(ca.readCBI())
|
||||
|
||||
if opts.data_style is None or opts.data_style == MetaDataStyle.COMET:
|
||||
if has[ MetaDataStyle.COMET ]:
|
||||
print "------CoMet tags--------"
|
||||
if opts.raw:
|
||||
print u"{0}".format(ca.readRawCoMet())
|
||||
else:
|
||||
print u"{0}".format(ca.readCoMet())
|
||||
|
||||
|
||||
elif opts.delete_tags:
|
||||
style_name = MetaDataStyle.name[ opts.data_style ]
|
||||
if has[ opts.data_style ]:
|
||||
if not opts.dryrun:
|
||||
if not ca.removeMetadata( opts.data_style ):
|
||||
print u"{0}: Tag removal seemed to fail!".format( filename )
|
||||
else:
|
||||
print u"{0}: Removed {1} tags.".format( filename, style_name )
|
||||
else:
|
||||
print u"{0}: dry-run. {1} tags not removed".format( filename, style_name )
|
||||
else:
|
||||
print u"{0}: This archive doesn't have {1} tags to remove.".format( filename, style_name )
|
||||
|
||||
elif opts.copy_tags:
|
||||
dst_style_name = MetaDataStyle.name[ opts.data_style ]
|
||||
if opts.no_overwrite and has[ opts.data_style ]:
|
||||
print u"{0}: Already has {1} tags. Not overwriting.".format(filename, dst_style_name)
|
||||
return
|
||||
if opts.copy_source == opts.data_style:
|
||||
print u"{0}: Destination and source are same: {1}. Nothing to do.".format(filename, dst_style_name)
|
||||
return
|
||||
|
||||
src_style_name = MetaDataStyle.name[ opts.copy_source ]
|
||||
if has[ opts.copy_source ]:
|
||||
if not opts.dryrun:
|
||||
md = ca.readMetadata( opts.copy_source )
|
||||
|
||||
if settings.apply_cbl_transform_on_bulk_operation and opts.data_style == MetaDataStyle.CBI:
|
||||
md = CBLTransformer( md, settings ).apply()
|
||||
|
||||
if not ca.writeMetadata( md, opts.data_style ):
|
||||
print u"{0}: Tag copy seemed to fail!".format( filename )
|
||||
else:
|
||||
print u"{0}: Copied {1} tags to {2} .".format( filename, src_style_name, dst_style_name )
|
||||
else:
|
||||
print u"{0}: dry-run. {1} tags not copied".format( filename, src_style_name )
|
||||
else:
|
||||
print u"{0}: This archive doesn't have {1} tags to copy.".format( filename, src_style_name )
|
||||
|
||||
|
||||
elif opts.save_tags:
|
||||
|
||||
if opts.no_overwrite and has[ opts.data_style ]:
|
||||
print u"{0}: Already has {1} tags. Not overwriting.".format(filename, MetaDataStyle.name[ opts.data_style ])
|
||||
return
|
||||
|
||||
if batch_mode:
|
||||
print u"Processing {0}...".format(filename)
|
||||
|
||||
md = create_local_metadata( opts, ca, has[ opts.data_style ] )
|
||||
if md.issue is None or md.issue == "":
|
||||
if opts.assume_issue_is_one_if_not_set:
|
||||
md.issue = "1"
|
||||
|
||||
# now, search online
|
||||
if opts.search_online:
|
||||
if opts.issue_id is not None:
|
||||
# we were given the actual ID to search with
|
||||
try:
|
||||
cv_md = ComicVineTalker().fetchIssueDataByIssueID( opts.issue_id, settings )
|
||||
except ComicVineTalkerException:
|
||||
print >> sys.stderr,"Network error while getting issue details. Save aborted"
|
||||
match_results.fetchDataFailures.append(filename)
|
||||
return
|
||||
|
||||
if cv_md is None:
|
||||
print >> sys.stderr,"No match for ID {0} was found.".format(opts.issue_id)
|
||||
match_results.noMatches.append(filename)
|
||||
return
|
||||
|
||||
if settings.apply_cbl_transform_on_cv_import:
|
||||
cv_md = CBLTransformer( cv_md, settings ).apply()
|
||||
else:
|
||||
ii = IssueIdentifier( ca, settings )
|
||||
|
||||
if md is None or md.isEmpty:
|
||||
print >> sys.stderr,"No metadata given to search online with!"
|
||||
match_results.noMatches.append(filename)
|
||||
return
|
||||
|
||||
def myoutput( text ):
|
||||
if opts.verbose:
|
||||
IssueIdentifier.defaultWriteOutput( text )
|
||||
|
||||
# use our overlayed MD struct to search
|
||||
ii.setAdditionalMetadata( md )
|
||||
ii.onlyUseAdditionalMetaData = True
|
||||
ii.setOutputFunction( myoutput )
|
||||
ii.cover_page_index = md.getCoverPageIndexList()[0]
|
||||
matches = ii.search()
|
||||
|
||||
result = ii.search_result
|
||||
|
||||
found_match = False
|
||||
choices = False
|
||||
low_confidence = False
|
||||
|
||||
if result == ii.ResultNoMatches:
|
||||
pass
|
||||
elif result == ii.ResultFoundMatchButBadCoverScore:
|
||||
low_confidence = True
|
||||
found_match = True
|
||||
elif result == ii.ResultFoundMatchButNotFirstPage :
|
||||
found_match = True
|
||||
elif result == ii.ResultMultipleMatchesWithBadImageScores:
|
||||
low_confidence = True
|
||||
choices = True
|
||||
elif result == ii.ResultOneGoodMatch:
|
||||
found_match = True
|
||||
elif result == ii.ResultMultipleGoodMatches:
|
||||
choices = True
|
||||
|
||||
if choices:
|
||||
if low_confidence:
|
||||
print >> sys.stderr,"Online search: Multiple low confidence matches. Save aborted"
|
||||
match_results.lowConfidenceMatches.append(MultipleMatch(filename,matches))
|
||||
return
|
||||
else:
|
||||
print >> sys.stderr,"Online search: Multiple good matches. Save aborted"
|
||||
match_results.multipleMatches.append(MultipleMatch(filename,matches))
|
||||
return
|
||||
if low_confidence and opts.abortOnLowConfidence:
|
||||
print >> sys.stderr,"Online search: Low confidence match. Save aborted"
|
||||
match_results.lowConfidenceMatches.append(MultipleMatch(filename,matches))
|
||||
return
|
||||
if not found_match:
|
||||
print >> sys.stderr,"Online search: No match found. Save aborted"
|
||||
match_results.noMatches.append(filename)
|
||||
return
|
||||
|
||||
|
||||
# we got here, so we have a single match
|
||||
|
||||
# now get the particular issue data
|
||||
cv_md = actual_issue_data_fetch(matches[0], settings)
|
||||
if cv_md is None:
|
||||
match_results.fetchDataFailures.append(filename)
|
||||
return
|
||||
|
||||
md.overlay( cv_md )
|
||||
|
||||
# ok, done building our metadata. time to save
|
||||
if not actual_metadata_save( ca, opts, md ):
|
||||
match_results.writeFailures.append(filename)
|
||||
else:
|
||||
match_results.goodMatches.append(filename)
|
||||
|
||||
elif opts.rename_file:
|
||||
|
||||
msg_hdr = ""
|
||||
if batch_mode:
|
||||
msg_hdr = u"{0}: ".format(filename)
|
||||
|
||||
if opts.data_style is not None:
|
||||
use_tags = has[ opts.data_style ]
|
||||
else:
|
||||
use_tags = False
|
||||
|
||||
md = create_local_metadata( opts, ca, use_tags )
|
||||
|
||||
if md.series is None:
|
||||
print >> sys.stderr, msg_hdr + "Can't rename without series name"
|
||||
return
|
||||
|
||||
new_ext = None # default
|
||||
if settings.rename_extension_based_on_archive:
|
||||
if ca.isZip():
|
||||
new_ext = ".cbz"
|
||||
elif ca.isRar():
|
||||
new_ext = ".cbr"
|
||||
|
||||
renamer = FileRenamer( md )
|
||||
renamer.setTemplate( settings.rename_template )
|
||||
renamer.setIssueZeroPadding( settings.rename_issue_number_padding )
|
||||
renamer.setSmartCleanup( settings.rename_use_smart_string_cleanup )
|
||||
|
||||
new_name = renamer.determineName( filename, ext=new_ext )
|
||||
|
||||
if new_name == os.path.basename(filename):
|
||||
print >> sys.stderr, msg_hdr + "Filename is already good!"
|
||||
return
|
||||
|
||||
folder = os.path.dirname( os.path.abspath( filename ) )
|
||||
new_abs_path = utils.unique_file( os.path.join( folder, new_name ) )
|
||||
|
||||
suffix = ""
|
||||
if not opts.dryrun:
|
||||
# rename the file
|
||||
os.rename( filename, new_abs_path )
|
||||
else:
|
||||
suffix = " (dry-run, no change)"
|
||||
|
||||
print u"renamed '{0}' -> '{1}' {2}".format(os.path.basename(filename), new_name, suffix)
|
||||
|
||||
elif opts.export_to_zip:
|
||||
msg_hdr = ""
|
||||
if batch_mode:
|
||||
msg_hdr = u"{0}: ".format(filename)
|
||||
|
||||
if not ca.isRar():
|
||||
print >> sys.stderr, msg_hdr + "Archive is not a RAR."
|
||||
return
|
||||
|
||||
rar_file = os.path.abspath( os.path.abspath( filename ) )
|
||||
new_file = os.path.splitext(rar_file)[0] + ".cbz"
|
||||
|
||||
if opts.abort_export_on_conflict and os.path.lexists( new_file ):
|
||||
print msg_hdr + "{0} already exists in the that folder.".format(os.path.split(new_file)[1])
|
||||
return
|
||||
|
||||
new_file = utils.unique_file( os.path.join( new_file ) )
|
||||
|
||||
delete_success = False
|
||||
export_success = False
|
||||
if not opts.dryrun:
|
||||
if ca.exportAsZip( new_file ):
|
||||
export_success = True
|
||||
if opts.delete_rar_after_export:
|
||||
try:
|
||||
os.unlink( rar_file )
|
||||
except:
|
||||
print >> sys.stderr, msg_hdr + "Error deleting original RAR after export"
|
||||
delete_success = False
|
||||
else:
|
||||
delete_success = True
|
||||
else:
|
||||
# last export failed, so remove the zip, if it exists
|
||||
if os.path.lexists( new_file ):
|
||||
os.remove( new_file )
|
||||
else:
|
||||
msg = msg_hdr + u"Dry-run: Would try to create {0}".format(os.path.split(new_file)[1])
|
||||
if opts.delete_rar_after_export:
|
||||
msg += u" and delete orginal."
|
||||
print msg
|
||||
return
|
||||
|
||||
msg = msg_hdr
|
||||
if export_success:
|
||||
msg += u"Archive exported successfully to: {0}".format( os.path.split(new_file)[1] )
|
||||
if opts.delete_rar_after_export and delete_success:
|
||||
msg += u" (Original deleted) "
|
||||
else:
|
||||
msg += u"Archive failed to export!"
|
||||
|
||||
print msg
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -25,6 +25,8 @@ import sys
|
||||
import tempfile
|
||||
import subprocess
|
||||
import platform
|
||||
import locale
|
||||
|
||||
if platform.system() == "Windows":
|
||||
import _subprocess
|
||||
import time
|
||||
@ -40,7 +42,6 @@ sys.path.insert(0, os.path.abspath(".") )
|
||||
import UnRAR2
|
||||
from UnRAR2.rar_exceptions import *
|
||||
|
||||
from options import Options, MetaDataStyle
|
||||
from comicinfoxml import ComicInfoXml
|
||||
from comicbookinfo import ComicBookInfo
|
||||
from comet import CoMet
|
||||
@ -48,6 +49,12 @@ from genericmetadata import GenericMetadata, PageType
|
||||
from filenameparser import FileNameParser
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
class MetaDataStyle:
|
||||
CBI = 0
|
||||
CIX = 1
|
||||
COMET = 2
|
||||
name = [ 'ComicBookLover', 'ComicRack', 'CoMet' ]
|
||||
|
||||
class ZipArchiver:
|
||||
|
||||
def __init__( self, path ):
|
||||
@ -68,12 +75,12 @@ class ZipArchiver:
|
||||
try:
|
||||
data = zf.read( archive_file )
|
||||
except zipfile.BadZipfile as e:
|
||||
print "bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file)
|
||||
print >> sys.stderr, "bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file)
|
||||
zf.close()
|
||||
raise IOError
|
||||
except Exception as e:
|
||||
zf.close()
|
||||
print "bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file)
|
||||
print >> sys.stderr, "bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file)
|
||||
raise IOError
|
||||
finally:
|
||||
zf.close()
|
||||
@ -112,7 +119,7 @@ class ZipArchiver:
|
||||
def rebuildZipFile( self, exclude_list ):
|
||||
|
||||
# this recompresses the zip archive, without the files in the exclude_list
|
||||
#print "Rebuilding zip {0} without {1}".format( self.path, exclude_list )
|
||||
#print ">> sys.stderr, Rebuilding zip {0} without {1}".format( self.path, exclude_list )
|
||||
|
||||
# generate temp file
|
||||
tmp_fd, tmp_name = tempfile.mkstemp( dir=os.path.dirname(self.path) )
|
||||
@ -215,7 +222,7 @@ class ZipArchiver:
|
||||
if not self.writeZipComment( self.path, comment ):
|
||||
return False
|
||||
except Exception as e:
|
||||
print "Error while copying to {0}: {1}".format(self.path, e)
|
||||
print >> sys.stderr, "Error while copying to {0}: {1}".format(self.path, e)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
@ -227,9 +234,9 @@ class ZipArchiver:
|
||||
class RarArchiver:
|
||||
|
||||
devnull = None
|
||||
def __init__( self, path ):
|
||||
def __init__( self, path, settings ):
|
||||
self.path = path
|
||||
self.rar_exe_path = None
|
||||
self.settings = settings
|
||||
|
||||
if RarArchiver.devnull is None:
|
||||
RarArchiver.devnull = open(os.devnull, "w")
|
||||
@ -252,7 +259,7 @@ class RarArchiver:
|
||||
|
||||
def setArchiveComment( self, comment ):
|
||||
|
||||
if self.rar_exe_path is not None:
|
||||
if self.settings.rar_exe_path is not None:
|
||||
try:
|
||||
# write comment to temp file
|
||||
tmp_fd, tmp_name = tempfile.mkstemp()
|
||||
@ -263,7 +270,7 @@ class RarArchiver:
|
||||
working_dir = os.path.dirname( os.path.abspath( self.path ) )
|
||||
|
||||
# use external program to write comment to Rar archive
|
||||
subprocess.call([self.rar_exe_path, 'c', '-w' + working_dir , '-c-', '-z' + tmp_name, self.path],
|
||||
subprocess.call([self.settings.rar_exe_path, 'c', '-w' + working_dir , '-c-', '-z' + tmp_name, self.path],
|
||||
startupinfo=self.startupinfo,
|
||||
stdout=RarArchiver.devnull)
|
||||
|
||||
@ -288,28 +295,28 @@ class RarArchiver:
|
||||
rarc = self.getRARObj()
|
||||
|
||||
tries = 0
|
||||
while tries < 10:
|
||||
while tries < 7:
|
||||
try:
|
||||
tries = tries+1
|
||||
entries = rarc.read_files( archive_file )
|
||||
|
||||
if entries[0][0].size != len(entries[0][1]):
|
||||
print "readArchiveFile(): [file is not expected size: {0} vs {1}] {2}:{3} [attempt # {4}]".format(
|
||||
print >> sys.stderr, "readArchiveFile(): [file is not expected size: {0} vs {1}] {2}:{3} [attempt # {4}]".format(
|
||||
entries[0][0].size,len(entries[0][1]), self.path, archive_file, tries)
|
||||
continue
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
print "readArchiveFile(): [{0}] {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries)
|
||||
print >> sys.stderr, "readArchiveFile(): [{0}] {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries)
|
||||
time.sleep(1)
|
||||
except Exception as e:
|
||||
print "Unexpected exception in readArchiveFile(): [{0}] for {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries)
|
||||
print >> sys.stderr, "Unexpected exception in readArchiveFile(): [{0}] for {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries)
|
||||
break
|
||||
|
||||
else:
|
||||
#Success"
|
||||
#entries is a list of of tuples: ( rarinfo, filedata)
|
||||
if tries > 1:
|
||||
print "Attempted read_files() {0} times".format(tries)
|
||||
print >> sys.stderr, "Attempted read_files() {0} times".format(tries)
|
||||
if (len(entries) == 1):
|
||||
return entries[0][1]
|
||||
else:
|
||||
@ -321,7 +328,7 @@ class RarArchiver:
|
||||
|
||||
def writeArchiveFile( self, archive_file, data ):
|
||||
|
||||
if self.rar_exe_path is not None:
|
||||
if self.settings.rar_exe_path is not None:
|
||||
try:
|
||||
tmp_folder = tempfile.mkdtemp()
|
||||
|
||||
@ -336,7 +343,7 @@ class RarArchiver:
|
||||
f.close()
|
||||
|
||||
# use external program to write file to Rar archive
|
||||
subprocess.call([self.rar_exe_path, 'a', '-w' + working_dir ,'-c-', '-ep', self.path, tmp_file],
|
||||
subprocess.call([self.settings.rar_exe_path, 'a', '-w' + working_dir ,'-c-', '-ep', self.path, tmp_file],
|
||||
startupinfo=self.startupinfo,
|
||||
stdout=RarArchiver.devnull)
|
||||
|
||||
@ -352,10 +359,10 @@ class RarArchiver:
|
||||
return False
|
||||
|
||||
def removeArchiveFile( self, archive_file ):
|
||||
if self.rar_exe_path is not None:
|
||||
if self.settings.rar_exe_path is not None:
|
||||
try:
|
||||
# use external program to remove file from Rar archive
|
||||
subprocess.call([self.rar_exe_path, 'd','-c-', self.path, archive_file],
|
||||
subprocess.call([self.settings.rar_exe_path, 'd','-c-', self.path, archive_file],
|
||||
startupinfo=self.startupinfo,
|
||||
stdout=RarArchiver.devnull)
|
||||
|
||||
@ -375,13 +382,17 @@ class RarArchiver:
|
||||
#return namelist
|
||||
|
||||
tries = 0
|
||||
while tries < 10:
|
||||
while tries < 7:
|
||||
try:
|
||||
tries = tries+1
|
||||
namelist = [ item.filename for item in rarc.infolist() ]
|
||||
#namelist = [ item.filename for item in rarc.infolist() ]
|
||||
namelist = []
|
||||
for item in rarc.infolist():
|
||||
if item.size != 0:
|
||||
namelist.append( item.filename )
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
print "getArchiveFilenameList(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries)
|
||||
print >> sys.stderr, "getArchiveFilenameList(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries)
|
||||
time.sleep(1)
|
||||
|
||||
else:
|
||||
@ -393,13 +404,13 @@ class RarArchiver:
|
||||
|
||||
def getRARObj( self ):
|
||||
tries = 0
|
||||
while tries < 10:
|
||||
while tries < 7:
|
||||
try:
|
||||
tries = tries+1
|
||||
rarc = UnRAR2.RarFile( self.path )
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
print "getRARObj(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries)
|
||||
print >> sys.stderr, "getRARObj(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries)
|
||||
time.sleep(1)
|
||||
|
||||
else:
|
||||
@ -499,11 +510,12 @@ class ComicArchive:
|
||||
class ArchiveType:
|
||||
Zip, Rar, Folder, Unknown = range(4)
|
||||
|
||||
def __init__( self, path ):
|
||||
def __init__( self, path, settings ):
|
||||
self.path = path
|
||||
self.ci_xml_filename = 'ComicInfo.xml'
|
||||
self.comet_default_filename = 'CoMet.xml'
|
||||
self.resetCache()
|
||||
self.settings = settings
|
||||
|
||||
if self.zipTest():
|
||||
self.archive_type = self.ArchiveType.Zip
|
||||
@ -511,7 +523,7 @@ class ComicArchive:
|
||||
|
||||
elif self.rarTest():
|
||||
self.archive_type = self.ArchiveType.Rar
|
||||
self.archiver = RarArchiver( self.path )
|
||||
self.archiver = RarArchiver( self.path, settings )
|
||||
|
||||
elif os.path.isdir( self.path ):
|
||||
self.archive_type = self.ArchiveType.Folder
|
||||
@ -521,10 +533,9 @@ class ComicArchive:
|
||||
self.archiver = UnknownArchiver( self.path )
|
||||
|
||||
if ComicArchive.logo_data is None:
|
||||
fname = os.path.join(ComicTaggerSettings.baseDir(), 'graphics','nocover.png' )
|
||||
fname = ComicTaggerSettings.getGraphic('nocover.png')
|
||||
with open(fname, 'rb') as fd:
|
||||
ComicArchive.logo_data = fd.read()
|
||||
print len(ComicArchive.logo_data)
|
||||
ComicArchive.logo_data = fd.read()
|
||||
|
||||
# Clears the cached data
|
||||
def resetCache( self ):
|
||||
@ -537,14 +548,14 @@ class ComicArchive:
|
||||
self.cix_md = None
|
||||
self.cbi_md = None
|
||||
self.comet_md = None
|
||||
|
||||
|
||||
def loadCache( self, style_list ):
|
||||
for style in style_list:
|
||||
self.readMetadata(style)
|
||||
|
||||
def rename( self, path ):
|
||||
self.path = path
|
||||
self.archiver.path = path
|
||||
|
||||
def setExternalRarProgram( self, rar_exe_path ):
|
||||
if self.isRar():
|
||||
self.archiver.rar_exe_path = rar_exe_path
|
||||
|
||||
def zipTest( self ):
|
||||
return zipfile.is_zipfile( self.path )
|
||||
@ -571,7 +582,7 @@ class ComicArchive:
|
||||
if self.archive_type == self.ArchiveType.Unknown :
|
||||
return False
|
||||
|
||||
elif check_rar_status and self.isRar() and self.archiver.rar_exe_path is None:
|
||||
elif check_rar_status and self.isRar() and self.settings.rar_exe_path is None:
|
||||
return False
|
||||
|
||||
elif not os.access(self.path, os.W_OK):
|
||||
@ -596,7 +607,7 @@ class ComicArchive:
|
||||
ext = os.path.splitext(self.path)[1].lower()
|
||||
|
||||
if (
|
||||
( self.isZip() or self.isRar() or self.isFolder() )
|
||||
( self.isZip() or self.isRar() ) #or self.isFolder() )
|
||||
and
|
||||
( self.getNumberOfPages() > 2)
|
||||
|
||||
@ -659,13 +670,16 @@ class ComicArchive:
|
||||
try:
|
||||
image_data = self.archiver.readArchiveFile( filename )
|
||||
except IOError:
|
||||
print "Error reading in page. Substituting logo page."
|
||||
print >> sys.stderr, "Error reading in page. Substituting logo page."
|
||||
image_data = ComicArchive.logo_data
|
||||
|
||||
return image_data
|
||||
|
||||
def getPageName( self, index ):
|
||||
|
||||
if index is None:
|
||||
return None
|
||||
|
||||
page_list = self.getPageNameList()
|
||||
|
||||
num_pages = len( page_list )
|
||||
@ -674,6 +688,56 @@ class ComicArchive:
|
||||
|
||||
return page_list[index]
|
||||
|
||||
def getScannerPageIndex( self ):
|
||||
|
||||
scanner_page_index = None
|
||||
|
||||
#make a guess at the scanner page
|
||||
name_list = self.getPageNameList()
|
||||
count = self.getNumberOfPages()
|
||||
|
||||
#too few pages to really know
|
||||
if count < 5:
|
||||
return None
|
||||
|
||||
# count the length of every filename, and count occurences
|
||||
length_buckets = dict()
|
||||
for name in name_list:
|
||||
fname = os.path.split(name)[1]
|
||||
length = len(fname)
|
||||
if length_buckets.has_key( length ):
|
||||
length_buckets[ length ] += 1
|
||||
else:
|
||||
length_buckets[ length ] = 1
|
||||
|
||||
# sort by most common
|
||||
sorted_buckets = sorted(length_buckets.iteritems(), key=lambda (k,v): (v,k), reverse=True)
|
||||
|
||||
# statistical mode occurence is first
|
||||
mode_length = sorted_buckets[0][0]
|
||||
|
||||
# we are only going to consider the final image file:
|
||||
final_name = os.path.split(name_list[count-1])[1]
|
||||
|
||||
common_length_list = list()
|
||||
for name in name_list:
|
||||
if len(os.path.split(name)[1]) == mode_length:
|
||||
common_length_list.append( os.path.split(name)[1] )
|
||||
|
||||
prefix = os.path.commonprefix(common_length_list)
|
||||
|
||||
if mode_length <= 7 and prefix == "":
|
||||
#probably all numbers
|
||||
if len(final_name) > mode_length:
|
||||
scanner_page_index = count-1
|
||||
|
||||
# see if the last page doesn't start with the same prefix as most others
|
||||
elif not final_name.startswith(prefix):
|
||||
scanner_page_index = count-1
|
||||
|
||||
return scanner_page_index
|
||||
|
||||
|
||||
def getPageNameList( self , sort_list=True):
|
||||
|
||||
if self.page_list is None:
|
||||
@ -682,12 +746,19 @@ class ComicArchive:
|
||||
|
||||
# seems like some archive creators are on Windows, and don't know about case-sensitivity!
|
||||
if sort_list:
|
||||
files.sort(key=lambda x: x.lower())
|
||||
def keyfunc(k):
|
||||
#hack to account for some weird scanner ID pages
|
||||
basename=os.path.split(k)[1]
|
||||
if basename < '0':
|
||||
k = os.path.join(os.path.split(k)[0], "z" + basename)
|
||||
return k.lower()
|
||||
|
||||
files.sort(key=keyfunc)
|
||||
|
||||
# make a sub-list of image files
|
||||
self.page_list = []
|
||||
for name in files:
|
||||
if ( name[-4:].lower() in [ ".jpg", "jpeg", ".png" ] and os.path.basename(name)[0] != "." ):
|
||||
if ( name[-4:].lower() in [ ".jpg", "jpeg", ".png", ".gif" ] and os.path.basename(name)[0] != "." ):
|
||||
self.page_list.append(name)
|
||||
|
||||
return self.page_list
|
||||
@ -736,8 +807,7 @@ class ComicArchive:
|
||||
if write_success:
|
||||
self.has_cbi = True
|
||||
self.cbi_md = metadata
|
||||
else:
|
||||
self.resetCache()
|
||||
self.resetCache()
|
||||
return write_success
|
||||
else:
|
||||
return False
|
||||
@ -748,8 +818,7 @@ class ComicArchive:
|
||||
if write_success:
|
||||
self.has_cbi = False
|
||||
self.cbi_md = None
|
||||
else:
|
||||
self.resetCache()
|
||||
self.resetCache()
|
||||
return write_success
|
||||
return True
|
||||
|
||||
@ -792,8 +861,7 @@ class ComicArchive:
|
||||
if write_success:
|
||||
self.has_cix = True
|
||||
self.cix_md = metadata
|
||||
else:
|
||||
self.resetCache()
|
||||
self.resetCache()
|
||||
return write_success
|
||||
else:
|
||||
return False
|
||||
@ -804,8 +872,7 @@ class ComicArchive:
|
||||
if write_success:
|
||||
self.has_cix = False
|
||||
self.cix_md = None
|
||||
else:
|
||||
self.resetCache()
|
||||
self.resetCache()
|
||||
return write_success
|
||||
return True
|
||||
|
||||
@ -848,13 +915,13 @@ class ComicArchive:
|
||||
|
||||
def readRawCoMet( self ):
|
||||
if not self.hasCoMet():
|
||||
print self.path, "doesn't have CoMet data!"
|
||||
print >> sys.stderr, self.path, "doesn't have CoMet data!"
|
||||
return None
|
||||
|
||||
try:
|
||||
raw_comet = self.archiver.readArchiveFile( self.comet_filename )
|
||||
except IOError:
|
||||
print "Error reading in raw CoMet!"
|
||||
print >> sys.stderr, "Error reading in raw CoMet!"
|
||||
raw_comet = ""
|
||||
return raw_comet
|
||||
|
||||
@ -875,8 +942,7 @@ class ComicArchive:
|
||||
if write_success:
|
||||
self.has_comet = True
|
||||
self.comet_md = metadata
|
||||
else:
|
||||
self.resetCache()
|
||||
self.resetCache()
|
||||
return write_success
|
||||
else:
|
||||
return False
|
||||
@ -887,8 +953,7 @@ class ComicArchive:
|
||||
if write_success:
|
||||
self.has_comet = False
|
||||
self.comet_md = None
|
||||
else:
|
||||
self.resetCache()
|
||||
self.resetCache()
|
||||
return write_success
|
||||
return True
|
||||
|
||||
@ -907,7 +972,7 @@ class ComicArchive:
|
||||
data = self.archiver.readArchiveFile( n )
|
||||
except:
|
||||
data = ""
|
||||
print "Error reading in Comet XML for validation!"
|
||||
print >> sys.stderr, "Error reading in Comet XML for validation!"
|
||||
if CoMet().validateString( data ):
|
||||
# since we found it, save it!
|
||||
self.comet_filename = n
|
||||
@ -962,6 +1027,9 @@ class ComicArchive:
|
||||
metadata.year = fnp.year
|
||||
if fnp.issue_count != "":
|
||||
metadata.issueCount = fnp.issue_count
|
||||
if self.settings.parse_scan_info:
|
||||
if fnp.remainder != "":
|
||||
metadata.scanInfo = fnp.remainder
|
||||
|
||||
metadata.isEmpty = False
|
||||
|
@ -115,7 +115,7 @@ class ComicBookInfo:
|
||||
#helper func
|
||||
def toInt(s):
|
||||
i = None
|
||||
if type(s) == str or type(s) == int:
|
||||
if type(s) in [ str, unicode, int ]:
|
||||
try:
|
||||
i = int(s)
|
||||
except ValueError:
|
@ -89,36 +89,21 @@ class ComicInfoXml:
|
||||
if md_entry is not None:
|
||||
ET.SubElement(root, cix_entry).text = u"{0}".format(md_entry)
|
||||
|
||||
assign( 'Title', md.title )
|
||||
assign( 'Series', md.series )
|
||||
assign( 'Number', md.issue )
|
||||
assign( 'Title', md.title )
|
||||
assign( 'Count', md.issueCount )
|
||||
assign( 'Volume', md.volume )
|
||||
assign( 'AlternateSeries', md.alternateSeries )
|
||||
assign( 'AlternateNumber', md.alternateNumber )
|
||||
assign( 'StoryArc', md.storyArc )
|
||||
assign( 'SeriesGroup', md.seriesGroup )
|
||||
assign( 'AlternateCount', md.alternateCount )
|
||||
assign( 'Summary', md.comments )
|
||||
assign( 'Notes', md.notes )
|
||||
assign( 'Year', md.year )
|
||||
assign( 'Month', md.month )
|
||||
assign( 'Publisher', md.publisher )
|
||||
assign( 'Imprint', md.imprint )
|
||||
assign( 'Genre', md.genre )
|
||||
assign( 'Web', md.webLink )
|
||||
assign( 'PageCount', md.pageCount )
|
||||
assign( 'Format', md.format )
|
||||
assign( 'LanguageISO', md.language )
|
||||
assign( 'Manga', md.manga )
|
||||
assign( 'Characters', md.characters )
|
||||
assign( 'Teams', md.teams )
|
||||
assign( 'Locations', md.locations )
|
||||
assign( 'ScanInformation', md.scanInfo )
|
||||
assign( 'StoryArc', md.storyArc )
|
||||
assign( 'SeriesGroup', md.seriesGroup )
|
||||
assign( 'AgeRating', md.maturityRating )
|
||||
|
||||
if md.blackAndWhite is not None and md.blackAndWhite:
|
||||
ET.SubElement(root, 'BlackAndWhite').text = "Yes"
|
||||
assign( 'Day', md.day )
|
||||
|
||||
# need to specially process the credits, since they are structured differently than CIX
|
||||
credit_writer_list = list()
|
||||
@ -181,7 +166,23 @@ class ComicInfoXml:
|
||||
if len( credit_editor_list ) > 0:
|
||||
node = ET.SubElement(root, 'Editor')
|
||||
node.text = utils.listToString( credit_editor_list )
|
||||
|
||||
|
||||
assign( 'Publisher', md.publisher )
|
||||
assign( 'Imprint', md.imprint )
|
||||
assign( 'Genre', md.genre )
|
||||
assign( 'Web', md.webLink )
|
||||
assign( 'PageCount', md.pageCount )
|
||||
assign( 'LanguageISO', md.language )
|
||||
assign( 'Format', md.format )
|
||||
assign( 'AgeRating', md.maturityRating )
|
||||
if md.blackAndWhite is not None and md.blackAndWhite:
|
||||
ET.SubElement(root, 'BlackAndWhite').text = "Yes"
|
||||
assign( 'Manga', md.manga )
|
||||
assign( 'Characters', md.characters )
|
||||
assign( 'Teams', md.teams )
|
||||
assign( 'Locations', md.locations )
|
||||
assign( 'ScanInformation', md.scanInfo )
|
||||
|
||||
# loop and add the page entries under pages node
|
||||
if len( md.pages ) > 0:
|
||||
pages_node = ET.SubElement(root, 'Pages')
|
||||
@ -229,6 +230,7 @@ class ComicInfoXml:
|
||||
md.notes = xlate( 'Notes' )
|
||||
md.year = xlate( 'Year' )
|
||||
md.month = xlate( 'Month' )
|
||||
md.day = xlate( 'Day' )
|
||||
md.publisher = xlate( 'Publisher' )
|
||||
md.imprint = xlate( 'Imprint' )
|
||||
md.genre = xlate( 'Genre' )
|
@ -27,6 +27,7 @@ import datetime
|
||||
|
||||
import ctversion
|
||||
from settings import ComicTaggerSettings
|
||||
import utils
|
||||
|
||||
class ComicVineCacher:
|
||||
|
||||
@ -97,17 +98,23 @@ class ComicVineCacher:
|
||||
"PRIMARY KEY (id) )"
|
||||
)
|
||||
|
||||
cur.execute("CREATE TABLE AltCovers(" +
|
||||
"issue_id INT," +
|
||||
"url_list TEXT," +
|
||||
"timestamp DATE DEFAULT (datetime('now','localtime')), " +
|
||||
"PRIMARY KEY (issue_id) )"
|
||||
)
|
||||
|
||||
cur.execute("CREATE TABLE Issues(" +
|
||||
"id INT," +
|
||||
"volume_id INT," +
|
||||
"name TEXT," +
|
||||
"issue_number TEXT," +
|
||||
"image_url TEXT," +
|
||||
"image_hash TEXT," +
|
||||
"thumb_image_url TEXT," +
|
||||
"thumb_image_hash TEXT," +
|
||||
"publish_month TEXT," +
|
||||
"publish_year TEXT," +
|
||||
"super_url TEXT," +
|
||||
"thumb_url TEXT," +
|
||||
"cover_date TEXT," +
|
||||
"site_detail_url TEXT," +
|
||||
"description TEXT," +
|
||||
"timestamp DATE DEFAULT (datetime('now','localtime')), " +
|
||||
"PRIMARY KEY (id ) )"
|
||||
)
|
||||
@ -149,7 +156,7 @@ class ComicVineCacher:
|
||||
url,
|
||||
record['description'])
|
||||
)
|
||||
|
||||
|
||||
def get_search_results( self, search_term ):
|
||||
|
||||
results = list()
|
||||
@ -184,7 +191,52 @@ class ComicVineCacher:
|
||||
|
||||
return results
|
||||
|
||||
def add_alt_covers( self, issue_id, url_list ):
|
||||
|
||||
con = lite.connect( self.db_file )
|
||||
|
||||
with con:
|
||||
con.text_factory = unicode
|
||||
cur = con.cursor()
|
||||
|
||||
# remove all previous entries with this search term
|
||||
cur.execute("DELETE FROM AltCovers WHERE issue_id = ?", [ issue_id ])
|
||||
|
||||
url_list_str = utils.listToString(url_list)
|
||||
# now add in new record
|
||||
cur.execute("INSERT INTO AltCovers " +
|
||||
"(issue_id, url_list ) " +
|
||||
"VALUES( ?, ? )" ,
|
||||
( issue_id,
|
||||
url_list_str)
|
||||
)
|
||||
|
||||
|
||||
def get_alt_covers( self, issue_id ):
|
||||
|
||||
con = lite.connect( self.db_file )
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
con.text_factory = unicode
|
||||
|
||||
# purge stale issue info - probably issue data won't change much....
|
||||
a_month_ago = datetime.datetime.today()-datetime.timedelta(days=30)
|
||||
cur.execute( "DELETE FROM AltCovers WHERE timestamp < ?", [ str(a_month_ago) ] )
|
||||
|
||||
cur.execute("SELECT url_list FROM AltCovers WHERE issue_id=?", [ issue_id ])
|
||||
row = cur.fetchone()
|
||||
if row is None :
|
||||
return None
|
||||
else:
|
||||
url_list_str = row[0]
|
||||
if len(url_list_str) == 0:
|
||||
return []
|
||||
raw_list = url_list_str.split(",")
|
||||
url_list = []
|
||||
for item in raw_list:
|
||||
url_list.append( str(item).strip())
|
||||
return url_list
|
||||
|
||||
def add_volume_info( self, cv_volume_record ):
|
||||
|
||||
con = lite.connect( self.db_file )
|
||||
@ -209,18 +261,34 @@ class ComicVineCacher:
|
||||
}
|
||||
self.upsert( cur, "volumes", "id", cv_volume_record['id'], data)
|
||||
|
||||
# now add in issues
|
||||
|
||||
for issue in cv_volume_record['issues']:
|
||||
def add_volume_issues_info( self, volume_id, cv_volume_issues ):
|
||||
|
||||
con = lite.connect( self.db_file )
|
||||
|
||||
with con:
|
||||
|
||||
cur = con.cursor()
|
||||
|
||||
timestamp = datetime.datetime.now()
|
||||
|
||||
# add in issues
|
||||
|
||||
for issue in cv_volume_issues:
|
||||
|
||||
data = {
|
||||
"volume_id": cv_volume_record['id'],
|
||||
"name": issue['name'],
|
||||
"issue_number": issue['issue_number'],
|
||||
"timestamp": timestamp
|
||||
"volume_id": volume_id,
|
||||
"name": issue['name'],
|
||||
"issue_number": issue['issue_number'],
|
||||
"site_detail_url": issue['site_detail_url'],
|
||||
"cover_date": issue['cover_date'],
|
||||
"super_url": issue['image']['super_url'],
|
||||
"thumb_url": issue['image']['thumb_url'],
|
||||
"description": issue['description'],
|
||||
"timestamp": timestamp
|
||||
}
|
||||
self.upsert( cur, "issues" , "id", issue['id'], data)
|
||||
|
||||
|
||||
|
||||
def get_volume_info( self, volume_id ):
|
||||
|
||||
@ -234,10 +302,6 @@ class ComicVineCacher:
|
||||
# purge stale volume info
|
||||
a_week_ago = datetime.datetime.today()-datetime.timedelta(days=7)
|
||||
cur.execute( "DELETE FROM Volumes WHERE timestamp < ?", [ str(a_week_ago) ] )
|
||||
|
||||
# purge stale issue info - probably issue data won't change much....
|
||||
a_month_ago = datetime.datetime.today()-datetime.timedelta(days=30)
|
||||
cur.execute( "DELETE FROM Issues WHERE timestamp < ?", [ str(a_month_ago) ] )
|
||||
|
||||
# fetch
|
||||
cur.execute("SELECT id,name,publisher,count_of_issues,start_year FROM Volumes WHERE id = ?", [ volume_id ] )
|
||||
@ -257,25 +321,51 @@ class ComicVineCacher:
|
||||
result['count_of_issues'] = row[3]
|
||||
result['start_year'] = row[4]
|
||||
result['issues'] = list()
|
||||
|
||||
return result
|
||||
|
||||
cur.execute("SELECT id,name,issue_number,image_url,image_hash FROM Issues WHERE volume_id = ?", [ volume_id ] )
|
||||
def get_volume_issues_info( self, volume_id ):
|
||||
|
||||
result = None
|
||||
|
||||
con = lite.connect( self.db_file )
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
con.text_factory = unicode
|
||||
|
||||
# purge stale issue info - probably issue data won't change much....
|
||||
a_week_ago = datetime.datetime.today()-datetime.timedelta(days=7)
|
||||
cur.execute( "DELETE FROM Issues WHERE timestamp < ?", [ str(a_week_ago) ] )
|
||||
|
||||
# fetch
|
||||
results = list()
|
||||
|
||||
cur.execute("SELECT id,name,issue_number,site_detail_url,cover_date,super_url,thumb_url,description FROM Issues WHERE volume_id = ?", [ volume_id ] )
|
||||
rows = cur.fetchall()
|
||||
|
||||
# now process the results
|
||||
for row in rows:
|
||||
record = dict()
|
||||
record['id'] = row[0]
|
||||
record['name'] = row[1]
|
||||
record['issue_number'] = row[2]
|
||||
record['image_url'] = row[3]
|
||||
record['image_hash'] = row[4]
|
||||
|
||||
result['issues'].append(record)
|
||||
record['id'] = row[0]
|
||||
record['name'] = row[1]
|
||||
record['issue_number'] = row[2]
|
||||
record['site_detail_url'] = row[3]
|
||||
record['cover_date'] = row[4]
|
||||
record['image'] = dict()
|
||||
record['image']['super_url'] = row[5]
|
||||
record['image']['thumb_url'] = row[6]
|
||||
record['description'] = row[7]
|
||||
|
||||
results.append(record)
|
||||
|
||||
return result
|
||||
if len(results) == 0:
|
||||
return None
|
||||
|
||||
return results
|
||||
|
||||
|
||||
|
||||
def add_issue_select_details( self, issue_id, image_url, thumb_image_url, publish_month, publish_year ):
|
||||
def add_issue_select_details( self, issue_id, image_url, thumb_image_url, cover_date, site_detail_url ):
|
||||
|
||||
con = lite.connect( self.db_file )
|
||||
|
||||
@ -285,10 +375,10 @@ class ComicVineCacher:
|
||||
timestamp = datetime.datetime.now()
|
||||
|
||||
data = {
|
||||
"image_url": image_url,
|
||||
"thumb_image_url": thumb_image_url,
|
||||
"publish_month": publish_month,
|
||||
"publish_year": publish_year,
|
||||
"super_url": image_url,
|
||||
"thumb_url": thumb_image_url,
|
||||
"cover_date": cover_date,
|
||||
"site_detail_url": site_detail_url,
|
||||
"timestamp": timestamp
|
||||
}
|
||||
self.upsert( cur, "issues" , "id", issue_id, data)
|
||||
@ -302,13 +392,23 @@ class ComicVineCacher:
|
||||
cur = con.cursor()
|
||||
con.text_factory = unicode
|
||||
|
||||
cur.execute("SELECT image_url,thumb_image_url,publish_month,publish_year FROM Issues WHERE id=?", [ issue_id ])
|
||||
cur.execute("SELECT super_url,thumb_url,cover_date,site_detail_url FROM Issues WHERE id=?", [ issue_id ])
|
||||
row = cur.fetchone()
|
||||
|
||||
if row[0] is None :
|
||||
return None, None, None, None
|
||||
details = dict()
|
||||
if row is None or row[0] is None :
|
||||
details['image_url'] = None
|
||||
details['thumb_image_url'] = None
|
||||
details['cover_date'] = None
|
||||
details['site_detail_url'] = None
|
||||
|
||||
else:
|
||||
return row[0],row[1],row[2],row[3]
|
||||
details['image_url'] = row[0]
|
||||
details['thumb_image_url'] = row[1]
|
||||
details['cover_date'] = row[2]
|
||||
details['site_detail_url'] = row[3]
|
||||
|
||||
return details
|
||||
|
||||
|
||||
def upsert( self, cur, tablename, pkname, pkval, data):
|
639
comictaggerlib/comicvinetalker.py
Normal file
@ -0,0 +1,639 @@
|
||||
"""
|
||||
A python class to manage communication with Comic Vine's REST API
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
|
||||
import json
|
||||
from pprint import pprint
|
||||
import urllib2, urllib
|
||||
import math
|
||||
import re
|
||||
import datetime
|
||||
import ctversion
|
||||
import sys
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
try:
|
||||
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
||||
from PyQt4.QtCore import QUrl, pyqtSignal, QObject, QByteArray
|
||||
except ImportError:
|
||||
# No Qt, so define a few dummy QObjects to help us compile
|
||||
class QObject():
|
||||
def __init__(self,*args):
|
||||
pass
|
||||
class pyqtSignal():
|
||||
def __init__(self,*args):
|
||||
pass
|
||||
def emit(a,b,c):
|
||||
pass
|
||||
|
||||
import utils
|
||||
from settings import ComicTaggerSettings
|
||||
from comicvinecacher import ComicVineCacher
|
||||
from genericmetadata import GenericMetadata
|
||||
from issuestring import IssueString
|
||||
|
||||
class CVTypeID:
|
||||
Volume = "4050"
|
||||
Issue = "4000"
|
||||
|
||||
class ComicVineTalkerException(Exception):
|
||||
pass
|
||||
|
||||
class ComicVineTalker(QObject):
|
||||
|
||||
logo_url = "http://static.comicvine.com/bundles/comicvinesite/images/logo.png"
|
||||
|
||||
def __init__(self, api_key=""):
|
||||
QObject.__init__(self)
|
||||
|
||||
self.api_base_url = "http://www.comicvine.com/api"
|
||||
|
||||
# key that is registered to comictagger
|
||||
self.api_key = '27431e6787042105bd3e47e169a624521f89f3a4'
|
||||
|
||||
self.log_func = None
|
||||
|
||||
def setLogFunc( self , log_func ):
|
||||
self.log_func = log_func
|
||||
|
||||
def writeLog( self , text ):
|
||||
if self.log_func is None:
|
||||
#sys.stdout.write(text.encode( errors='replace') )
|
||||
#sys.stdout.flush()
|
||||
print >> sys.stderr, text
|
||||
else:
|
||||
self.log_func( text )
|
||||
|
||||
def parseDateStr( self, date_str):
|
||||
day = None
|
||||
month = None
|
||||
year = None
|
||||
if date_str is not None:
|
||||
parts = date_str.split('-')
|
||||
year = parts[0]
|
||||
if len(parts) > 1:
|
||||
month = parts[1]
|
||||
if len(parts) > 2:
|
||||
day = parts[2]
|
||||
return day, month, year
|
||||
|
||||
def testKey( self ):
|
||||
|
||||
test_url = self.api_base_url + "/issue/1/?api_key=" + self.api_key + "&format=json&field_list=name"
|
||||
resp = urllib2.urlopen( test_url )
|
||||
content = resp.read()
|
||||
|
||||
cv_response = json.loads( content )
|
||||
|
||||
# Bogus request, but if the key is wrong, you get error 100: "Invalid API Key"
|
||||
return cv_response[ 'status_code' ] != 100
|
||||
|
||||
def getUrlContent( self, url ):
|
||||
try:
|
||||
resp = urllib2.urlopen( url )
|
||||
return resp.read()
|
||||
except Exception as e:
|
||||
self.writeLog( str(e) )
|
||||
raise ComicVineTalkerException("Network Error!")
|
||||
|
||||
def searchForSeries( self, series_name , callback=None, refresh_cache=False ):
|
||||
|
||||
# remove cruft from the search string
|
||||
series_name = utils.removearticles( series_name ).lower().strip()
|
||||
|
||||
# before we search online, look in our cache, since we might have
|
||||
# done this same search recently
|
||||
cvc = ComicVineCacher( )
|
||||
if not refresh_cache:
|
||||
cached_search_results = cvc.get_search_results( series_name )
|
||||
|
||||
if len (cached_search_results) > 0:
|
||||
return cached_search_results
|
||||
|
||||
original_series_name = series_name
|
||||
|
||||
series_name = urllib.quote_plus(series_name.encode("utf-8"))
|
||||
#series_name = urllib.quote_plus(unicode(series_name))
|
||||
search_url = self.api_base_url + "/search/?api_key=" + self.api_key + "&format=json&resources=volume&query=" + series_name + "&field_list=name,id,start_year,publisher,image,description,count_of_issues"
|
||||
content = self.getUrlContent(search_url)
|
||||
|
||||
cv_response = json.loads(content)
|
||||
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
self.writeLog( "Comic Vine query failed with error: [{0}]. \n".format( cv_response[ 'error' ] ))
|
||||
return None
|
||||
|
||||
search_results = list()
|
||||
|
||||
# see http://api.comicvine.com/documentation/#handling_responses
|
||||
|
||||
limit = cv_response['limit']
|
||||
current_result_count = cv_response['number_of_page_results']
|
||||
total_result_count = cv_response['number_of_total_results']
|
||||
|
||||
if callback is None:
|
||||
self.writeLog( "Found {0} of {1} results\n".format( cv_response['number_of_page_results'], cv_response['number_of_total_results']))
|
||||
search_results.extend( cv_response['results'])
|
||||
page = 1
|
||||
|
||||
if callback is not None:
|
||||
callback( current_result_count, total_result_count )
|
||||
|
||||
# see if we need to keep asking for more pages...
|
||||
while ( current_result_count < total_result_count ):
|
||||
if callback is None:
|
||||
self.writeLog("getting another page of results {0} of {1}...\n".format( current_result_count, total_result_count))
|
||||
page += 1
|
||||
|
||||
content = self.getUrlContent(search_url + "&page="+str(page))
|
||||
|
||||
cv_response = json.loads(content)
|
||||
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
self.writeLog( "Comic Vine query failed with error: [{0}]. \n".format( cv_response[ 'error' ] ))
|
||||
return None
|
||||
search_results.extend( cv_response['results'])
|
||||
current_result_count += cv_response['number_of_page_results']
|
||||
|
||||
if callback is not None:
|
||||
callback( current_result_count, total_result_count )
|
||||
|
||||
|
||||
#for record in search_results:
|
||||
# #print( u"{0}: {1} ({2})".format(record['id'], record['name'] , record['start_year'] ) )
|
||||
# #print record
|
||||
# #record['count_of_issues'] = record['count_of_isssues']
|
||||
#print u"{0}: {1} ({2})".format(search_results['results'][0]['id'], search_results['results'][0]['name'] , search_results['results'][0]['start_year'] )
|
||||
|
||||
# cache these search results
|
||||
cvc.add_search_results( original_series_name, search_results )
|
||||
|
||||
return search_results
|
||||
|
||||
def fetchVolumeData( self, series_id ):
|
||||
|
||||
# before we search online, look in our cache, since we might already
|
||||
# have this info
|
||||
cvc = ComicVineCacher( )
|
||||
cached_volume_result = cvc.get_volume_info( series_id )
|
||||
|
||||
if cached_volume_result is not None:
|
||||
return cached_volume_result
|
||||
|
||||
|
||||
volume_url = self.api_base_url + "/volume/" + CVTypeID.Volume + "-" + str(series_id) + "/?api_key=" + self.api_key + "&field_list=name,id,start_year,publisher,count_of_issues&format=json"
|
||||
|
||||
content = self.getUrlContent(volume_url)
|
||||
cv_response = json.loads(content)
|
||||
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] )
|
||||
return None
|
||||
|
||||
volume_results = cv_response['results']
|
||||
|
||||
cvc.add_volume_info( volume_results )
|
||||
|
||||
return volume_results
|
||||
|
||||
def fetchIssuesByVolume( self, series_id ):
|
||||
|
||||
# before we search online, look in our cache, since we might already
|
||||
# have this info
|
||||
cvc = ComicVineCacher( )
|
||||
cached_volume_issues_result = cvc.get_volume_issues_info( series_id )
|
||||
|
||||
if cached_volume_issues_result is not None:
|
||||
return cached_volume_issues_result
|
||||
|
||||
#---------------------------------
|
||||
issues_url = self.api_base_url + "/issues/" + "?api_key=" + self.api_key + "&filter=volume:" + str(series_id) + "&field_list=id,volume,issue_number,name,image,cover_date,site_detail_url,description&format=json"
|
||||
content = self.getUrlContent(issues_url)
|
||||
cv_response = json.loads(content)
|
||||
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] )
|
||||
return None
|
||||
#------------------------------------
|
||||
|
||||
limit = cv_response['limit']
|
||||
current_result_count = cv_response['number_of_page_results']
|
||||
total_result_count = cv_response['number_of_total_results']
|
||||
#print "ATB total_result_count", total_result_count
|
||||
|
||||
#print "ATB Found {0} of {1} results".format( cv_response['number_of_page_results'], cv_response['number_of_total_results'])
|
||||
volume_issues_result = cv_response['results']
|
||||
page = 1
|
||||
offset = 0
|
||||
|
||||
# see if we need to keep asking for more pages...
|
||||
while ( current_result_count < total_result_count ):
|
||||
#print "ATB getting another page of issue results {0} of {1}...".format( current_result_count, total_result_count)
|
||||
page += 1
|
||||
offset += cv_response['number_of_page_results']
|
||||
|
||||
#print issues_url+ "&offset="+str(offset)
|
||||
content = self.getUrlContent(issues_url + "&offset="+str(offset))
|
||||
cv_response = json.loads(content)
|
||||
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
self.writeLog( "Comic Vine query failed with error: [{0}]. \n".format( cv_response[ 'error' ] ))
|
||||
return None
|
||||
volume_issues_result.extend( cv_response['results'])
|
||||
current_result_count += cv_response['number_of_page_results']
|
||||
|
||||
self.repairUrls( volume_issues_result )
|
||||
|
||||
cvc.add_volume_issues_info( series_id, volume_issues_result )
|
||||
|
||||
return volume_issues_result
|
||||
|
||||
|
||||
def fetchIssuesByVolumeIssueNumAndYear( self, volume_id_list, issue_number, year ):
|
||||
volume_filter = "volume:"
|
||||
for vid in volume_id_list:
|
||||
volume_filter += str(vid) + "|"
|
||||
|
||||
year_filter = ""
|
||||
if year is not None and str(year).isdigit():
|
||||
year_filter = ",cover_date:{0}-1-1|{1}-1-1".format(year, int(year)+1)
|
||||
|
||||
issue_number = urllib.quote_plus(unicode(issue_number).encode("utf-8"))
|
||||
|
||||
filter = "&filter=" + volume_filter + year_filter + ",issue_number:" + issue_number
|
||||
|
||||
issues_url = self.api_base_url + "/issues/" + "?api_key=" + self.api_key + filter + "&field_list=id,volume,issue_number,name,image,cover_date,site_detail_url,description&format=json"
|
||||
|
||||
content = self.getUrlContent(issues_url)
|
||||
cv_response = json.loads(content)
|
||||
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] )
|
||||
return None
|
||||
#------------------------------------
|
||||
|
||||
limit = cv_response['limit']
|
||||
current_result_count = cv_response['number_of_page_results']
|
||||
total_result_count = cv_response['number_of_total_results']
|
||||
#print "ATB total_result_count", total_result_count
|
||||
|
||||
#print "ATB Found {0} of {1} results\n".format( cv_response['number_of_page_results'], cv_response['number_of_total_results'])
|
||||
filtered_issues_result = cv_response['results']
|
||||
page = 1
|
||||
offset = 0
|
||||
|
||||
# see if we need to keep asking for more pages...
|
||||
while ( current_result_count < total_result_count ):
|
||||
#print "ATB getting another page of issue results {0} of {1}...\n".format( current_result_count, total_result_count)
|
||||
page += 1
|
||||
offset += cv_response['number_of_page_results']
|
||||
|
||||
#print issues_url+ "&offset="+str(offset)
|
||||
content = self.getUrlContent(issues_url + "&offset="+str(offset))
|
||||
cv_response = json.loads(content)
|
||||
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
self.writeLog( "Comic Vine query failed with error: [{0}]. \n".format( cv_response[ 'error' ] ))
|
||||
return None
|
||||
filtered_issues_result.extend( cv_response['results'])
|
||||
current_result_count += cv_response['number_of_page_results']
|
||||
|
||||
self.repairUrls( filtered_issues_result )
|
||||
|
||||
return filtered_issues_result
|
||||
|
||||
|
||||
|
||||
def fetchIssueData( self, series_id, issue_number, settings ):
|
||||
|
||||
volume_results = self.fetchVolumeData( series_id )
|
||||
issues_list_results = self.fetchIssuesByVolume( series_id )
|
||||
|
||||
found = False
|
||||
for record in issues_list_results:
|
||||
if IssueString(issue_number).asString() is None:
|
||||
issue_number = 1
|
||||
if IssueString(record['issue_number']).asString().lower() == IssueString(issue_number).asString().lower():
|
||||
found = True
|
||||
break
|
||||
|
||||
if (found):
|
||||
issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + str(record['id']) + "/?api_key=" + self.api_key + "&format=json"
|
||||
|
||||
content = self.getUrlContent(issue_url)
|
||||
cv_response = json.loads(content)
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] )
|
||||
return None
|
||||
issue_results = cv_response['results']
|
||||
|
||||
else:
|
||||
return None
|
||||
|
||||
# now, map the comicvine data to generic metadata
|
||||
return self.mapCVDataToMetadata( volume_results, issue_results, settings )
|
||||
|
||||
def fetchIssueDataByIssueID( self, issue_id, settings ):
|
||||
|
||||
issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + str(issue_id) + "/?api_key=" + self.api_key + "&format=json"
|
||||
content = self.getUrlContent(issue_url)
|
||||
cv_response = json.loads(content)
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] )
|
||||
return None
|
||||
|
||||
issue_results = cv_response['results']
|
||||
|
||||
volume_results = self.fetchVolumeData( issue_results['volume']['id'] )
|
||||
|
||||
# now, map the comicvine data to generic metadata
|
||||
md = self.mapCVDataToMetadata( volume_results, issue_results, settings )
|
||||
md.isEmpty = False
|
||||
return md
|
||||
|
||||
def mapCVDataToMetadata(self, volume_results, issue_results, settings ):
|
||||
|
||||
# now, map the comicvine data to generic metadata
|
||||
metadata = GenericMetadata()
|
||||
|
||||
metadata.series = issue_results['volume']['name']
|
||||
|
||||
num_s = IssueString(issue_results['issue_number']).asString()
|
||||
metadata.issue = num_s
|
||||
metadata.title = issue_results['name']
|
||||
|
||||
metadata.publisher = volume_results['publisher']['name']
|
||||
metadata.day, metadata.month, metadata.year = self.parseDateStr( issue_results['cover_date'] )
|
||||
|
||||
#metadata.issueCount = volume_results['count_of_issues']
|
||||
metadata.comments = self.cleanup_html(issue_results['description'])
|
||||
if settings.use_series_start_as_volume:
|
||||
metadata.volume = volume_results['start_year']
|
||||
|
||||
metadata.notes = "Tagged with ComicTagger {0} using info from Comic Vine on {1}. [Issue ID {2}]".format(
|
||||
ctversion.version,
|
||||
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
issue_results['id'])
|
||||
#metadata.notes += issue_results['site_detail_url']
|
||||
|
||||
metadata.webLink = issue_results['site_detail_url']
|
||||
|
||||
person_credits = issue_results['person_credits']
|
||||
for person in person_credits:
|
||||
if person.has_key('role'):
|
||||
roles = person['role'].split(',')
|
||||
for role in roles:
|
||||
# can we determine 'primary' from CV??
|
||||
metadata.addCredit( person['name'], role.title().strip(), False )
|
||||
|
||||
character_credits = issue_results['character_credits']
|
||||
character_list = list()
|
||||
for character in character_credits:
|
||||
character_list.append( character['name'] )
|
||||
metadata.characters = utils.listToString( character_list )
|
||||
|
||||
team_credits = issue_results['team_credits']
|
||||
team_list = list()
|
||||
for team in team_credits:
|
||||
team_list.append( team['name'] )
|
||||
metadata.teams = utils.listToString( team_list )
|
||||
|
||||
location_credits = issue_results['location_credits']
|
||||
location_list = list()
|
||||
for location in location_credits:
|
||||
location_list.append( location['name'] )
|
||||
metadata.locations = utils.listToString( location_list )
|
||||
|
||||
story_arc_credits = issue_results['story_arc_credits']
|
||||
arc_list = []
|
||||
for arc in story_arc_credits:
|
||||
arc_list.append(arc['name'])
|
||||
if len(arc_list) > 0:
|
||||
metadata.storyArc = utils.listToString(arc_list)
|
||||
|
||||
return metadata
|
||||
|
||||
def cleanup_html( self, string):
|
||||
|
||||
if string is None:
|
||||
return ""
|
||||
# remove all newlines first
|
||||
string = string.replace("\n", "")
|
||||
|
||||
#put in our own
|
||||
string = string.replace("<br>", "\n")
|
||||
string = string.replace("</p>", "\n\n")
|
||||
string = string.replace("<h4>", "*")
|
||||
string = string.replace("</h4>", "*\n")
|
||||
|
||||
# now strip all other tags
|
||||
p = re.compile(r'<[^<]*?>')
|
||||
newstring = p.sub('',string)
|
||||
|
||||
newstring = newstring.replace(' ',' ')
|
||||
newstring = newstring.replace('&','&')
|
||||
|
||||
newstring = newstring.strip()
|
||||
return newstring
|
||||
|
||||
def fetchIssueDate( self, issue_id ):
|
||||
details = self.fetchIssueSelectDetails( issue_id )
|
||||
day, month, year = self.parseDateStr( details['cover_date'] )
|
||||
return month, year
|
||||
|
||||
def fetchIssueCoverURLs( self, issue_id ):
|
||||
details = self.fetchIssueSelectDetails( issue_id )
|
||||
return details['image_url'], details['thumb_image_url']
|
||||
|
||||
def fetchIssuePageURL( self, issue_id ):
|
||||
details = self.fetchIssueSelectDetails( issue_id )
|
||||
return details['site_detail_url']
|
||||
|
||||
def fetchIssueSelectDetails( self, issue_id ):
|
||||
|
||||
#cached_image_url,cached_thumb_url,cached_month,cached_year = self.fetchCachedIssueSelectDetails( issue_id )
|
||||
cached_details = self.fetchCachedIssueSelectDetails( issue_id )
|
||||
if cached_details['image_url'] is not None:
|
||||
return cached_details
|
||||
|
||||
issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + str(issue_id) + "/?api_key=" + self.api_key + "&format=json&field_list=image,cover_date,site_detail_url"
|
||||
|
||||
content = self.getUrlContent(issue_url)
|
||||
|
||||
details = dict()
|
||||
details['image_url'] = None
|
||||
details['thumb_image_url'] = None
|
||||
details['cover_date'] = None
|
||||
details['site_detail_url'] = None
|
||||
|
||||
cv_response = json.loads(content)
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] )
|
||||
return details
|
||||
|
||||
details['image_url'] = cv_response['results']['image']['super_url']
|
||||
details['thumb_image_url'] = cv_response['results']['image']['thumb_url']
|
||||
details['cover_date'] = cv_response['results']['cover_date']
|
||||
details['site_detail_url'] = cv_response['results']['site_detail_url']
|
||||
|
||||
if details['image_url'] is not None:
|
||||
self.cacheIssueSelectDetails( issue_id,
|
||||
details['image_url'],
|
||||
details['thumb_image_url'],
|
||||
details['cover_date'],
|
||||
details['site_detail_url'] )
|
||||
#print details['site_detail_url']
|
||||
return details
|
||||
|
||||
def fetchCachedIssueSelectDetails( self, issue_id ):
|
||||
|
||||
# before we search online, look in our cache, since we might already
|
||||
# have this info
|
||||
cvc = ComicVineCacher( )
|
||||
return cvc.get_issue_select_details( issue_id )
|
||||
|
||||
def cacheIssueSelectDetails( self, issue_id, image_url, thumb_url, cover_date, page_url ):
|
||||
cvc = ComicVineCacher( )
|
||||
cvc.add_issue_select_details( issue_id, image_url, thumb_url, cover_date, page_url )
|
||||
|
||||
|
||||
def fetchAlternateCoverURLs(self, issue_id, issue_page_url):
|
||||
url_list = self.fetchCachedAlternateCoverURLs( issue_id )
|
||||
if url_list is not None:
|
||||
return url_list
|
||||
|
||||
# scrape the CV issue page URL to get the alternate cover URLs
|
||||
resp = urllib2.urlopen( issue_page_url )
|
||||
content = resp.read()
|
||||
alt_cover_url_list = self.parseOutAltCoverUrls( content)
|
||||
|
||||
# cache this alt cover URL list
|
||||
self.cacheAlternateCoverURLs( issue_id, alt_cover_url_list )
|
||||
|
||||
return alt_cover_url_list
|
||||
|
||||
def parseOutAltCoverUrls( self, page_html ):
|
||||
soup = BeautifulSoup( page_html )
|
||||
|
||||
alt_cover_url_list = []
|
||||
|
||||
# Using knowledge of the layout of the ComicVine issue page here:
|
||||
# look for the divs that are in the classes 'content-pod' and 'alt-cover'
|
||||
div_list = soup.find_all( 'div')
|
||||
covers_found = 0
|
||||
for d in div_list:
|
||||
if d.has_key('class'):
|
||||
c = d['class']
|
||||
if 'imgboxart' in c and 'issue-cover' in c:
|
||||
covers_found += 1
|
||||
if covers_found != 1:
|
||||
alt_cover_url_list.append( d.img['src'] )
|
||||
|
||||
return alt_cover_url_list
|
||||
|
||||
def fetchCachedAlternateCoverURLs( self, issue_id ):
|
||||
|
||||
# before we search online, look in our cache, since we might already
|
||||
# have this info
|
||||
cvc = ComicVineCacher( )
|
||||
url_list = cvc.get_alt_covers( issue_id )
|
||||
if url_list is not None:
|
||||
return url_list
|
||||
else:
|
||||
return None
|
||||
|
||||
def cacheAlternateCoverURLs( self, issue_id, url_list ):
|
||||
cvc = ComicVineCacher( )
|
||||
cvc.add_alt_covers( issue_id, url_list )
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
urlFetchComplete = pyqtSignal( str , str, int)
|
||||
|
||||
def asyncFetchIssueCoverURLs( self, issue_id ):
|
||||
|
||||
self.issue_id = issue_id
|
||||
details = self.fetchCachedIssueSelectDetails( issue_id )
|
||||
if details['image_url'] is not None:
|
||||
self.urlFetchComplete.emit( details['image_url'],details['thumb_image_url'], self.issue_id )
|
||||
return
|
||||
|
||||
issue_url = "http://www.comicvine.com/api/issue/" + CVTypeID.Issue + "-" + str(issue_id) + "/?api_key=" + self.api_key + "&format=json&field_list=image,cover_date,site_detail_url"
|
||||
self.nam = QNetworkAccessManager()
|
||||
self.nam.finished.connect( self.asyncFetchIssueCoverURLComplete )
|
||||
self.nam.get(QNetworkRequest(QUrl(issue_url)))
|
||||
|
||||
def asyncFetchIssueCoverURLComplete( self, reply ):
|
||||
|
||||
# read in the response
|
||||
data = reply.readAll()
|
||||
|
||||
try:
|
||||
cv_response = json.loads(str(data))
|
||||
except:
|
||||
print >> sys.stderr, "Comic Vine query failed to get JSON data"
|
||||
print >> sys.stderr, str(data)
|
||||
return
|
||||
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] )
|
||||
return
|
||||
|
||||
image_url = cv_response['results']['image']['super_url']
|
||||
thumb_url = cv_response['results']['image']['thumb_url']
|
||||
cover_date = cv_response['results']['cover_date']
|
||||
page_url = cv_response['results']['site_detail_url']
|
||||
|
||||
self.cacheIssueSelectDetails( self.issue_id, image_url, thumb_url, cover_date, page_url )
|
||||
|
||||
self.urlFetchComplete.emit( image_url, thumb_url, self.issue_id )
|
||||
|
||||
altUrlListFetchComplete = pyqtSignal( list, int)
|
||||
|
||||
def asyncFetchAlternateCoverURLs( self, issue_id, issue_page_url ):
|
||||
# This async version requires the issue page url to be provided!
|
||||
self.issue_id = issue_id
|
||||
url_list = self.fetchCachedAlternateCoverURLs( issue_id )
|
||||
if url_list is not None:
|
||||
self.altUrlListFetchComplete.emit( url_list, int(self.issue_id) )
|
||||
return
|
||||
|
||||
self.nam = QNetworkAccessManager()
|
||||
self.nam.finished.connect( self.asyncFetchAlternateCoverURLsComplete )
|
||||
self.nam.get(QNetworkRequest(QUrl(str(issue_page_url))))
|
||||
|
||||
|
||||
def asyncFetchAlternateCoverURLsComplete( self, reply ):
|
||||
# read in the response
|
||||
html = str(reply.readAll())
|
||||
alt_cover_url_list = self.parseOutAltCoverUrls( html )
|
||||
|
||||
# cache this alt cover URL list
|
||||
self.cacheAlternateCoverURLs( self.issue_id, alt_cover_url_list )
|
||||
|
||||
self.altUrlListFetchComplete.emit( alt_cover_url_list, int(self.issue_id) )
|
||||
|
||||
def repairUrls(self, issue_list):
|
||||
#make sure there are URLs for the image fields
|
||||
for issue in issue_list:
|
||||
if issue['image'] is None:
|
||||
issue['image'] = dict()
|
||||
issue['image']['super_url'] = ComicVineTalker.logo_url
|
||||
issue['image']['thumb_url'] = ComicVineTalker.logo_url
|
||||
|
290
comictaggerlib/coverimagewidget.py
Normal file
@ -0,0 +1,290 @@
|
||||
"""
|
||||
A PyQt4 widget display cover images from either local archive, or from ComicVine
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from PyQt4.QtCore import *
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4 import uic
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
from genericmetadata import GenericMetadata, PageType
|
||||
from comicarchive import MetaDataStyle
|
||||
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from imagefetcher import ImageFetcher
|
||||
from pageloader import PageLoader
|
||||
from imagepopup import ImagePopup
|
||||
import utils
|
||||
|
||||
# helper func to allow a label to be clickable
|
||||
def clickable(widget):
|
||||
|
||||
class Filter(QObject):
|
||||
|
||||
dblclicked = pyqtSignal()
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
|
||||
if obj == widget:
|
||||
if event.type() == QEvent.MouseButtonDblClick:
|
||||
self.dblclicked.emit()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
filter = Filter(widget)
|
||||
widget.installEventFilter(filter)
|
||||
return filter.dblclicked
|
||||
|
||||
|
||||
class CoverImageWidget(QWidget):
|
||||
|
||||
ArchiveMode = 0
|
||||
AltCoverMode = 1
|
||||
URLMode = 1
|
||||
|
||||
def __init__(self, parent, mode ):
|
||||
super(CoverImageWidget, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('coverimagewidget.ui' ), self)
|
||||
|
||||
utils.reduceWidgetFontSize( self.label )
|
||||
|
||||
self.mode = mode
|
||||
self.comicVine = ComicVineTalker()
|
||||
self.page_loader = None
|
||||
self.showControls = True
|
||||
|
||||
self.btnLeft.setIcon(QIcon(ComicTaggerSettings.getGraphic('left.png')))
|
||||
self.btnRight.setIcon(QIcon(ComicTaggerSettings.getGraphic('right.png')))
|
||||
|
||||
self.btnLeft.clicked.connect( self.decrementImage )
|
||||
self.btnRight.clicked.connect( self.incrementImage )
|
||||
self.resetWidget()
|
||||
clickable(self.lblImage).connect(self.showPopup)
|
||||
|
||||
self.updateContent()
|
||||
|
||||
def resetWidget(self):
|
||||
self.comic_archive = None
|
||||
self.issue_id = None
|
||||
self.comicVine = None
|
||||
self.cover_fetcher = None
|
||||
self.url_list = []
|
||||
if self.page_loader is not None:
|
||||
self.page_loader.abandoned = True
|
||||
self.page_loader = None
|
||||
self.imageIndex = -1
|
||||
self.imageCount = 1
|
||||
|
||||
def clear( self ):
|
||||
self.resetWidget()
|
||||
self.updateContent()
|
||||
|
||||
def incrementImage( self ):
|
||||
self.imageIndex += 1
|
||||
if self.imageIndex == self.imageCount:
|
||||
self.imageIndex = 0
|
||||
self.updateContent()
|
||||
|
||||
def decrementImage( self ):
|
||||
self.imageIndex -= 1
|
||||
if self.imageIndex == -1:
|
||||
self.imageIndex = self.imageCount -1
|
||||
self.updateContent()
|
||||
|
||||
def setArchive( self, ca, page=0 ):
|
||||
if self.mode == CoverImageWidget.ArchiveMode:
|
||||
self.resetWidget()
|
||||
self.comic_archive = ca
|
||||
self.imageIndex = page
|
||||
self.imageCount = ca.getNumberOfPages()
|
||||
self.updateContent()
|
||||
|
||||
def setURL( self, url ):
|
||||
if self.mode == CoverImageWidget.URLMode:
|
||||
self.resetWidget()
|
||||
self.updateContent()
|
||||
|
||||
self.url_list = [ url ]
|
||||
self.imageIndex = 0
|
||||
self.imageCount = 1
|
||||
self.updateContent()
|
||||
|
||||
def setIssueID( self, issue_id ):
|
||||
if self.mode == CoverImageWidget.AltCoverMode:
|
||||
self.resetWidget()
|
||||
self.updateContent()
|
||||
|
||||
self.issue_id = issue_id
|
||||
|
||||
self.comicVine = ComicVineTalker()
|
||||
self.comicVine.urlFetchComplete.connect( self.primaryUrlFetchComplete )
|
||||
self.comicVine.asyncFetchIssueCoverURLs( int(self.issue_id) )
|
||||
|
||||
def primaryUrlFetchComplete( self, primary_url, thumb_url, issue_id ):
|
||||
self.url_list.append(str(primary_url))
|
||||
self.imageIndex = 0
|
||||
self.imageCount = len(self.url_list)
|
||||
self.updateContent()
|
||||
|
||||
#defer the alt cover search
|
||||
QTimer.singleShot(1, self.startAltCoverSearch)
|
||||
|
||||
def startAltCoverSearch( self ):
|
||||
|
||||
# now we need to get the list of alt cover URLs
|
||||
self.label.setText("Searching for alt. covers...")
|
||||
|
||||
# page URL should already be cached, so no need to defer
|
||||
self.comicVine = ComicVineTalker()
|
||||
issue_page_url = self.comicVine.fetchIssuePageURL( self.issue_id )
|
||||
self.comicVine.altUrlListFetchComplete.connect( self.altCoverUrlListFetchComplete )
|
||||
self.comicVine.asyncFetchAlternateCoverURLs( int(self.issue_id), issue_page_url)
|
||||
|
||||
def altCoverUrlListFetchComplete( self, url_list, issue_id ):
|
||||
if len(url_list) > 0:
|
||||
self.url_list.extend(url_list)
|
||||
self.imageCount = len(self.url_list)
|
||||
self.updateControls()
|
||||
|
||||
def setPage( self, pagenum ):
|
||||
if self.mode == CoverImageWidget.ArchiveMode:
|
||||
self.imageIndex = pagenum
|
||||
self.updateContent()
|
||||
|
||||
def updateContent( self ):
|
||||
self.updateImage()
|
||||
self.updateControls()
|
||||
|
||||
def updateImage( self ):
|
||||
if self.imageIndex == -1:
|
||||
self.loadDefault()
|
||||
elif self.mode in [ CoverImageWidget.AltCoverMode, CoverImageWidget.URLMode ]:
|
||||
self.loadURL()
|
||||
else:
|
||||
self.loadPage()
|
||||
|
||||
def updateControls( self ):
|
||||
if not self.showControls:
|
||||
self.btnLeft.hide()
|
||||
self.btnRight.hide()
|
||||
self.label.hide()
|
||||
return
|
||||
|
||||
if self.imageIndex == -1 or self.imageCount == 1:
|
||||
self.btnLeft.setEnabled(False)
|
||||
self.btnRight.setEnabled(False)
|
||||
self.btnLeft.hide()
|
||||
self.btnRight.hide()
|
||||
else:
|
||||
self.btnLeft.setEnabled(True)
|
||||
self.btnRight.setEnabled(True)
|
||||
self.btnLeft.show()
|
||||
self.btnRight.show()
|
||||
|
||||
if self.imageIndex == -1 or self.imageCount == 1:
|
||||
self.label.setText("")
|
||||
elif self.mode == CoverImageWidget.AltCoverMode:
|
||||
self.label.setText("Cover {0} ( of {1} )".format(self.imageIndex+1, self.imageCount))
|
||||
else:
|
||||
self.label.setText("Page {0} ( of {1} )".format(self.imageIndex+1, self.imageCount))
|
||||
|
||||
def loadURL( self ):
|
||||
self.loadDefault()
|
||||
self.cover_fetcher = ImageFetcher( )
|
||||
self.cover_fetcher.fetchComplete.connect(self.coverRemoteFetchComplete)
|
||||
self.cover_fetcher.fetch( self.url_list[self.imageIndex] )
|
||||
#print "ATB cover fetch started...."
|
||||
|
||||
# called when the image is done loading from internet
|
||||
def coverRemoteFetchComplete( self, image_data, issue_id ):
|
||||
img = QImage()
|
||||
img.loadFromData( image_data )
|
||||
self.current_pixmap = QPixmap(img)
|
||||
self.setDisplayPixmap( 0, 0)
|
||||
#print "ATB cover fetch complete!"
|
||||
|
||||
def loadPage( self ):
|
||||
if self.comic_archive is not None:
|
||||
if self.page_loader is not None:
|
||||
self.page_loader.abandoned = True
|
||||
self.page_loader = PageLoader( self.comic_archive, self.imageIndex )
|
||||
self.page_loader.loadComplete.connect( self.pageLoadComplete )
|
||||
self.page_loader.start()
|
||||
|
||||
def pageLoadComplete( self, img ):
|
||||
self.current_pixmap = QPixmap(img)
|
||||
self.setDisplayPixmap( 0, 0)
|
||||
self.page_loader = None
|
||||
|
||||
def loadDefault( self ):
|
||||
self.current_pixmap = QPixmap(ComicTaggerSettings.getGraphic('nocover.png'))
|
||||
#print "loadDefault called"
|
||||
self.setDisplayPixmap( 0, 0)
|
||||
|
||||
def resizeEvent( self, resize_event ):
|
||||
if self.current_pixmap is not None:
|
||||
delta_w = resize_event.size().width() - resize_event.oldSize().width()
|
||||
delta_h = resize_event.size().height() - resize_event.oldSize().height()
|
||||
#print "ATB resizeEvent deltas", resize_event.size().width(), resize_event.size().height()
|
||||
self.setDisplayPixmap( delta_w , delta_h )
|
||||
|
||||
def setDisplayPixmap( self, delta_w , delta_h ):
|
||||
# the deltas let us know what the new width and height of the label will be
|
||||
"""
|
||||
new_h = self.frame.height() + delta_h
|
||||
new_w = self.frame.width() + delta_w
|
||||
print "ATB setDisplayPixmap deltas", delta_w , delta_h
|
||||
print "ATB self.frame", self.frame.width(), self.frame.height()
|
||||
print "ATB self.", self.width(), self.height()
|
||||
|
||||
frame_w = new_w
|
||||
frame_h = new_h
|
||||
"""
|
||||
new_h = self.frame.height()
|
||||
new_w = self.frame.width()
|
||||
frame_w = self.frame.width()
|
||||
frame_h = self.frame.height()
|
||||
|
||||
new_h -= 4
|
||||
new_w -= 4
|
||||
|
||||
if new_h < 0:
|
||||
new_h = 0;
|
||||
if new_w < 0:
|
||||
new_w = 0;
|
||||
|
||||
#print "ATB setDisplayPixmap deltas", delta_w , delta_h
|
||||
#print "ATB self.frame", frame_w, frame_h
|
||||
#print "ATB new size", new_w, new_h
|
||||
|
||||
# scale the pixmap to fit in the frame
|
||||
scaled_pixmap = self.current_pixmap.scaled(new_w, new_h, Qt.KeepAspectRatio)
|
||||
self.lblImage.setPixmap( scaled_pixmap )
|
||||
|
||||
# move and resize the label to be centered in the fame
|
||||
img_w = scaled_pixmap.width()
|
||||
img_h = scaled_pixmap.height()
|
||||
self.lblImage.resize( img_w, img_h )
|
||||
self.lblImage.move( (frame_w - img_w)/2, (frame_h - img_h)/2 )
|
||||
|
||||
def showPopup( self ):
|
||||
self.popup = ImagePopup(self, self.current_pixmap)
|
@ -33,7 +33,7 @@ class CreditEditorWindow(QtGui.QDialog):
|
||||
def __init__(self, parent, mode, role, name, primary ):
|
||||
super(CreditEditorWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'crediteditorwindow.ui' ), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('crediteditorwindow.ui' ), self)
|
||||
|
||||
self.mode = mode
|
||||
|
@ -1,3 +1,3 @@
|
||||
# This file should contan only these comments, and the line below.
|
||||
# Used by packaging makefiles and app
|
||||
version="1.0.2a-beta"
|
||||
version="1.1.7-beta"
|
@ -36,9 +36,12 @@ class ExportWindow(QtGui.QDialog):
|
||||
def __init__( self, parent, settings, msg ):
|
||||
super(ExportWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'exportwindow.ui' ), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('exportwindow.ui' ), self)
|
||||
self.label.setText( msg )
|
||||
|
||||
self.setWindowFlags(self.windowFlags() &
|
||||
~QtCore.Qt.WindowContextHelpButtonHint )
|
||||
|
||||
self.settings = settings
|
||||
|
||||
self.cbxDeleteOriginal.setCheckState( QtCore.Qt.Unchecked )
|
@ -30,8 +30,11 @@ import os
|
||||
from urllib import unquote
|
||||
|
||||
class FileNameParser:
|
||||
def fixSpaces( self, string ):
|
||||
placeholders = ['[-_]',' +']
|
||||
def fixSpaces( self, string, remove_dashes=True ):
|
||||
if remove_dashes:
|
||||
placeholders = ['[-_]',' +']
|
||||
else:
|
||||
placeholders = ['[_]',' +']
|
||||
for ph in placeholders:
|
||||
string = re.sub(ph, ' ', string )
|
||||
return string.strip()
|
||||
@ -78,14 +81,20 @@ class FileNameParser:
|
||||
found = False
|
||||
issue = ''
|
||||
|
||||
# first, look for multiple "--", this mean's it's formatted differently from most:
|
||||
# first, look for multiple "--", this means it's formatted differently from most:
|
||||
if "--" in filename:
|
||||
# the pattern seems to be that anything to left of the first "--" is the series name followed by issue
|
||||
filename = filename.split("--")[0]
|
||||
elif "___" in filename:
|
||||
# the pattern seems to be that anything to left of the first "__" is the series name followed by issue
|
||||
filename = filename.split("__")[0]
|
||||
|
||||
filename = filename.replace("+", " ")
|
||||
|
||||
# remove parenthetical phrases
|
||||
filename = re.sub( "\(.*\)", "", filename)
|
||||
filename = re.sub( "\[.*\]", "", filename)
|
||||
|
||||
# guess based on position
|
||||
|
||||
# replace any name seperators with spaces
|
||||
@ -101,22 +110,31 @@ class FileNameParser:
|
||||
word_list[i+2] ="XXX"
|
||||
|
||||
|
||||
# first look for the last "#" followed by a digit in the filename. this is almost certainly the issue number
|
||||
#issnum = re.search('#\d+', filename)
|
||||
matchlist = re.findall("#[-+]?(([0-9]*\.[0-9]+|[0-9]+)(\w*))", filename)
|
||||
if len(matchlist) > 0:
|
||||
#get the last item
|
||||
issue = matchlist[ len(matchlist) - 1][0]
|
||||
found = True
|
||||
|
||||
# assume the last number in the filename that is under 4 digits is the issue number
|
||||
for word in reversed(word_list):
|
||||
if len(word) > 0 and word[0] == "#":
|
||||
word = word[1:]
|
||||
if (
|
||||
(word.isdigit() and len(word) < 4) or
|
||||
(self.isPointIssue(word))
|
||||
):
|
||||
issue = word
|
||||
found = True
|
||||
#print 'Assuming issue number is ' + str(issue) + ' based on the position.'
|
||||
break
|
||||
if not found:
|
||||
for word in reversed(word_list):
|
||||
if len(word) > 0 and word[0] == "#":
|
||||
word = word[1:]
|
||||
if (
|
||||
(word.isdigit() and len(word) < 4) or
|
||||
(self.isPointIssue(word))
|
||||
):
|
||||
issue = word
|
||||
found = True
|
||||
#print 'Assuming issue number is ' + str(issue) + ' based on the position.'
|
||||
break
|
||||
|
||||
if not found:
|
||||
# try a regex
|
||||
issnum = re.search('(?<=[_#\s-])(\d+[a-zA-Z]|\d+\.\d|\d+)', filename)
|
||||
issnum = re.search('(?<=[_#\s-])(\d+[a-zA-Z]+|\d+\.\d|\d+)', filename)
|
||||
if issnum:
|
||||
issue = issnum.group()
|
||||
found = True
|
||||
@ -133,8 +151,8 @@ class FileNameParser:
|
||||
# TODO: we really should pass in the *INDEX* of the issue, that makes
|
||||
# finding it easier
|
||||
|
||||
|
||||
tmpstr = self.fixSpaces(filename)
|
||||
filename = filename.replace("+", " ")
|
||||
tmpstr = self.fixSpaces(filename, remove_dashes=False)
|
||||
|
||||
#remove pound signs. this might mess up the series name if there is a# in it.
|
||||
tmpstr = tmpstr.replace("#", " ")
|
||||
@ -173,6 +191,28 @@ class FileNameParser:
|
||||
year = re.sub("[^0-9]", "", year)
|
||||
return year
|
||||
|
||||
def getRemainder( self, filename, year, count ):
|
||||
#make a guess at where the the non-interesting stuff begins
|
||||
|
||||
remainder = ""
|
||||
|
||||
if "--" in filename:
|
||||
remainder = filename.split("--",1)[1]
|
||||
elif "__" in filename:
|
||||
remainder = filename.split("__",1)[1]
|
||||
elif "(" in filename:
|
||||
remainder = "(" + filename.split("(",1)[1]
|
||||
|
||||
remainder = self.fixSpaces(remainder, remove_dashes=False)
|
||||
if year != "":
|
||||
remainder = remainder.replace(year,"",1)
|
||||
if count != "":
|
||||
remainder = remainder.replace("of "+count,"",1)
|
||||
|
||||
remainder = remainder.replace("()","")
|
||||
|
||||
return remainder.strip()
|
||||
|
||||
def parseFilename( self, filename ):
|
||||
|
||||
# remove the path
|
||||
@ -190,20 +230,12 @@ class FileNameParser:
|
||||
if filename.count("_28") > 1 and filename.count("_29") > 1:
|
||||
filename = filename.replace("_28", "(")
|
||||
filename = filename.replace("_29", ")")
|
||||
|
||||
# ----HACK
|
||||
# remove the first word that word is a 3 digit number.
|
||||
# some story arcs collection packs do this, but it's ugly
|
||||
# this will probably break something, i.e. "100 bullets"
|
||||
word = filename.split(' ')[0]
|
||||
if len(word) == 3 and word[0] =='0' and word.isdigit():
|
||||
filename = filename[4:]
|
||||
# ----HACK -
|
||||
|
||||
self.issue = self.getIssueNumber(filename)
|
||||
self.series, self.volume = self.getSeriesName(filename, self.issue)
|
||||
self.year = self.getYear(filename)
|
||||
self.issue_count = self.getIssueCount(filename)
|
||||
self.remainder = self.getRemainder( filename, self.year, self.issue_count )
|
||||
|
||||
if self.issue != "":
|
||||
# strip off leading zeros
|
@ -21,6 +21,7 @@ limitations under the License.
|
||||
import os
|
||||
import re
|
||||
import datetime
|
||||
import utils
|
||||
from issuestring import IssueString
|
||||
|
||||
class FileRenamer:
|
||||
@ -71,6 +72,7 @@ class FileRenamer:
|
||||
|
||||
md = self.metdata
|
||||
new_name = self.template
|
||||
preferred_encoding = utils.get_actual_preferred_encoding()
|
||||
|
||||
#print u"{0}".format(md)
|
||||
|
||||
@ -93,8 +95,21 @@ class FileRenamer:
|
||||
if (type(md.month) == str and md.month.isdigit()) or type(md.month) == int:
|
||||
if int(md.month) in range(1,13):
|
||||
dt = datetime.datetime( 1970, int(md.month), 1, 0, 0)
|
||||
month_name = dt.strftime("%B")
|
||||
month_name = dt.strftime(u"%B".encode(preferred_encoding)).decode(preferred_encoding)
|
||||
new_name = self.replaceToken( new_name, month_name, '%month_name%')
|
||||
|
||||
new_name = self.replaceToken( new_name, md.genre, '%genre%')
|
||||
new_name = self.replaceToken( new_name, md.language, '%language_code%')
|
||||
new_name = self.replaceToken( new_name, md.criticalRating , '%criticalrating%')
|
||||
new_name = self.replaceToken( new_name, md.alternateSeries, '%alternateseries%')
|
||||
new_name = self.replaceToken( new_name, md.alternateNumber, '%alternatenumber%')
|
||||
new_name = self.replaceToken( new_name, md.alternateCount, '%alternatecount%')
|
||||
new_name = self.replaceToken( new_name, md.imprint, '%imprint%')
|
||||
new_name = self.replaceToken( new_name, md.format, '%format%')
|
||||
new_name = self.replaceToken( new_name, md.maturityRating, '%maturityrating%')
|
||||
new_name = self.replaceToken( new_name, md.storyArc, '%storyarc%')
|
||||
new_name = self.replaceToken( new_name, md.seriesGroup, '%seriesgroup%')
|
||||
new_name = self.replaceToken( new_name, md.scanInfo, '%scaninfo%')
|
||||
|
||||
if self.smart_cleanup:
|
||||
|
@ -20,6 +20,7 @@ limitations under the License.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from PyQt4.QtCore import *
|
||||
from PyQt4.QtGui import *
|
||||
@ -28,8 +29,9 @@ from PyQt4.QtCore import pyqtSignal
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
from comicarchive import ComicArchive
|
||||
from comicarchive import MetaDataStyle
|
||||
from genericmetadata import GenericMetadata, PageType
|
||||
from options import MetaDataStyle
|
||||
import utils
|
||||
|
||||
class FileTableWidget( QTableWidget ):
|
||||
|
||||
@ -69,14 +71,12 @@ class FileSelectionList(QWidget):
|
||||
def __init__(self, parent , settings ):
|
||||
super(FileSelectionList, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'fileselectionlist.ui' ), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('fileselectionlist.ui' ), self)
|
||||
|
||||
self.settings = settings
|
||||
#self.twList = FileTableWidget( self )
|
||||
#gridlayout = QGridLayout( self )
|
||||
#gridlayout.addWidget( self.twList )
|
||||
|
||||
utils.reduceWidgetFontSize( self.twList )
|
||||
|
||||
#self.twList.itemSelectionChanged.connect( self.itemSelectionChangedCB )
|
||||
self.twList.currentItemChanged.connect( self.currentItemChangedCB )
|
||||
|
||||
self.currentItem = None
|
||||
@ -98,6 +98,14 @@ class FileSelectionList(QWidget):
|
||||
self.addAction(removeAction)
|
||||
self.addAction(self.separator)
|
||||
|
||||
def getSorting(self):
|
||||
col = self.twList.horizontalHeader().sortIndicatorSection()
|
||||
order = self.twList.horizontalHeader().sortIndicatorOrder()
|
||||
return col, order
|
||||
|
||||
def setSorting(self, col, order):
|
||||
col = self.twList.horizontalHeader().setSortIndicator( col, order)
|
||||
|
||||
def addAppAction( self, action ):
|
||||
self.insertAction( None , action )
|
||||
|
||||
@ -159,21 +167,15 @@ class FileSelectionList(QWidget):
|
||||
self.listCleared.emit()
|
||||
|
||||
def addPathList( self, pathlist ):
|
||||
filelist = []
|
||||
for p in pathlist:
|
||||
# if path is a folder, walk it recursivly, and all files underneath
|
||||
if os.path.isdir( unicode(p)):
|
||||
for root,dirs,files in os.walk( unicode(p) ):
|
||||
for f in files:
|
||||
filelist.append(os.path.join(root,unicode(f)))
|
||||
else:
|
||||
filelist.append(unicode(p))
|
||||
|
||||
filelist = utils.get_recursive_filelist( pathlist )
|
||||
|
||||
# we now have a list of files to add
|
||||
|
||||
progdialog = QProgressDialog("", "Cancel", 0, len(filelist), self)
|
||||
progdialog.setWindowTitle( "Adding Files" )
|
||||
progdialog.setWindowModality(Qt.WindowModal)
|
||||
#progdialog.setWindowModality(Qt.WindowModal)
|
||||
progdialog.setWindowModality(Qt.ApplicationModal)
|
||||
progdialog.show()
|
||||
|
||||
firstAdded = None
|
||||
@ -184,6 +186,7 @@ class FileSelectionList(QWidget):
|
||||
break
|
||||
progdialog.setValue(idx)
|
||||
progdialog.setLabelText(f)
|
||||
utils.centerWindowOnParent( progdialog )
|
||||
QCoreApplication.processEvents()
|
||||
row = self.addPathItem( f )
|
||||
if firstAdded is None and row is not None:
|
||||
@ -224,12 +227,9 @@ class FileSelectionList(QWidget):
|
||||
if self.isListDupe(path):
|
||||
return None
|
||||
|
||||
ca = ComicArchive( path )
|
||||
if self.settings.rar_exe_path != "":
|
||||
ca.setExternalRarProgram( self.settings.rar_exe_path )
|
||||
ca = ComicArchive( path, self.settings )
|
||||
|
||||
if ca.seemsToBeAComicArchive() :
|
||||
|
||||
row = self.twList.rowCount()
|
||||
self.twList.insertRow( row )
|
||||
|
@ -63,6 +63,7 @@ class GenericMetadata:
|
||||
self.publisher = None
|
||||
self.month = None
|
||||
self.year = None
|
||||
self.day = None
|
||||
self.issueCount = None
|
||||
self.volume = None
|
||||
self.genre = None
|
||||
@ -125,6 +126,7 @@ class GenericMetadata:
|
||||
assign( "issueCount", new_md.issueCount )
|
||||
assign( "title", new_md.title )
|
||||
assign( "publisher", new_md.publisher )
|
||||
assign( "day", new_md.day )
|
||||
assign( "month", new_md.month )
|
||||
assign( "year", new_md.year )
|
||||
assign( "volume", new_md.volume )
|
||||
@ -256,8 +258,9 @@ class GenericMetadata:
|
||||
add_attr_string( "issueCount" )
|
||||
add_attr_string( "title" )
|
||||
add_attr_string( "publisher" )
|
||||
add_attr_string( "month" )
|
||||
add_attr_string( "year" )
|
||||
add_attr_string( "month" )
|
||||
add_attr_string( "day" )
|
||||
add_attr_string( "volume" )
|
||||
add_attr_string( "volumeCount" )
|
||||
add_attr_string( "genre" )
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
BIN
comictaggerlib/graphics/left.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
comictaggerlib/graphics/longbox.png
Normal file
After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.5 KiB |
BIN
comictaggerlib/graphics/popup_bg.png
Normal file
After Width: | Height: | Size: 362 B |
BIN
comictaggerlib/graphics/right.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
@ -1,4 +1,22 @@
|
||||
"""
|
||||
A pthyon class to manage creating image content hashes, and calculate hamming distances
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2013 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
import StringIO
|
||||
import sys
|
||||
|
86
comictaggerlib/imagepopup.py
Normal file
@ -0,0 +1,86 @@
|
||||
"""
|
||||
A PyQT4 widget to display a popup image
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
import os
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
|
||||
class ImagePopup(QtGui.QDialog):
|
||||
|
||||
def __init__(self, parent, image_pixmap):
|
||||
super(ImagePopup, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('imagepopup.ui' ), self)
|
||||
|
||||
QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
|
||||
|
||||
#self.setWindowModality(QtCore.Qt.WindowModal)
|
||||
self.setWindowFlags(QtCore.Qt.Popup)
|
||||
self.setWindowState(QtCore.Qt.WindowFullScreen)
|
||||
|
||||
self.imagePixmap = image_pixmap
|
||||
|
||||
screen_size = QtGui.QDesktopWidget().screenGeometry()
|
||||
self.resize(screen_size.width(), screen_size.height())
|
||||
self.move( 0, 0)
|
||||
|
||||
# This is a total hack. Uses a snapshot of the desktop, and overlays a
|
||||
# translucent screen over it. Probably can do it better by setting opacity of a
|
||||
# widget
|
||||
self.desktopBg = QtGui.QPixmap.grabWindow(QtGui.QApplication.desktop ().winId(),
|
||||
0,0, screen_size.width(), screen_size.height())
|
||||
bg = QtGui.QPixmap(ComicTaggerSettings.getGraphic('popup_bg.png'))
|
||||
self.clientBgPixmap = bg.scaled(screen_size.width(), screen_size.height())
|
||||
self.setMask(self.clientBgPixmap.mask())
|
||||
|
||||
self.applyImagePixmap()
|
||||
self.showFullScreen()
|
||||
self.raise_( )
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
|
||||
def paintEvent (self, event):
|
||||
self.painter = QtGui.QPainter(self)
|
||||
self.painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
self.painter.drawPixmap(0, 0, self.desktopBg)
|
||||
self.painter.drawPixmap(0, 0, self.clientBgPixmap)
|
||||
self.painter.end()
|
||||
|
||||
def applyImagePixmap( self ):
|
||||
win_h = self.height()
|
||||
win_w = self.width()
|
||||
|
||||
if self.imagePixmap.width() > win_w or self.imagePixmap.height() > win_h:
|
||||
# scale the pixmap to fit in the frame
|
||||
display_pixmap = self.imagePixmap.scaled(win_w, win_h, QtCore.Qt.KeepAspectRatio)
|
||||
self.lblImage.setPixmap( display_pixmap )
|
||||
else:
|
||||
display_pixmap = self.imagePixmap
|
||||
self.lblImage.setPixmap( display_pixmap )
|
||||
|
||||
# move and resize the label to be centered in the fame
|
||||
img_w = display_pixmap.width()
|
||||
img_h = display_pixmap.height()
|
||||
self.lblImage.resize( img_w, img_h )
|
||||
self.lblImage.move( (win_w - img_w)/2, (win_h - img_h)/2 )
|
||||
|
||||
def mousePressEvent( self , event):
|
||||
self.close()
|
@ -20,6 +20,7 @@ limitations under the License.
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
|
||||
@ -29,6 +30,15 @@ from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from imagefetcher import ImageFetcher
|
||||
from settings import ComicTaggerSettings
|
||||
from issuestring import IssueString
|
||||
from coverimagewidget import CoverImageWidget
|
||||
import utils
|
||||
|
||||
class IssueNumberTableWidgetItem(QtGui.QTableWidgetItem):
|
||||
def __lt__(self, other):
|
||||
selfStr = self.data(QtCore.Qt.DisplayRole).toString()
|
||||
otherStr = other.data(QtCore.Qt.DisplayRole).toString()
|
||||
return (IssueString(selfStr).asFloat() <
|
||||
IssueString(otherStr).asFloat())
|
||||
|
||||
class IssueSelectionWindow(QtGui.QDialog):
|
||||
|
||||
@ -37,8 +47,20 @@ class IssueSelectionWindow(QtGui.QDialog):
|
||||
def __init__(self, parent, settings, series_id, issue_number):
|
||||
super(IssueSelectionWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'issueselectionwindow.ui' ), self)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('issueselectionwindow.ui' ), self)
|
||||
|
||||
self.coverWidget = CoverImageWidget( self.coverImageContainer, CoverImageWidget.AltCoverMode )
|
||||
gridlayout = QtGui.QGridLayout( self.coverImageContainer )
|
||||
gridlayout.addWidget( self.coverWidget )
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
|
||||
utils.reduceWidgetFontSize( self.twList )
|
||||
utils.reduceWidgetFontSize( self.teDescription, 1 )
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
self.series_id = series_id
|
||||
self.settings = settings
|
||||
self.url_fetch_thread = None
|
||||
@ -72,6 +94,7 @@ class IssueSelectionWindow(QtGui.QDialog):
|
||||
try:
|
||||
comicVine = ComicVineTalker( )
|
||||
volume_data = comicVine.fetchVolumeData( self.series_id )
|
||||
self.issue_list = comicVine.fetchIssuesByVolume( self.series_id )
|
||||
except ComicVineTalkerException:
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
QtGui.QMessageBox.critical(self, self.tr("Network Issue"), self.tr("Could not connect to ComicVine to list issues!"))
|
||||
@ -79,8 +102,6 @@ class IssueSelectionWindow(QtGui.QDialog):
|
||||
|
||||
while self.twList.rowCount() > 0:
|
||||
self.twList.removeRow(0)
|
||||
|
||||
self.issue_list = volume_data['issues']
|
||||
|
||||
self.twList.setSortingEnabled(False)
|
||||
|
||||
@ -89,18 +110,35 @@ class IssueSelectionWindow(QtGui.QDialog):
|
||||
self.twList.insertRow(row)
|
||||
|
||||
item_text = record['issue_number']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item = IssueNumberTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setData( QtCore.Qt.UserRole ,record['id'])
|
||||
item.setData(QtCore.Qt.DisplayRole, float(item_text))
|
||||
item.setData(QtCore.Qt.DisplayRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, item)
|
||||
|
||||
item_text = record['name']
|
||||
item_text = record['cover_date']
|
||||
if item_text is None:
|
||||
item_text = ""
|
||||
#remove the day of "YYYY-MM-DD"
|
||||
parts = item_text.split("-")
|
||||
if len(parts) > 1:
|
||||
item_text = parts[0] + "-" + parts[1]
|
||||
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 1, item)
|
||||
|
||||
item_text = record['name']
|
||||
if item_text is None:
|
||||
item_text = ""
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 2, item)
|
||||
|
||||
if IssueString(record['issue_number']).asString() == IssueString(self.issue_number).asString():
|
||||
if IssueString(record['issue_number']).asString().lower() == IssueString(self.issue_number).asString().lower():
|
||||
self.initial_id = record['id']
|
||||
|
||||
row += 1
|
||||
@ -119,35 +157,18 @@ class IssueSelectionWindow(QtGui.QDialog):
|
||||
return
|
||||
if prev is not None and prev.row() == curr.row():
|
||||
return
|
||||
|
||||
|
||||
self.issue_id, b = self.twList.item( curr.row(), 0 ).data( QtCore.Qt.UserRole ).toInt()
|
||||
|
||||
# list selection was changed, update the the issue cover
|
||||
for record in self.issue_list:
|
||||
if record['id'] == self.issue_id:
|
||||
|
||||
if record['id'] == self.issue_id:
|
||||
self.issue_number = record['issue_number']
|
||||
|
||||
self.labelThumbnail.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
|
||||
self.cv = ComicVineTalker( )
|
||||
self.cv.urlFetchComplete.connect( self.urlFetchComplete )
|
||||
self.cv.asyncFetchIssueCoverURLs( int(self.issue_id) )
|
||||
self.coverWidget.setIssueID( int(self.issue_id) )
|
||||
if record['description'] is None:
|
||||
self.teDescription.setText ( "" )
|
||||
else:
|
||||
self.teDescription.setText ( record['description'] )
|
||||
|
||||
break
|
||||
|
||||
# called when the cover URL has been fetched
|
||||
def urlFetchComplete( self, image_url, thumb_url, issue_id ):
|
||||
|
||||
self.cover_fetcher = ImageFetcher( )
|
||||
self.cover_fetcher.fetchComplete.connect(self.coverFetchComplete)
|
||||
self.cover_fetcher.fetch( str(image_url), user_data=issue_id )
|
||||
|
||||
# called when the image is done loading
|
||||
def coverFetchComplete( self, image_data, issue_id ):
|
||||
if self.issue_id == issue_id:
|
||||
img = QtGui.QImage()
|
||||
img.loadFromData( image_data )
|
||||
self.labelThumbnail.setPixmap(QtGui.QPixmap(img))
|
||||
|
@ -39,7 +39,7 @@ class IssueString:
|
||||
self.suffix = ""
|
||||
return
|
||||
|
||||
self.text = str(text)
|
||||
self.text = unicode(text)
|
||||
#strip out non float-y stuff
|
||||
tmp_num_str = re.sub('[^0-9.-]',"", self.text )
|
||||
|
@ -30,9 +30,11 @@ class LogWindow(QtGui.QDialog):
|
||||
def __init__(self, parent):
|
||||
super(LogWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'logwindow.ui' ), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('logwindow.ui' ), self)
|
||||
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
def setText( self, text ):
|
||||
self.textEdit.setPlainText( text )
|
79
comictaggerlib/main.py
Executable file
@ -0,0 +1,79 @@
|
||||
"""
|
||||
A python app to (automatically) tag comic archives
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import signal
|
||||
import os
|
||||
import traceback
|
||||
import platform
|
||||
|
||||
import utils
|
||||
import cli
|
||||
from settings import ComicTaggerSettings
|
||||
from options import Options
|
||||
|
||||
try:
|
||||
qt_available = True
|
||||
from PyQt4 import QtCore, QtGui
|
||||
from taggerwindow import TaggerWindow
|
||||
except ImportError as e:
|
||||
qt_available = False
|
||||
#---------------------------------------
|
||||
|
||||
def ctmain():
|
||||
utils.fix_output_encoding()
|
||||
settings = ComicTaggerSettings()
|
||||
|
||||
opts = Options()
|
||||
opts.parseCmdLineArgs()
|
||||
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
|
||||
if not qt_available and not opts.no_gui:
|
||||
opts.no_gui = True
|
||||
print >> sys.stderr, "PyQt4 is not available. ComicTagger is limited to command-line mode."
|
||||
|
||||
if opts.no_gui:
|
||||
cli.cli_mode( opts, settings )
|
||||
else:
|
||||
app = QtGui.QApplication(sys.argv)
|
||||
|
||||
if platform.system() != "Linux":
|
||||
img = QtGui.QPixmap(ComicTaggerSettings.getGraphic('tags.png'))
|
||||
|
||||
splash = QtGui.QSplashScreen(img)
|
||||
splash.show()
|
||||
splash.raise_()
|
||||
app.processEvents()
|
||||
|
||||
try:
|
||||
tagger_window = TaggerWindow( opts.file_list, settings )
|
||||
tagger_window.show()
|
||||
|
||||
if platform.system() != "Linux":
|
||||
splash.finish( tagger_window )
|
||||
|
||||
sys.exit(app.exec_())
|
||||
except Exception, e:
|
||||
QtGui.QMessageBox.critical(QtGui.QMainWindow(), "Error", "Unhandled exception in app:\n" + traceback.format_exc() )
|
||||
|
||||
|
||||
|
||||
|
160
comictaggerlib/matchselectionwindow.py
Normal file
@ -0,0 +1,160 @@
|
||||
"""
|
||||
A PyQT4 dialog to select from automated issue matches
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
|
||||
from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
|
||||
|
||||
from imagefetcher import ImageFetcher
|
||||
from settings import ComicTaggerSettings
|
||||
from comicarchive import MetaDataStyle
|
||||
from coverimagewidget import CoverImageWidget
|
||||
from comicvinetalker import ComicVineTalker
|
||||
import utils
|
||||
|
||||
class MatchSelectionWindow(QtGui.QDialog):
|
||||
|
||||
volume_id = 0
|
||||
|
||||
def __init__(self, parent, matches, comic_archive):
|
||||
super(MatchSelectionWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('matchselectionwindow.ui' ), self)
|
||||
|
||||
self.altCoverWidget = CoverImageWidget( self.altCoverContainer, CoverImageWidget.AltCoverMode )
|
||||
gridlayout = QtGui.QGridLayout( self.altCoverContainer )
|
||||
gridlayout.addWidget( self.altCoverWidget )
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
|
||||
self.archiveCoverWidget = CoverImageWidget( self.archiveCoverContainer, CoverImageWidget.ArchiveMode )
|
||||
gridlayout = QtGui.QGridLayout( self.archiveCoverContainer )
|
||||
gridlayout.addWidget( self.archiveCoverWidget )
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
|
||||
utils.reduceWidgetFontSize( self.twList )
|
||||
utils.reduceWidgetFontSize( self.teDescription, 1 )
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
self.matches = matches
|
||||
self.comic_archive = comic_archive
|
||||
|
||||
self.twList.currentItemChanged.connect(self.currentItemChanged)
|
||||
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
|
||||
|
||||
self.updateData()
|
||||
|
||||
def updateData( self):
|
||||
|
||||
self.setCoverImage()
|
||||
self.populateTable()
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.selectRow( 0 )
|
||||
|
||||
path = self.comic_archive.path
|
||||
self.setWindowTitle( u"Select correct match: {0}".format(
|
||||
os.path.split(path)[1] ))
|
||||
|
||||
def populateTable( self ):
|
||||
|
||||
while self.twList.rowCount() > 0:
|
||||
self.twList.removeRow(0)
|
||||
|
||||
self.twList.setSortingEnabled(False)
|
||||
|
||||
row = 0
|
||||
for match in self.matches:
|
||||
self.twList.insertRow(row)
|
||||
|
||||
item_text = match['series']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setData( QtCore.Qt.UserRole, (match,))
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, item)
|
||||
|
||||
if match['publisher'] is not None:
|
||||
item_text = u"{0}".format(match['publisher'])
|
||||
else:
|
||||
item_text = u"Unknown"
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 1, item)
|
||||
|
||||
month_str = u""
|
||||
year_str = u"????"
|
||||
if match['month'] is not None:
|
||||
month_str = u"-{0:02d}".format(int(match['month']))
|
||||
if match['year'] is not None:
|
||||
year_str = u"{0}".format(match['year'])
|
||||
|
||||
item_text = year_str + month_str
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 2, item)
|
||||
|
||||
item_text = match['issue_title']
|
||||
if item_text is None:
|
||||
item_text = ""
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 3, item)
|
||||
|
||||
row += 1
|
||||
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.sortItems( 2 , QtCore.Qt.AscendingOrder )
|
||||
self.twList.selectRow(0)
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.horizontalHeader().setStretchLastSection(True)
|
||||
|
||||
|
||||
def cellDoubleClicked( self, r, c ):
|
||||
self.accept()
|
||||
|
||||
def currentItemChanged( self, curr, prev ):
|
||||
|
||||
if curr is None:
|
||||
return
|
||||
if prev is not None and prev.row() == curr.row():
|
||||
return
|
||||
|
||||
self.altCoverWidget.setIssueID( self.currentMatch()['issue_id'] )
|
||||
if self.currentMatch()['description'] is None:
|
||||
self.teDescription.setText ( "" )
|
||||
else:
|
||||
self.teDescription.setText ( self.currentMatch()['description'] )
|
||||
|
||||
def setCoverImage( self ):
|
||||
self.archiveCoverWidget.setArchive( self.comic_archive)
|
||||
|
||||
def currentMatch( self ):
|
||||
row = self.twList.currentRow()
|
||||
match = self.twList.item(row, 0).data( QtCore.Qt.UserRole ).toPyObject()[0]
|
||||
return match
|
||||
|
@ -22,22 +22,12 @@ import sys
|
||||
import getopt
|
||||
import platform
|
||||
import os
|
||||
|
||||
import traceback
|
||||
import ctversion
|
||||
import utils
|
||||
from genericmetadata import GenericMetadata
|
||||
|
||||
class Enum(set):
|
||||
def __getattr__(self, name):
|
||||
if name in self:
|
||||
return name
|
||||
raise AttributeError
|
||||
|
||||
class MetaDataStyle:
|
||||
CBI = 0
|
||||
CIX = 1
|
||||
COMET = 2
|
||||
name = [ 'ComicBookLover', 'ComicRack', 'CoMet' ]
|
||||
|
||||
from comicarchive import MetaDataStyle
|
||||
from versionchecker import VersionChecker
|
||||
|
||||
class Options:
|
||||
help_text = """
|
||||
@ -57,6 +47,7 @@ If no options are given, {0} will run in windowed mode
|
||||
-s, --save Save out tags as specified type (via -t)
|
||||
Must specify also at least -o, -p, or -m
|
||||
--nooverwrite Don't modify tag block if it already exists ( relevent for -s or -c )
|
||||
-1, --assume-issue-one Assume issue number is 1 if not found ( relevent for -s )
|
||||
-n, --dryrun Don't actually modify file (only relevent for -d, -s, or -r)
|
||||
-t, --type=TYPE Specify TYPE as either "CR", "CBL", or "COMET" (as either
|
||||
ComicRack, ComicBookLover, or CoMet style tags, respectivly)
|
||||
@ -78,12 +69,21 @@ If no options are given, {0} will run in windowed mode
|
||||
Some names that can be used:
|
||||
series, issue, issueCount, year, publisher, title
|
||||
-r, --rename Rename the file based on specified tag style.
|
||||
--noabort Don't abort save operation when online match is of low confidence
|
||||
--noabort Don't abort save operation when online match is of low confidence
|
||||
-e, --export-to-zip Export RAR archive to Zip format
|
||||
--delete-rar Delete original RAR archive after successful export to Zip
|
||||
--abort-on-conflict Don't export to zip if intended new filename exists (Otherwise, creates
|
||||
a new unique filename)
|
||||
-S, --script=FILE Run an "add-on" python script that uses the comictagger library for custom
|
||||
processing. Script arguments can follow the script name
|
||||
-R, --recursive Recursively include files in sub-folders
|
||||
-v, --verbose Be noisy when doing what it does
|
||||
--terse Don't say much (for print mode)
|
||||
--version Display version
|
||||
-h, --help Display this message
|
||||
"""
|
||||
-h, --help Display this message
|
||||
|
||||
For more help visit the wiki at: http://code.google.com/p/comictagger/
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
@ -96,6 +96,9 @@ If no options are given, {0} will run in windowed mode
|
||||
self.print_tags = False
|
||||
self.copy_tags = False
|
||||
self.delete_tags = False
|
||||
self.export_to_zip = False
|
||||
self.abort_export_on_conflict = False
|
||||
self.delete_rar_after_export = False
|
||||
self.search_online = False
|
||||
self.dryrun = False
|
||||
self.abortOnLowConfidence = True
|
||||
@ -107,6 +110,10 @@ If no options are given, {0} will run in windowed mode
|
||||
self.no_overwrite = False
|
||||
self.interactive = False
|
||||
self.issue_id = None
|
||||
self.recursive = False
|
||||
self.run_script = False
|
||||
self.script = None
|
||||
self.assume_issue_is_one_if_not_set = False
|
||||
self.file_list = []
|
||||
|
||||
def display_msg_and_quit( self, msg, code, show_help=False ):
|
||||
@ -179,20 +186,26 @@ If no options are given, {0} will run in windowed mode
|
||||
# parse command line options
|
||||
try:
|
||||
opts, args = getopt.getopt( input_args,
|
||||
"hpdt:fm:vonsrc:i",
|
||||
"hpdt:fm:vonsrc:ieRS:1",
|
||||
[ "help", "print", "delete", "type=", "copy=", "parsefilename", "metadata=", "verbose",
|
||||
"online", "dryrun", "save", "rename" , "raw", "noabort", "terse", "nooverwrite",
|
||||
"interactive", "nosummary", "version", "id=" ])
|
||||
|
||||
"interactive", "nosummary", "version", "id=" , "recursive", "script=",
|
||||
"export-to-zip", "delete-rar", "abort-on-conflict", "assume-issue-one" ] )
|
||||
|
||||
except getopt.GetoptError as err:
|
||||
self.display_msg_and_quit( str(err), 2 )
|
||||
|
||||
|
||||
# process options
|
||||
for o, a in opts:
|
||||
if o in ("-h", "--help"):
|
||||
self.display_msg_and_quit( None, 0, show_help=True )
|
||||
if o in ("-v", "--verbose"):
|
||||
self.verbose = True
|
||||
if o in ("-S", "--script"):
|
||||
self.run_script = True
|
||||
self.script = a
|
||||
if o in ("-R", "--recursive"):
|
||||
self.recursive = True
|
||||
if o in ("-p", "--print"):
|
||||
self.print_tags = True
|
||||
if o in ("-d", "--delete"):
|
||||
@ -219,6 +232,12 @@ If no options are given, {0} will run in windowed mode
|
||||
self.save_tags = True
|
||||
if o in ("-r", "--rename"):
|
||||
self.rename_file = True
|
||||
if o in ("-e", "--export_to_zip"):
|
||||
self.export_to_zip = True
|
||||
if o == "--delete-rar":
|
||||
self.delete_rar_after_export = True
|
||||
if o == "--abort-on-conflict":
|
||||
self.abort_export_on_conflict = True
|
||||
if o in ("-f", "--parsefilename"):
|
||||
self.parse_filename = True
|
||||
if o == "--id":
|
||||
@ -231,10 +250,18 @@ If no options are given, {0} will run in windowed mode
|
||||
self.terse = True
|
||||
if o == "--nosummary":
|
||||
self.show_save_summary = False
|
||||
if o in ("-1", "--assume-issue-one"):
|
||||
self.assume_issue_is_one_if_not_set = True
|
||||
if o == "--nooverwrite":
|
||||
self.no_overwrite = True
|
||||
if o == "--version":
|
||||
print "ComicTagger version: ", ctversion.version
|
||||
print "ComicTagger {0}: Copyright (c) 2012-2013 Anthony Beville".format(ctversion.version)
|
||||
print "Distributed under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)"
|
||||
new_version = VersionChecker().getLatestVersion("", False)
|
||||
if new_version is not None:
|
||||
print "----------------------------------------"
|
||||
print "New version available online: {0}".format(new_version)
|
||||
print "----------------------------------------"
|
||||
sys.exit(0)
|
||||
if o in ("-t", "--type"):
|
||||
if a.lower() == "cr":
|
||||
@ -245,19 +272,56 @@ If no options are given, {0} will run in windowed mode
|
||||
self.data_style = MetaDataStyle.COMET
|
||||
else:
|
||||
self.display_msg_and_quit( "Invalid tag type", 1 )
|
||||
|
||||
if self.print_tags or self.delete_tags or self.save_tags or self.copy_tags or self.rename_file:
|
||||
|
||||
if self.print_tags or self.delete_tags or self.save_tags or self.copy_tags or self.rename_file or self.export_to_zip:
|
||||
self.no_gui = True
|
||||
|
||||
count = 0
|
||||
if self.run_script: count += 1
|
||||
if self.print_tags: count += 1
|
||||
if self.delete_tags: count += 1
|
||||
if self.save_tags: count += 1
|
||||
if self.copy_tags: count += 1
|
||||
if self.rename_file: count += 1
|
||||
if self.export_to_zip: count +=1
|
||||
|
||||
if count > 1:
|
||||
self.display_msg_and_quit( "Must choose only one action of print, delete, save, copy, or rename", 1 )
|
||||
self.display_msg_and_quit( "Must choose only one action of print, delete, save, copy, rename, export, or run script", 1 )
|
||||
|
||||
if self.script is not None:
|
||||
# we were given a script. special case for the args:
|
||||
# 1. ignore everthing before the -S,
|
||||
# 2. pass all the ones that follow (including script name) to the script
|
||||
script_args = list()
|
||||
for idx, arg in enumerate(sys.argv):
|
||||
if arg in [ '-S', '--script']:
|
||||
#found script!
|
||||
script_args = sys.argv[idx+1:]
|
||||
break
|
||||
sys.argv = script_args
|
||||
if not os.path.exists(self.script):
|
||||
print "Can't find {0}".format( self.script )
|
||||
else:
|
||||
# I *think* this makes sense:
|
||||
# assume the base name of the file is the module name
|
||||
# add the folder of the given file to the python path
|
||||
# import module
|
||||
dirname = os.path.dirname(self.script)
|
||||
module_name = os.path.splitext(os.path.basename(self.script))[0]
|
||||
sys.path = [dirname] + sys.path
|
||||
try:
|
||||
script = __import__(module_name)
|
||||
|
||||
# Determine if the entry point exists before trying to run it
|
||||
if "main" in dir(script):
|
||||
script.main()
|
||||
else:
|
||||
print "Can't find entry point \"main()\" in module \"{0}\"".format( module_name )
|
||||
except Exception as e:
|
||||
print "Script raised an unhandled exception: ", e
|
||||
print traceback.format_exc()
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
if len(args) > 0:
|
||||
if platform.system() == "Windows":
|
||||
@ -266,13 +330,14 @@ If no options are given, {0} will run in windowed mode
|
||||
self.file_list = []
|
||||
for item in args:
|
||||
self.file_list.extend(glob.glob(item))
|
||||
self.filename = self.file_list[0]
|
||||
if len(self.file_list) > 0:
|
||||
self.filename = self.file_list[0]
|
||||
else:
|
||||
self.filename = args[0]
|
||||
self.file_list = args
|
||||
|
||||
if self.no_gui and self.filename is None:
|
||||
self.display_msg_and_quit( "Command requires a filename!", 1 )
|
||||
self.display_msg_and_quit( "Command requires at least one filename!", 1 )
|
||||
|
||||
if self.delete_tags and self.data_style is None:
|
||||
self.display_msg_and_quit( "Please specify the type to delete with -t", 1 )
|
||||
@ -286,3 +351,5 @@ If no options are given, {0} will run in windowed mode
|
||||
#if self.rename_file and self.data_style is None:
|
||||
# self.display_msg_and_quit( "Please specify the type to use for renaming with -t", 1 )
|
||||
|
||||
if self.recursive:
|
||||
self.file_list = utils.get_recursive_filelist( self.file_list )
|
@ -18,27 +18,43 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import platform
|
||||
import sys
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
import os
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
from coverimagewidget import CoverImageWidget
|
||||
|
||||
class PageBrowserWindow(QtGui.QDialog):
|
||||
|
||||
def __init__(self, parent, metadata):
|
||||
super(PageBrowserWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'pagebrowser.ui' ), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('pagebrowser.ui' ), self)
|
||||
|
||||
self.pageWidget = CoverImageWidget( self.pageContainer, CoverImageWidget.ArchiveMode )
|
||||
gridlayout = QtGui.QGridLayout( self.pageContainer )
|
||||
gridlayout.addWidget( self.pageWidget )
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
self.pageWidget.showControls = False
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
self.lblPage.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
self.lblPage.setSizePolicy(QtGui.QSizePolicy.Ignored, QtGui.QSizePolicy.Ignored)
|
||||
self.comic_archive = None
|
||||
self.current_pixmap = None
|
||||
self.page_count = 0
|
||||
self.current_page_num = 0
|
||||
self.metadata = metadata
|
||||
|
||||
self.buttonBox.button(QtGui.QDialogButtonBox.Close).setDefault(True)
|
||||
if platform.system() == "Darwin":
|
||||
self.btnPrev.setText("<<")
|
||||
self.btnNext.setText(">>")
|
||||
else:
|
||||
self.btnPrev.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('left.png' )))
|
||||
self.btnNext.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('right.png')))
|
||||
|
||||
self.btnNext.clicked.connect( self.nextPage )
|
||||
self.btnPrev.clicked.connect( self.prevPage )
|
||||
self.show()
|
||||
@ -46,72 +62,49 @@ class PageBrowserWindow(QtGui.QDialog):
|
||||
self.btnNext.setEnabled( False )
|
||||
self.btnPrev.setEnabled( False )
|
||||
|
||||
def reset( self ):
|
||||
self.comic_archive = None
|
||||
self.page_count = 0
|
||||
self.current_page_num = 0
|
||||
self.metadata = None
|
||||
|
||||
self.btnNext.setEnabled( False )
|
||||
self.btnPrev.setEnabled( False )
|
||||
self.pageWidget.clear()
|
||||
|
||||
def setComicArchive(self, ca):
|
||||
|
||||
self.comic_archive = ca
|
||||
self.page_count = ca.getNumberOfPages()
|
||||
self.current_page_num = 0
|
||||
|
||||
self.pageWidget.setArchive( self.comic_archive )
|
||||
self.setPage()
|
||||
|
||||
if self.page_count > 1:
|
||||
self.btnNext.setEnabled( True )
|
||||
self.btnPrev.setEnabled( True )
|
||||
|
||||
def nextPage(self):
|
||||
|
||||
if self.current_page_num + 1 < self.page_count:
|
||||
self.current_page_num += 1
|
||||
else:
|
||||
self.current_page_num = 0
|
||||
self.setPage()
|
||||
|
||||
def prevPage(self):
|
||||
|
||||
if self.current_page_num - 1 >= 0:
|
||||
self.current_page_num -= 1
|
||||
else:
|
||||
self.current_page_num = self.page_count - 1
|
||||
self.setPage()
|
||||
|
||||
def setPage( self ):
|
||||
archive_page_index = self.metadata.getArchivePageIndex( self.current_page_num )
|
||||
image_data = self.comic_archive.getPage( archive_page_index )
|
||||
|
||||
if image_data is not None:
|
||||
self.setCurrentPixmap( image_data )
|
||||
self.setDisplayPixmap( 0, 0)
|
||||
if self.metadata is not None:
|
||||
archive_page_index = self.metadata.getArchivePageIndex( self.current_page_num )
|
||||
else:
|
||||
archive_page_index = self.current_page_num
|
||||
|
||||
self.pageWidget.setPage( archive_page_index )
|
||||
self.setWindowTitle("Page Browser - Page {0} (of {1}) ".format(self.current_page_num+1, self.page_count ) )
|
||||
|
||||
if self.current_page_num + 1 < self.page_count:
|
||||
self.btnNext.setEnabled( True )
|
||||
else:
|
||||
self.btnNext.setEnabled( False )
|
||||
|
||||
if self.current_page_num - 1 >= 0:
|
||||
self.btnPrev.setEnabled( True )
|
||||
else:
|
||||
self.btnPrev.setEnabled( False )
|
||||
|
||||
|
||||
def setCurrentPixmap( self, image_data ):
|
||||
if image_data is not None:
|
||||
img = QtGui.QImage()
|
||||
img.loadFromData( image_data )
|
||||
self.current_pixmap = QtGui.QPixmap(QtGui.QPixmap(img))
|
||||
|
||||
def resizeEvent( self, resize_event ):
|
||||
if self.current_pixmap is not None:
|
||||
delta_w = resize_event.size().width() - resize_event.oldSize().width()
|
||||
delta_h = resize_event.size().height() - resize_event.oldSize().height()
|
||||
|
||||
self.setDisplayPixmap( delta_w , delta_h )
|
||||
|
||||
def setDisplayPixmap( self, delta_w , delta_h ):
|
||||
# the deltas let us know what the new width and height of the label will be
|
||||
new_h = self.lblPage.height() + delta_h
|
||||
new_w = self.lblPage.width() + delta_w
|
||||
|
||||
if new_h < 0:
|
||||
new_h = 0;
|
||||
if new_w < 0:
|
||||
new_w = 0;
|
||||
scaled_pixmap = self.current_pixmap.scaled(new_w, new_h, QtCore.Qt.KeepAspectRatio)
|
||||
self.lblPage.setPixmap( scaled_pixmap )
|
||||
#QtCore.QCoreApplication.processEvents()
|
||||
|
||||
|
||||
|
||||
|
@ -26,8 +26,10 @@ from PyQt4 import uic
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
from genericmetadata import GenericMetadata, PageType
|
||||
from options import MetaDataStyle
|
||||
from comicarchive import MetaDataStyle
|
||||
from pageloader import PageLoader
|
||||
from coverimagewidget import CoverImageWidget
|
||||
|
||||
|
||||
def itemMoveEvents( widget ):
|
||||
|
||||
@ -76,11 +78,13 @@ class PageListEditor(QWidget):
|
||||
def __init__(self, parent ):
|
||||
super(PageListEditor, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'pagelisteditor.ui' ), self )
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('pagelisteditor.ui' ), self)
|
||||
|
||||
self.comic_archive = None
|
||||
self.pages_list = None
|
||||
self.page_loader = None
|
||||
self.pageWidget = CoverImageWidget( self.pageContainer, CoverImageWidget.ArchiveMode )
|
||||
gridlayout = QGridLayout( self.pageContainer )
|
||||
gridlayout.addWidget( self.pageWidget )
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
self.pageWidget.showControls = False
|
||||
|
||||
self.resetPage()
|
||||
|
||||
@ -107,8 +111,10 @@ class PageListEditor(QWidget):
|
||||
self.first_front_page = None
|
||||
|
||||
def resetPage( self ):
|
||||
self.current_pixmap = QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' ))
|
||||
self.setDisplayPixmap( 0, 0)
|
||||
self.pageWidget.clear()
|
||||
self.comboBox.setDisabled(True)
|
||||
self.comic_archive = None
|
||||
self.pages_list = None
|
||||
|
||||
def moveCurrentUp( self ):
|
||||
row = self.listWidget.currentRow()
|
||||
@ -157,18 +163,8 @@ class PageListEditor(QWidget):
|
||||
#idx = int(str (self.listWidget.item( row ).text()))
|
||||
idx = int(self.listWidget.item( row ).data(Qt.UserRole).toPyObject()[0]['Image'])
|
||||
|
||||
if self.page_loader is not None:
|
||||
self.page_loader.abandoned = True
|
||||
|
||||
if self.comic_archive is not None:
|
||||
self.page_loader = PageLoader( self.comic_archive, idx )
|
||||
self.page_loader.loadComplete.connect( self.actualChangePageImage )
|
||||
self.page_loader.start()
|
||||
|
||||
def actualChangePageImage( self, img ):
|
||||
self.page_loader = None
|
||||
self.current_pixmap = QPixmap(img)
|
||||
self.setDisplayPixmap( 0, 0)
|
||||
self.pageWidget.setArchive( self.comic_archive, idx )
|
||||
|
||||
def getFirstFrontCover( self ):
|
||||
frontCover = 0
|
||||
@ -203,44 +199,13 @@ class PageListEditor(QWidget):
|
||||
# wrap the dict in a tuple to keep from being converted to QStrings
|
||||
item.setData(Qt.UserRole, (page_dict,) )
|
||||
item.setText( self.listEntryText( page_dict ) )
|
||||
|
||||
|
||||
def resizeEvent( self, resize_event ):
|
||||
if self.current_pixmap is not None:
|
||||
delta_w = resize_event.size().width() - resize_event.oldSize().width()
|
||||
delta_h = resize_event.size().height() - resize_event.oldSize().height()
|
||||
self.setDisplayPixmap( delta_w , delta_h )
|
||||
|
||||
def setDisplayPixmap( self, delta_w , delta_h ):
|
||||
# the deltas let us know what the new width and height of the label will be
|
||||
new_h = self.frame.height() + delta_h
|
||||
new_w = self.frame.width() + delta_w
|
||||
|
||||
frame_w = new_w
|
||||
frame_h = new_h
|
||||
|
||||
new_h -= 4
|
||||
new_w -= 4
|
||||
|
||||
if new_h < 0:
|
||||
new_h = 0;
|
||||
if new_w < 0:
|
||||
new_w = 0;
|
||||
|
||||
# scale the pixmap to fit in the frame
|
||||
scaled_pixmap = self.current_pixmap.scaled(new_w, new_h, Qt.KeepAspectRatio)
|
||||
self.label.setPixmap( scaled_pixmap )
|
||||
|
||||
# ,pve and resize the label to be centered in the fame
|
||||
img_w = scaled_pixmap.width()
|
||||
img_h = scaled_pixmap.height()
|
||||
self.label.resize( img_w, img_h )
|
||||
self.label.move( (frame_w - img_w)/2, (frame_h - img_h)/2 )
|
||||
|
||||
|
||||
def setData( self, comic_archive, pages_list ):
|
||||
self.comic_archive = comic_archive
|
||||
self.pages_list = pages_list
|
||||
if pages_list is not None and len(pages_list) > 0:
|
||||
self.comboBox.setDisabled(False)
|
||||
|
||||
self.listWidget.itemSelectionChanged.disconnect( self.changePage )
|
||||
|
||||
@ -256,7 +221,7 @@ class PageListEditor(QWidget):
|
||||
self.listWidget.setCurrentRow ( 0 )
|
||||
|
||||
def listEntryText(self, page_dict):
|
||||
text = page_dict['Image']
|
||||
text = str(int(page_dict['Image']) + 1)
|
||||
if 'Type' in page_dict:
|
||||
text += " (" + self.pageTypeNames[page_dict['Type']] + ")"
|
||||
return text
|
||||
@ -278,9 +243,9 @@ class PageListEditor(QWidget):
|
||||
# depending on the current data style, certain fields are disabled
|
||||
|
||||
inactive_color = QColor(255, 170, 150)
|
||||
active_palette = self.label.palette()
|
||||
active_palette = self.comboBox.palette()
|
||||
|
||||
inactive_palette3 = self.label.palette()
|
||||
inactive_palette3 = self.comboBox.palette()
|
||||
inactive_palette3.setColor(QPalette.Base, inactive_color)
|
||||
|
||||
|
||||
@ -302,8 +267,7 @@ class PageListEditor(QWidget):
|
||||
|
||||
elif data_style == MetaDataStyle.CoMet:
|
||||
pass
|
||||
|
||||
def showEvent( self, event ):
|
||||
# make sure to adjust the size and pos of the pixmap based on frame size
|
||||
self.setDisplayPixmap( 0,0 )
|
||||
|
||||
|
||||
# make sure combo is disabled when no list
|
||||
if self.comic_archive is None:
|
||||
self.comboBox.setEnabled( False )
|
@ -22,7 +22,7 @@ import sys
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
import os
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
import utils
|
||||
|
||||
class IDProgressWindow(QtGui.QDialog):
|
||||
|
||||
@ -30,14 +30,13 @@ class IDProgressWindow(QtGui.QDialog):
|
||||
def __init__(self, parent):
|
||||
super(IDProgressWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'progresswindow.ui' ), self)
|
||||
|
||||
# we can't specify relative font sizes in the UI designer, so
|
||||
# make font for scroll window a smidge smaller
|
||||
f = self.textEdit.font()
|
||||
if f.pointSize() > 10:
|
||||
f.setPointSize( f.pointSize() - 2 )
|
||||
self.textEdit.setFont( f )
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('progresswindow.ui' ), self)
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
utils.reduceWidgetFontSize( self.textEdit )
|
||||
|
||||
|
||||
|
@ -23,7 +23,7 @@ from PyQt4 import QtCore, QtGui, uic
|
||||
from settings import ComicTaggerSettings
|
||||
from settingswindow import SettingsWindow
|
||||
from filerenamer import FileRenamer
|
||||
from options import MetaDataStyle
|
||||
from comicarchive import MetaDataStyle
|
||||
|
||||
import os
|
||||
import utils
|
||||
@ -33,9 +33,13 @@ class RenameWindow(QtGui.QDialog):
|
||||
def __init__( self, parent, comic_archive_list, data_style, settings ):
|
||||
super(RenameWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'renamewindow.ui' ), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('renamewindow.ui' ), self)
|
||||
self.label.setText("Preview (based on {0} tags):".format(MetaDataStyle.name[data_style]))
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
self.settings = settings
|
||||
self.comic_archive_list = comic_archive_list
|
||||
self.data_style = data_style
|
@ -21,8 +21,10 @@ limitations under the License.
|
||||
#import sys
|
||||
import os
|
||||
import sys
|
||||
import ConfigParser
|
||||
import configparser
|
||||
import platform
|
||||
import codecs
|
||||
import uuid
|
||||
|
||||
import utils
|
||||
|
||||
@ -35,22 +37,42 @@ class ComicTaggerSettings:
|
||||
else:
|
||||
return os.path.join( os.path.expanduser('~') , '.ComicTagger')
|
||||
|
||||
frozen_win_exe_path = None
|
||||
|
||||
@staticmethod
|
||||
def baseDir():
|
||||
if platform.system() == "Darwin" and getattr(sys, 'frozen', None):
|
||||
return sys._MEIPASS
|
||||
if getattr(sys, 'frozen', None):
|
||||
if platform.system() == "Darwin":
|
||||
return sys._MEIPASS
|
||||
else: # Windows
|
||||
#Preserve this value, in case sys.argv gets changed importing a plugin script
|
||||
if ComicTaggerSettings.frozen_win_exe_path is None:
|
||||
ComicTaggerSettings.frozen_win_exe_path = os.path.dirname( os.path.abspath( sys.argv[0] ) )
|
||||
return ComicTaggerSettings.frozen_win_exe_path
|
||||
else:
|
||||
#print "ATB basename", os.path.dirname( os.path.abspath( sys.argv[0] ) )
|
||||
return os.path.dirname( os.path.abspath( sys.argv[0] ) )
|
||||
return os.path.dirname( os.path.abspath( __file__) )
|
||||
|
||||
@staticmethod
|
||||
def getGraphic( filename ):
|
||||
graphic_folder = os.path.join(ComicTaggerSettings.baseDir(), 'graphics')
|
||||
return os.path.join( graphic_folder, filename )
|
||||
|
||||
@staticmethod
|
||||
def getUIFile( filename ):
|
||||
ui_folder = os.path.join(ComicTaggerSettings.baseDir(), 'ui')
|
||||
return os.path.join( ui_folder, filename )
|
||||
|
||||
def setDefaultValues( self ):
|
||||
|
||||
# General Settings
|
||||
self.rar_exe_path = ""
|
||||
self.unrar_exe_path = ""
|
||||
self.allow_cbi_in_rar = True
|
||||
self.check_for_new_version = True
|
||||
self.send_usage_stats = False
|
||||
|
||||
# automatic settings
|
||||
self.install_id = uuid.uuid4().hex
|
||||
self.last_selected_save_data_style = 0
|
||||
self.last_selected_load_data_style = 0
|
||||
self.last_opened_folder = ""
|
||||
@ -60,14 +82,21 @@ class ComicTaggerSettings:
|
||||
self.last_main_window_y = 0
|
||||
self.last_form_side_width = -1
|
||||
self.last_list_side_width = -1
|
||||
self.last_filelist_sorted_column = -1
|
||||
self.last_filelist_sorted_order = 0
|
||||
|
||||
# identifier settings
|
||||
self.id_length_delta_thresh = 5
|
||||
self.id_publisher_blacklist = "Panini Comics, Abril, Scholastic Book Services, Editorial Televisa"
|
||||
self.id_publisher_blacklist = "Panini Comics, Abril, Planeta DeAgostini, Editorial Televisa"
|
||||
|
||||
# Show/ask dialog flags
|
||||
self.ask_about_cbi_in_rar = True
|
||||
self.show_disclaimer = True
|
||||
self.dont_notify_about_this_version = ""
|
||||
self.ask_about_usage_stats = True
|
||||
|
||||
#filename parsing settings
|
||||
self.parse_scan_info = True
|
||||
|
||||
# Comic Vine settings
|
||||
self.use_series_start_as_volume = False
|
||||
@ -95,7 +124,7 @@ class ComicTaggerSettings:
|
||||
self.folder = ""
|
||||
self.setDefaultValues()
|
||||
|
||||
self.config = ConfigParser.RawConfigParser()
|
||||
self.config = configparser.RawConfigParser()
|
||||
self.folder = ComicTaggerSettings.getSettingsFolder()
|
||||
|
||||
if not os.path.exists( self.folder ):
|
||||
@ -131,18 +160,35 @@ class ComicTaggerSettings:
|
||||
self.unrar_exe_path = utils.which("unrar")
|
||||
if self.unrar_exe_path != "":
|
||||
self.save()
|
||||
|
||||
# make sure unrar/rar program is now in the path for the UnRAR class to use
|
||||
utils.addtopath(os.path.dirname(self.unrar_exe_path))
|
||||
utils.addtopath(os.path.dirname(self.rar_exe_path))
|
||||
|
||||
def reset( self ):
|
||||
os.unlink( self.settings_file )
|
||||
self.__init__()
|
||||
|
||||
def load(self):
|
||||
|
||||
self.config.read( self.settings_file )
|
||||
|
||||
def readline_generator(f):
|
||||
line = f.readline()
|
||||
while line:
|
||||
yield line
|
||||
line = f.readline()
|
||||
|
||||
#self.config.readfp(codecs.open(self.settings_file, "r", "utf8"))
|
||||
self.config.read_file(readline_generator(codecs.open(self.settings_file, "r", "utf8")))
|
||||
|
||||
self.rar_exe_path = self.config.get( 'settings', 'rar_exe_path' )
|
||||
self.unrar_exe_path = self.config.get( 'settings', 'unrar_exe_path' )
|
||||
|
||||
if self.config.has_option('settings', 'check_for_new_version'):
|
||||
self.check_for_new_version = self.config.getboolean( 'settings', 'check_for_new_version' )
|
||||
if self.config.has_option('settings', 'send_usage_stats'):
|
||||
self.send_usage_stats = self.config.getboolean( 'settings', 'send_usage_stats' )
|
||||
|
||||
if self.config.has_option('auto', 'install_id'):
|
||||
self.install_id = self.config.get( 'auto', 'install_id' )
|
||||
if self.config.has_option('auto', 'last_selected_load_data_style'):
|
||||
self.last_selected_load_data_style = self.config.getint( 'auto', 'last_selected_load_data_style' )
|
||||
if self.config.has_option('auto', 'last_selected_save_data_style'):
|
||||
@ -161,17 +207,28 @@ class ComicTaggerSettings:
|
||||
self.last_form_side_width = self.config.getint( 'auto', 'last_form_side_width' )
|
||||
if self.config.has_option('auto', 'last_list_side_width'):
|
||||
self.last_list_side_width = self.config.getint( 'auto', 'last_list_side_width' )
|
||||
if self.config.has_option('auto', 'last_filelist_sorted_column'):
|
||||
self.last_filelist_sorted_column = self.config.getint( 'auto', 'last_filelist_sorted_column' )
|
||||
if self.config.has_option('auto', 'last_filelist_sorted_order'):
|
||||
self.last_filelist_sorted_order = self.config.getint( 'auto', 'last_filelist_sorted_order' )
|
||||
|
||||
if self.config.has_option('identifier', 'id_length_delta_thresh'):
|
||||
self.id_length_delta_thresh = self.config.getint( 'identifier', 'id_length_delta_thresh' )
|
||||
if self.config.has_option('identifier', 'id_publisher_blacklist'):
|
||||
self.id_publisher_blacklist = self.config.get( 'identifier', 'id_publisher_blacklist' )
|
||||
|
||||
if self.config.has_option('filenameparser', 'parse_scan_info'):
|
||||
self.parse_scan_info = self.config.getboolean( 'filenameparser', 'parse_scan_info' )
|
||||
|
||||
if self.config.has_option('dialogflags', 'ask_about_cbi_in_rar'):
|
||||
self.ask_about_cbi_in_rar = self.config.getboolean( 'dialogflags', 'ask_about_cbi_in_rar' )
|
||||
if self.config.has_option('dialogflags', 'show_disclaimer'):
|
||||
self.show_disclaimer = self.config.getboolean( 'dialogflags', 'show_disclaimer' )
|
||||
|
||||
if self.config.has_option('dialogflags', 'dont_notify_about_this_version'):
|
||||
self.dont_notify_about_this_version = self.config.get( 'dialogflags', 'dont_notify_about_this_version' )
|
||||
if self.config.has_option('dialogflags', 'ask_about_usage_stats'):
|
||||
self.ask_about_usage_stats = self.config.getboolean( 'dialogflags', 'ask_about_usage_stats' )
|
||||
|
||||
if self.config.has_option('comicvine', 'use_series_start_as_volume'):
|
||||
self.use_series_start_as_volume = self.config.getboolean( 'comicvine', 'use_series_start_as_volume' )
|
||||
|
||||
@ -205,13 +262,16 @@ class ComicTaggerSettings:
|
||||
|
||||
if not self.config.has_section( 'settings' ):
|
||||
self.config.add_section( 'settings' )
|
||||
|
||||
|
||||
self.config.set( 'settings', 'check_for_new_version', self.check_for_new_version )
|
||||
self.config.set( 'settings', 'rar_exe_path', self.rar_exe_path )
|
||||
self.config.set( 'settings', 'unrar_exe_path', self.unrar_exe_path )
|
||||
self.config.set( 'settings', 'send_usage_stats', self.send_usage_stats )
|
||||
|
||||
if not self.config.has_section( 'auto' ):
|
||||
self.config.add_section( 'auto' )
|
||||
|
||||
self.config.set( 'auto', 'install_id', self.install_id )
|
||||
self.config.set( 'auto', 'last_selected_load_data_style', self.last_selected_load_data_style )
|
||||
self.config.set( 'auto', 'last_selected_save_data_style', self.last_selected_save_data_style )
|
||||
self.config.set( 'auto', 'last_opened_folder', self.last_opened_folder )
|
||||
@ -221,6 +281,8 @@ class ComicTaggerSettings:
|
||||
self.config.set( 'auto', 'last_main_window_y', self.last_main_window_y )
|
||||
self.config.set( 'auto', 'last_form_side_width', self.last_form_side_width )
|
||||
self.config.set( 'auto', 'last_list_side_width', self.last_list_side_width )
|
||||
self.config.set( 'auto', 'last_filelist_sorted_column', self.last_filelist_sorted_column )
|
||||
self.config.set( 'auto', 'last_filelist_sorted_order', self.last_filelist_sorted_order )
|
||||
|
||||
if not self.config.has_section( 'identifier' ):
|
||||
self.config.add_section( 'identifier' )
|
||||
@ -233,7 +295,14 @@ class ComicTaggerSettings:
|
||||
|
||||
self.config.set( 'dialogflags', 'ask_about_cbi_in_rar', self.ask_about_cbi_in_rar )
|
||||
self.config.set( 'dialogflags', 'show_disclaimer', self.show_disclaimer )
|
||||
self.config.set( 'dialogflags', 'dont_notify_about_this_version', self.dont_notify_about_this_version )
|
||||
self.config.set( 'dialogflags', 'ask_about_usage_stats', self.ask_about_usage_stats )
|
||||
|
||||
if not self.config.has_section( 'filenameparser' ):
|
||||
self.config.add_section( 'filenameparser' )
|
||||
|
||||
self.config.set( 'filenameparser', 'parse_scan_info', self.parse_scan_info )
|
||||
|
||||
if not self.config.has_section( 'comicvine' ):
|
||||
self.config.add_section( 'comicvine' )
|
||||
|
||||
@ -259,6 +328,8 @@ class ComicTaggerSettings:
|
||||
self.config.set( 'rename', 'rename_use_smart_string_cleanup', self.rename_use_smart_string_cleanup )
|
||||
self.config.set( 'rename', 'rename_extension_based_on_archive', self.rename_extension_based_on_archive )
|
||||
|
||||
with open( self.settings_file, 'wb') as configfile:
|
||||
with codecs.open( self.settings_file, 'wb', 'utf8') as configfile:
|
||||
self.config.write(configfile)
|
||||
|
||||
#make sure the basedir is cached, in case we're on windows running a script from frozen binary
|
||||
ComicTaggerSettings.baseDir()
|
@ -54,10 +54,12 @@ class SettingsWindow(QtGui.QDialog):
|
||||
def __init__(self, parent, settings ):
|
||||
super(SettingsWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'settingswindow.ui' ), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('settingswindow.ui' ), self)
|
||||
|
||||
self.settings = settings
|
||||
|
||||
self.setWindowFlags(self.windowFlags() &
|
||||
~QtCore.Qt.WindowContextHelpButtonHint )
|
||||
|
||||
self.settings = settings
|
||||
self.name = "Settings"
|
||||
|
||||
if platform.system() == "Windows":
|
||||
@ -79,7 +81,7 @@ class SettingsWindow(QtGui.QDialog):
|
||||
|
||||
|
||||
nldtTip = (
|
||||
""" <html>The <b>Name Length Delta Threshold</b> is for eliminating automatic
|
||||
""" <html>The <b>Default Name Length Match Tolerance</b> is for eliminating automatic
|
||||
search matches that are too long compared to your series name search. The higher
|
||||
it is, the more likely to have a good match, but each search will take longer and
|
||||
use more bandwidth. Too low, and only the very closest lexical matches will be
|
||||
@ -117,6 +119,12 @@ class SettingsWindow(QtGui.QDialog):
|
||||
self.leNameLengthDeltaThresh.setText( str(self.settings.id_length_delta_thresh) )
|
||||
self.tePublisherBlacklist.setPlainText( self.settings.id_publisher_blacklist )
|
||||
|
||||
if self.settings.check_for_new_version:
|
||||
self.cbxCheckForNewVersion.setCheckState( QtCore.Qt.Checked)
|
||||
|
||||
if self.settings.parse_scan_info:
|
||||
self.cbxParseScanInfo.setCheckState( QtCore.Qt.Checked)
|
||||
|
||||
if self.settings.use_series_start_as_volume:
|
||||
self.cbxUseSeriesStartAsVolume.setCheckState( QtCore.Qt.Checked)
|
||||
|
||||
@ -151,18 +159,23 @@ class SettingsWindow(QtGui.QDialog):
|
||||
self.settings.rar_exe_path = str(self.leRarExePath.text())
|
||||
self.settings.unrar_exe_path = str(self.leUnrarExePath.text())
|
||||
|
||||
# make sure unrar program is now in the path for the UnRAR class
|
||||
# make sure unrar/rar program is now in the path for the UnRAR class
|
||||
utils.addtopath(os.path.dirname(self.settings.unrar_exe_path))
|
||||
utils.addtopath(os.path.dirname(self.settings.rar_exe_path))
|
||||
|
||||
if not str(self.leNameLengthDeltaThresh.text()).isdigit():
|
||||
self.leNameLengthDeltaThresh.setText("0")
|
||||
|
||||
if not str(self.leIssueNumPadding.text()).isdigit():
|
||||
self.leIssueNumPadding.setText("0")
|
||||
|
||||
self.settings.check_for_new_version = self.cbxCheckForNewVersion.isChecked()
|
||||
|
||||
self.settings.id_length_delta_thresh = int(self.leNameLengthDeltaThresh.text())
|
||||
self.settings.id_publisher_blacklist = str(self.tePublisherBlacklist.toPlainText())
|
||||
|
||||
|
||||
self.settings.parse_scan_info = self.cbxParseScanInfo.isChecked()
|
||||
|
||||
self.settings.use_series_start_as_volume = self.cbxUseSeriesStartAsVolume.isChecked()
|
||||
|
||||
self.settings.assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked()
|
||||
@ -182,7 +195,6 @@ class SettingsWindow(QtGui.QDialog):
|
||||
self.settings.save()
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
|
||||
def selectRar( self ):
|
||||
self.selectFile( self.leRarExePath, "RAR" )
|
||||
|
||||
@ -223,5 +235,5 @@ class SettingsWindow(QtGui.QDialog):
|
||||
control.setText( str(fileList[0]) )
|
||||
|
||||
def showRenameTab( self ):
|
||||
self.tabWidget.setCurrentIndex(4)
|
||||
self.tabWidget.setCurrentIndex(5)
|
||||
|
@ -1,6 +1,6 @@
|
||||
# coding=utf-8
|
||||
"""
|
||||
The main window of the comictagger app
|
||||
The main window of the ComicTagger app
|
||||
"""
|
||||
|
||||
"""
|
||||
@ -29,9 +29,10 @@ import os
|
||||
import pprint
|
||||
import json
|
||||
import webbrowser
|
||||
import re
|
||||
|
||||
from volumeselectionwindow import VolumeSelectionWindow
|
||||
from options import MetaDataStyle
|
||||
from comicarchive import MetaDataStyle
|
||||
from comicinfoxml import ComicInfoXml
|
||||
from genericmetadata import GenericMetadata
|
||||
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
@ -53,6 +54,9 @@ from issueidentifier import IssueIdentifier
|
||||
from autotagstartwindow import AutoTagStartWindow
|
||||
from autotagprogresswindow import AutoTagProgressWindow
|
||||
from autotagmatchwindow import AutoTagMatchWindow
|
||||
from coverimagewidget import CoverImageWidget
|
||||
from versionchecker import VersionChecker
|
||||
|
||||
import utils
|
||||
import ctversion
|
||||
|
||||
@ -61,6 +65,7 @@ class OnlineMatchResults():
|
||||
self.goodMatches = []
|
||||
self.noMatches = []
|
||||
self.multipleMatches = []
|
||||
self.lowConfidenceMatches = []
|
||||
self.writeFailures = []
|
||||
self.fetchDataFailures = []
|
||||
|
||||
@ -68,31 +73,6 @@ class MultipleMatch():
|
||||
def __init__( self, ca, match_list):
|
||||
self.ca = ca
|
||||
self.matches = match_list
|
||||
|
||||
# this reads the environment and inits the right locale
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
|
||||
# helper func to allow a label to be clickable
|
||||
def clickable(widget):
|
||||
|
||||
class Filter(QtCore.QObject):
|
||||
|
||||
dblclicked = pyqtSignal()
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
|
||||
if obj == widget:
|
||||
if event.type() == QtCore.QEvent.MouseButtonDblClick:
|
||||
self.dblclicked.emit()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
filter = Filter(widget)
|
||||
widget.installEventFilter(filter)
|
||||
return filter.dblclicked
|
||||
|
||||
|
||||
|
||||
class TaggerWindow( QtGui.QMainWindow):
|
||||
|
||||
@ -102,9 +82,14 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
def __init__(self, file_list, settings, parent = None):
|
||||
super(TaggerWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'taggerwindow.ui' ), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('taggerwindow.ui' ), self)
|
||||
self.settings = settings
|
||||
|
||||
|
||||
self.archiveCoverWidget = CoverImageWidget( self.coverImageContainer, CoverImageWidget.ArchiveMode )
|
||||
gridlayout = QtGui.QGridLayout( self.coverImageContainer )
|
||||
gridlayout.addWidget( self.archiveCoverWidget )
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
|
||||
self.pageListEditor = PageListEditor( self.tabPages )
|
||||
gridlayout = QtGui.QGridLayout( self.tabPages )
|
||||
gridlayout.addWidget( self.pageListEditor )
|
||||
@ -116,18 +101,8 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
|
||||
self.fileSelectionList.selectionChanged.connect( self.fileListSelectionChanged )
|
||||
self.fileSelectionList.listCleared.connect( self.fileListCleared )
|
||||
|
||||
# ATB: Disable the list...
|
||||
#self.splitter.setSizes([100,0])
|
||||
#self.splitter.setHandleWidth(0)
|
||||
#self.splitter.handle(1).setDisabled(True)
|
||||
|
||||
# This is ugly, and should probably be done in a resource file
|
||||
if platform.system() == "Linux":
|
||||
self.scrollAreaWidgetContents.resize( self.scrollAreaWidgetContents.width(), 630)
|
||||
#if platform.system() == "Darwin":
|
||||
# self.scrollAreaWidgetContents.resize( 550, self.scrollAreaWidgetContents.height())
|
||||
|
||||
self.fileSelectionList.setSorting(self.settings.last_filelist_sorted_column,
|
||||
self.settings.last_filelist_sorted_order)
|
||||
|
||||
# we can't specify relative font sizes in the UI designer, so
|
||||
# walk through all the lablels in the main form, and make them
|
||||
@ -139,14 +114,11 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
f.setPointSize( f.pointSize() - 2 )
|
||||
f.setItalic( True )
|
||||
child.setFont( f )
|
||||
|
||||
#---------------------------
|
||||
|
||||
self.scrollAreaWidgetContents.adjustSize()
|
||||
|
||||
self.setWindowIcon(QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/app.png' )))
|
||||
|
||||
self.lblCover.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
|
||||
self.setWindowIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('app.png')))
|
||||
|
||||
self.save_data_style = settings.last_selected_save_data_style
|
||||
self.load_data_style = settings.last_selected_load_data_style
|
||||
|
||||
@ -155,6 +127,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.statusBar()
|
||||
self.populateComboBoxes()
|
||||
|
||||
self.page_browser = None
|
||||
self.resetApp()
|
||||
|
||||
# set up some basic field validators
|
||||
@ -173,7 +146,13 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
|
||||
#TODO set up an RE validator for issueNum that allows
|
||||
# for all sorts of wacky things
|
||||
|
||||
|
||||
# tweak some control fonts
|
||||
utils.reduceWidgetFontSize( self.lblFilename, 1 )
|
||||
utils.reduceWidgetFontSize( self.lblArchiveType )
|
||||
utils.reduceWidgetFontSize( self.lblTagList )
|
||||
utils.reduceWidgetFontSize( self.lblPageCount )
|
||||
|
||||
#make sure some editable comboboxes don't take drop actions
|
||||
self.cbFormat.lineEdit().setAcceptDrops(False)
|
||||
self.cbMaturityRating.lineEdit().setAcceptDrops(False)
|
||||
@ -185,13 +164,11 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.btnAddCredit.clicked.connect(self.addCredit)
|
||||
self.btnRemoveCredit.clicked.connect(self.removeCredit)
|
||||
self.twCredits.cellDoubleClicked.connect(self.editCredit)
|
||||
clickable(self.lblCover).connect(self.showPageBrowser)
|
||||
|
||||
self.connectDirtyFlagSignals()
|
||||
self.pageListEditor.modified.connect(self.setDirtyFlag)
|
||||
|
||||
self.pageListEditor.modified.connect(self.setDirtyFlag)
|
||||
self.pageListEditor.firstFrontCoverChanged.connect( self.frontCoverChanged )
|
||||
self.pageListEditor.listOrderChanged.connect( self.pageListOrderChanged )
|
||||
self.tabWidget.currentChanged.connect( self.tabChanged )
|
||||
|
||||
self.updateStyleTweaks()
|
||||
|
||||
@ -201,6 +178,9 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.splitter.setSizes([ self.settings.last_form_side_width , self.settings.last_list_side_width])
|
||||
self.raise_()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
self.resizeEvent( None )
|
||||
|
||||
self.splitter.splitterMoved.connect( self.splitterMovedEvent )
|
||||
|
||||
self.fileSelectionList.addAppAction( self.actionAutoIdentify )
|
||||
self.fileSelectionList.addAppAction( self.actionAutoTag )
|
||||
@ -226,26 +206,40 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
"""
|
||||
)
|
||||
self.settings.show_disclaimer = not checked
|
||||
|
||||
|
||||
if self.settings.ask_about_usage_stats:
|
||||
reply = QtGui.QMessageBox.question(self,
|
||||
self.tr("Anonymous Stats"),
|
||||
self.tr(
|
||||
"Is it okay if ComicTagger occasionally sends some anonymous usage statistics? Nothing nefarious, "
|
||||
"just trying to get a better idea of how the app is being used.\n\nThanks for your support!"
|
||||
),
|
||||
QtGui.QMessageBox.Yes|QtGui.QMessageBox.Default, QtGui.QMessageBox.No )
|
||||
|
||||
if reply == QtGui.QMessageBox.Yes:
|
||||
self.settings.send_usage_stats = True
|
||||
self.settings.ask_about_usage_stats = False
|
||||
|
||||
self.checkLatestVersionOnline()
|
||||
|
||||
def sigint_handler(self, *args):
|
||||
# defer the actual close in the app loop thread
|
||||
QtCore.QTimer.singleShot(200, self.close)
|
||||
|
||||
def resetApp( self ):
|
||||
|
||||
self.lblCover.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
|
||||
self.archiveCoverWidget.clear()
|
||||
self.comic_archive = None
|
||||
self.dirtyFlag = False
|
||||
self.clearForm()
|
||||
self.pageListEditor.resetPage()
|
||||
|
||||
if self.page_browser is not None:
|
||||
self.page_browser.reset()
|
||||
self.updateAppTitle()
|
||||
self.updateMenus()
|
||||
self.updateInfoBox()
|
||||
|
||||
self.droppedFile = None
|
||||
self.page_browser = None
|
||||
self.page_loader = None
|
||||
|
||||
|
||||
@ -316,6 +310,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.actionRename.setStatusTip( 'Rename archive based on tags' )
|
||||
self.actionRename.triggered.connect( self.renameArchive )
|
||||
|
||||
self.actionSettings.setShortcut( 'Ctrl+Shift+S' )
|
||||
self.actionSettings.setStatusTip( 'Configure ComicTagger' )
|
||||
self.actionSettings.triggered.connect( self.showSettings )
|
||||
|
||||
@ -352,16 +347,16 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.actionComicTaggerForum.triggered.connect( self.showForum )
|
||||
|
||||
# ToolBar
|
||||
|
||||
self.actionLoad.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/open.png')) )
|
||||
self.actionLoadFolder.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/longbox.png')) )
|
||||
self.actionWrite_Tags.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/save.png')) )
|
||||
self.actionParse_Filename.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/parse.png')) )
|
||||
self.actionSearchOnline.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/search.png')) )
|
||||
self.actionAutoIdentify.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/auto.png')) )
|
||||
self.actionAutoTag.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/autotag.png')) )
|
||||
self.actionClearEntryForm.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/clear.png')) )
|
||||
self.actionPageBrowser.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/browse.png') ))
|
||||
|
||||
self.actionLoad.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('open.png')))
|
||||
self.actionLoadFolder.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('longbox.png')))
|
||||
self.actionWrite_Tags.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('save.png')))
|
||||
self.actionParse_Filename.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('parse.png')))
|
||||
self.actionSearchOnline.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('search.png')))
|
||||
self.actionAutoIdentify.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('auto.png')))
|
||||
self.actionAutoTag.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('autotag.png')))
|
||||
self.actionClearEntryForm.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('clear.png')))
|
||||
self.actionPageBrowser.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('browse.png')))
|
||||
|
||||
self.toolBar.addAction( self.actionLoad )
|
||||
self.toolBar.addAction( self.actionLoadFolder )
|
||||
@ -390,18 +385,22 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
if rar_count != 0:
|
||||
dlg = ExportWindow( self, self.settings,
|
||||
self.tr("You have selected {0} archive(s) to export to Zip format. New archives will be created in the same folder as the original.\n\nPlease choose options below, and select OK.\n".format(rar_count) ))
|
||||
dlg.adjustSize( )
|
||||
dlg.setModal( True )
|
||||
if not dlg.exec_():
|
||||
return
|
||||
|
||||
progdialog = QtGui.QProgressDialog("", "Cancel", 0, rar_count, self)
|
||||
progdialog.setWindowTitle( "Exporting as ZIP" )
|
||||
progdialog.setWindowModality(QtCore.Qt.WindowModal)
|
||||
progdialog.setWindowModality(QtCore.Qt.ApplicationModal)
|
||||
progdialog.show()
|
||||
prog_idx = 0
|
||||
|
||||
new_archives_to_add = []
|
||||
archives_to_remove = []
|
||||
skipped_list = []
|
||||
failed_list = []
|
||||
success_count = 0
|
||||
|
||||
for ca in ca_list:
|
||||
if ca.isRar():
|
||||
@ -411,6 +410,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
progdialog.setValue(prog_idx)
|
||||
prog_idx += 1
|
||||
progdialog.setLabelText( ca.path )
|
||||
utils.centerWindowOnParent( progdialog )
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
original_path = os.path.abspath( ca.path )
|
||||
@ -419,11 +419,13 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
if os.path.lexists( export_name ):
|
||||
if dlg.fileConflictBehavior == ExportConflictOpts.dontCreate:
|
||||
export_name = None
|
||||
skipped_list.append( ca.path )
|
||||
elif dlg.fileConflictBehavior == ExportConflictOpts.createUnique:
|
||||
export_name = utils.unique_file( export_name )
|
||||
|
||||
if export_name is not None:
|
||||
if ca.exportAsZip( export_name ):
|
||||
success_count += 1
|
||||
if dlg.addToList:
|
||||
new_archives_to_add.append( export_name )
|
||||
if dlg.deleteOriginal:
|
||||
@ -431,15 +433,31 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
os.unlink( ca.path )
|
||||
|
||||
else:
|
||||
QtGui.QMessageBox.information(self, self.tr("Export as Zip Archive"),
|
||||
self.tr("An error occured while exporting {0} to {1}. Batch operation aborted!".format(original_path, export_name)))
|
||||
break
|
||||
# last export failed, so remove the zip, if it exists
|
||||
failed_list.append( ca.path )
|
||||
if os.path.lexists( export_name ):
|
||||
os.remove( export_name )
|
||||
|
||||
progdialog.close()
|
||||
|
||||
self.fileSelectionList.addPathList( new_archives_to_add )
|
||||
self.fileSelectionList.removeArchiveList( archives_to_remove )
|
||||
# ATB maybe show a summary of files created here...?
|
||||
|
||||
summary = u"Successfully created {0} Zip archive(s).".format( success_count )
|
||||
if len( skipped_list ) > 0:
|
||||
summary += u"\n\nThe following {0} RAR archive(s) were skipped due to file name conflicts:\n".format( len( skipped_list ) )
|
||||
for f in skipped_list:
|
||||
summary += u"\t{0}\n".format( f )
|
||||
if len( failed_list ) > 0:
|
||||
summary += u"\n\nThe following {0} RAR archive(s) failed to export due to read/write errors:\n".format( len( failed_list ) )
|
||||
for f in failed_list:
|
||||
summary += u"\t{0}\n".format( f )
|
||||
|
||||
dlg = LogWindow( self )
|
||||
dlg.setText( summary )
|
||||
dlg.setWindowTitle( "Archive Export to Zip Summary" )
|
||||
dlg.exec_()
|
||||
|
||||
|
||||
def aboutApp( self ):
|
||||
|
||||
@ -451,7 +469,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
msgBox = QtGui.QMessageBox()
|
||||
msgBox.setWindowTitle( self.tr("About " + self.appName ) )
|
||||
msgBox.setTextFormat( QtCore.Qt.RichText )
|
||||
msgBox.setIconPixmap( QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/about.png' )) )
|
||||
msgBox.setIconPixmap( QtGui.QPixmap(ComicTaggerSettings.getGraphic('about.png')) )
|
||||
msgBox.setText( "<br><br><br>"
|
||||
+ self.appName + " v" + self.version + "<br>"
|
||||
+ "(c)2012 Anthony Beville<br><br>"
|
||||
@ -485,6 +503,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
def actualLoadCurrentArchive( self ):
|
||||
if self.metadata.isEmpty:
|
||||
self.metadata = self.comic_archive.metadataFromFilename( )
|
||||
if len(self.metadata.pages) == 0:
|
||||
self.metadata.setDefaultPageList( self.comic_archive.getNumberOfPages() )
|
||||
|
||||
self.updateCoverImage()
|
||||
@ -498,21 +517,11 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.clearDirtyFlag() # also updates the app title
|
||||
self.updateInfoBox()
|
||||
self.updateMenus()
|
||||
self.updateAppTitle()
|
||||
|
||||
def updateCoverImage( self ):
|
||||
if self.page_loader is not None:
|
||||
self.page_loader.abandoned = True
|
||||
|
||||
cover_idx = self.metadata.getCoverPageIndexList()[0]
|
||||
|
||||
self.page_loader = PageLoader( self.comic_archive, cover_idx )
|
||||
self.page_loader.loadComplete.connect( self.actualUpdateCoverImage )
|
||||
self.page_loader.start()
|
||||
|
||||
def actualUpdateCoverImage( self, img ):
|
||||
self.page_loader = None
|
||||
self.lblCover.setPixmap(QtGui.QPixmap(img))
|
||||
self.lblCover.setScaledContents(True)
|
||||
self.archiveCoverWidget.setArchive( self.comic_archive, cover_idx)
|
||||
|
||||
def updateMenus( self ):
|
||||
|
||||
@ -569,7 +578,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
|
||||
filename = os.path.basename( ca.path )
|
||||
filename = os.path.splitext(filename)[0]
|
||||
filename = FileNameParser().fixSpaces(filename)
|
||||
filename = FileNameParser().fixSpaces(filename, False)
|
||||
|
||||
self.lblFilename.setText( filename )
|
||||
|
||||
@ -754,10 +763,13 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
item_text = role
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
self.twCredits.setItem(row, 1, item)
|
||||
|
||||
|
||||
item_text = name
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twCredits.setItem(row, 2, item)
|
||||
|
||||
@ -903,7 +915,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
|
||||
def queryOnline(self, autoselect=False):
|
||||
|
||||
issue_number = str(self.leIssueNum.text()).strip()
|
||||
issue_number = unicode(self.leIssueNum.text()).strip()
|
||||
|
||||
if autoselect and issue_number == "":
|
||||
QtGui.QMessageBox.information(self,"Automatic Identify Search", "Can't auto-identify without an issue number (yet!)")
|
||||
@ -959,25 +971,6 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
def commitMetadata(self):
|
||||
|
||||
if ( self.metadata is not None and self.comic_archive is not None):
|
||||
|
||||
if self.comic_archive.isRar() and self.save_data_style == MetaDataStyle.CBI:
|
||||
if self.settings.ask_about_cbi_in_rar:
|
||||
checked = OptionalMessageDialog.msg( self, "RAR and ComicBookLover",
|
||||
"""
|
||||
You are about to write a CBL tag block to a RAR archive!
|
||||
While technically possible, no known reader can read those tags from RAR
|
||||
yet.<br><br>
|
||||
If you would like this feature in the ComicBookLover apps, please go to their
|
||||
forums and add your voice to a feature request!
|
||||
<a href=http://forums.comicbooklover.com/categories/ipad-features>
|
||||
http://forums.comicbooklover.com/categories/ipad-features</a><br>
|
||||
<a href=http://forums.comicbooklover.com/categories/mac-features>
|
||||
http://forums.comicbooklover.com/categories/mac-features</a>
|
||||
""",
|
||||
)
|
||||
self.settings.ask_about_cbi_in_rar = not checked
|
||||
|
||||
|
||||
reply = QtGui.QMessageBox.question(self,
|
||||
self.tr("Save Tags"),
|
||||
self.tr("Are you sure you wish to save " + MetaDataStyle.name[self.save_data_style] + " tags to this archive?"),
|
||||
@ -988,6 +981,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.formToMetadata()
|
||||
|
||||
success = self.comic_archive.writeMetadata( self.metadata, self.save_data_style )
|
||||
self.comic_archive.loadCache( [ MetaDataStyle.CBI, MetaDataStyle.CIX ] )
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
|
||||
if not success:
|
||||
@ -1384,10 +1378,12 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
if reply == QtGui.QMessageBox.Yes:
|
||||
progdialog = QtGui.QProgressDialog("", "Cancel", 0, has_md_count, self)
|
||||
progdialog.setWindowTitle( "Removing Tags" )
|
||||
progdialog.setWindowModality(QtCore.Qt.WindowModal)
|
||||
progdialog.setWindowModality(QtCore.Qt.ApplicationModal)
|
||||
progdialog.show()
|
||||
prog_idx = 0
|
||||
|
||||
failed_list = []
|
||||
success_count = 0
|
||||
for ca in ca_list:
|
||||
if ca.hasMetadata( style ):
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
@ -1396,21 +1392,32 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
progdialog.setValue(prog_idx)
|
||||
prog_idx += 1
|
||||
progdialog.setLabelText( ca.path )
|
||||
utils.centerWindowOnParent( progdialog )
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
if ca.hasMetadata( style ) and ca.isWritable():
|
||||
if not ca.removeMetadata( style ):
|
||||
if has_md_count == 1:
|
||||
QtGui.QMessageBox.warning(self, self.tr("Remove failed"), self.tr("The tag removal operation seemed to fail!"))
|
||||
else:
|
||||
QtGui.QMessageBox.warning(self, self.tr("Remove failed"),
|
||||
self.tr("The tag removal operation seemed to fail for {0} Operation aborted!".format(ca.path)))
|
||||
break
|
||||
failed_list.append( ca.path )
|
||||
else:
|
||||
success_count += 1
|
||||
ca.loadCache( [ MetaDataStyle.CBI, MetaDataStyle.CIX ] )
|
||||
|
||||
progdialog.close()
|
||||
self.fileSelectionList.updateSelectedRows()
|
||||
self.updateInfoBox()
|
||||
self.updateMenus()
|
||||
|
||||
summary = u"Successfully removed tags in {0} archive(s).".format( success_count )
|
||||
if len( failed_list ) > 0:
|
||||
summary += u"\n\nThe remove operation failed in the following {0} archive(s):\n".format( len( failed_list ) )
|
||||
for f in failed_list:
|
||||
summary += u"\t{0}\n".format( f )
|
||||
|
||||
dlg = LogWindow( self )
|
||||
dlg.setText( summary )
|
||||
dlg.setWindowTitle( "Tag Remove Summary" )
|
||||
#dlg.adjustSize()
|
||||
dlg.exec_()
|
||||
|
||||
def copyTags( self ):
|
||||
# copy the indicated tags in the archive
|
||||
@ -1448,10 +1455,12 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
if reply == QtGui.QMessageBox.Yes:
|
||||
progdialog = QtGui.QProgressDialog("", "Cancel", 0, has_src_count, self)
|
||||
progdialog.setWindowTitle( "Copying Tags" )
|
||||
progdialog.setWindowModality(QtCore.Qt.WindowModal)
|
||||
progdialog.setWindowModality(QtCore.Qt.ApplicationModal)
|
||||
progdialog.show()
|
||||
prog_idx = 0
|
||||
|
||||
failed_list = []
|
||||
success_count = 0
|
||||
for ca in ca_list:
|
||||
if ca.hasMetadata( src_style ):
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
@ -1460,6 +1469,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
progdialog.setValue(prog_idx)
|
||||
prog_idx += 1
|
||||
progdialog.setLabelText( ca.path )
|
||||
utils.centerWindowOnParent( progdialog )
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
if ca.hasMetadata( src_style ) and ca.isWritable():
|
||||
@ -1469,16 +1479,27 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
md = CBLTransformer( md, self.settings ).apply()
|
||||
|
||||
if not ca.writeMetadata( md, dest_style ):
|
||||
if has_md_count == 1:
|
||||
QtGui.QMessageBox.warning(self, self.tr("Remove failed"), self.tr("The tag removal operation seemed to fail!"))
|
||||
else:
|
||||
QtGui.QMessageBox.warning(self, self.tr("Remove failed"),
|
||||
self.tr("The tag removal operation seemed to fail for {0} Operation aborted!".format(ca.path)))
|
||||
break
|
||||
failed_list.append( ca.path )
|
||||
else:
|
||||
success_count += 1
|
||||
|
||||
ca.loadCache( [ MetaDataStyle.CBI, MetaDataStyle.CIX ] )
|
||||
|
||||
progdialog.close()
|
||||
self.fileSelectionList.updateSelectedRows()
|
||||
self.updateInfoBox()
|
||||
self.updateMenus()
|
||||
|
||||
summary = u"Successfully copied tags in {0} archive(s).".format( success_count )
|
||||
if len( failed_list ) > 0:
|
||||
summary += u"\n\nThe copy operation failed in the following {0} archive(s):\n".format( len( failed_list ) )
|
||||
for f in failed_list:
|
||||
summary += u"\t{0}\n".format( f )
|
||||
|
||||
dlg = LogWindow( self )
|
||||
dlg.setText( summary )
|
||||
dlg.setWindowTitle( "Tag Copy Summary" )
|
||||
dlg.exec_()
|
||||
|
||||
def actualIssueDataFetch( self, match ):
|
||||
|
||||
@ -1515,8 +1536,15 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
# read in metadata, and parse file name if not there
|
||||
md = ca.readMetadata( self.save_data_style )
|
||||
if md.isEmpty:
|
||||
md = ca.metadataFromFilename()
|
||||
|
||||
md = ca.metadataFromFilename()
|
||||
if dlg.ignoreLeadingDigitsInFilename and md.series is not None:
|
||||
#remove all leading numbers
|
||||
md.series = re.sub( "([\d.]*)(.*)", "\\2", md.series)
|
||||
|
||||
# use the dialog specified search string
|
||||
if dlg.searchString is not None:
|
||||
md.series = dlg.searchString
|
||||
|
||||
if md is None or md.isEmpty:
|
||||
print "!!!!No metadata given to search online with!"
|
||||
return False, match_results
|
||||
@ -1529,7 +1557,8 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
ii.onlyUseAdditionalMetaData = True
|
||||
ii.setOutputFunction( self.autoTagLog )
|
||||
ii.cover_page_index = md.getCoverPageIndexList()[0]
|
||||
ii.setCoverURLCallback( self.atprogdialog.setTestImage )
|
||||
ii.setCoverURLCallback( self.atprogdialog.setTestImage )
|
||||
ii.setNameLengthDeltaThreshold( dlg.nameLengthMatchTolerance )
|
||||
|
||||
matches = ii.search()
|
||||
|
||||
@ -1538,6 +1567,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
found_match = False
|
||||
choices = False
|
||||
low_confidence = False
|
||||
no_match = False
|
||||
|
||||
if result == ii.ResultNoMatches:
|
||||
pass
|
||||
@ -1555,26 +1585,29 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
choices = True
|
||||
|
||||
if choices:
|
||||
self.autoTagLog( "Online search: Multiple matches. Save aborted\n" )
|
||||
match_results.multipleMatches.append(MultipleMatch(ca,matches))
|
||||
if low_confidence:
|
||||
self.autoTagLog( "Online search: Multiple low-confidence matches. Save aborted\n" )
|
||||
match_results.lowConfidenceMatches.append(MultipleMatch(ca,matches))
|
||||
else:
|
||||
self.autoTagLog( "Online search: Multiple matches. Save aborted\n" )
|
||||
match_results.multipleMatches.append(MultipleMatch(ca,matches))
|
||||
elif low_confidence and not dlg.autoSaveOnLow:
|
||||
self.autoTagLog( "Online search: Low confidence match. Save aborted\n" )
|
||||
match_results.noMatches.append(ca.path)
|
||||
match_results.lowConfidenceMatches.append(MultipleMatch(ca,matches))
|
||||
elif not found_match:
|
||||
self.autoTagLog( "Online search: No match found. Save aborted\n" )
|
||||
match_results.noMatches.append(ca.path)
|
||||
else:
|
||||
# a single match!
|
||||
if low_confidence:
|
||||
self.autoTagLog( "Online search: Low confidence match, but saving anyways, as incdicated...\n" )
|
||||
self.autoTagLog( "Online search: Low confidence match, but saving anyways, as indicated...\n" )
|
||||
|
||||
# now get the particular issue data
|
||||
cv_md = self.actualIssueDataFetch( matches[0] )
|
||||
if cv_md is None:
|
||||
match_results.fetchDataFailures.append(ca.path)
|
||||
|
||||
if cv_md is not None:
|
||||
|
||||
if cv_md is not None:
|
||||
md.overlay( cv_md )
|
||||
|
||||
if not ca.writeMetadata( md, self.save_data_style ):
|
||||
@ -1583,7 +1616,8 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
else:
|
||||
match_results.goodMatches.append(ca.path)
|
||||
success = True
|
||||
self.autoTagLog( "Save complete!\n" )
|
||||
self.autoTagLog( "Save complete!\n" )
|
||||
ca.loadCache( [ MetaDataStyle.CBI, MetaDataStyle.CIX ] )
|
||||
|
||||
return success, match_results
|
||||
|
||||
@ -1601,7 +1635,8 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
|
||||
atstartdlg = AutoTagStartWindow( self, self.settings,
|
||||
self.tr("You have selected {0} archive(s) to automatically identify and write {1} tags to.\n\n".format(len(ca_list), MetaDataStyle.name[style]) +
|
||||
"Please choose options below, and select OK.\n" ))
|
||||
"Please choose options below, and select OK to Auto-Tag.\n" ))
|
||||
atstartdlg.adjustSize( )
|
||||
atstartdlg.setModal( True )
|
||||
if not atstartdlg.exec_():
|
||||
return
|
||||
@ -1614,10 +1649,11 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
|
||||
self.autoTagLog( u"========================================================================\n" )
|
||||
self.autoTagLog( u"Auto-Tagging Started for {0} items\n".format(len(ca_list)))
|
||||
|
||||
|
||||
prog_idx = 0
|
||||
|
||||
match_results = OnlineMatchResults()
|
||||
archives_to_remove = []
|
||||
for ca in ca_list:
|
||||
self.autoTagLog( u"============================================================\n" )
|
||||
self.autoTagLog( u"Auto-Tagging {0} of {1}\n".format(prog_idx+1, len(ca_list)))
|
||||
@ -1633,13 +1669,21 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.atprogdialog.progressBar.setValue( prog_idx )
|
||||
prog_idx += 1
|
||||
self.atprogdialog.label.setText( ca.path )
|
||||
utils.centerWindowOnParent( self.atprogdialog )
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
if ca.isWritable():
|
||||
success, match_results = self.identifyAndTagSingleArchive( ca, match_results, atstartdlg )
|
||||
|
||||
self.atprogdialog.close()
|
||||
if success and atstartdlg.removeAfterSuccess:
|
||||
archives_to_remove.append( ca )
|
||||
|
||||
self.atprogdialog.close()
|
||||
|
||||
if atstartdlg.removeAfterSuccess:
|
||||
self.fileSelectionList.removeArchiveList( archives_to_remove )
|
||||
self.fileSelectionList.updateSelectedRows()
|
||||
|
||||
self.loadArchive( self.fileSelectionList.getCurrentArchive() )
|
||||
self.atprogdialog = None
|
||||
|
||||
@ -1648,6 +1692,8 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
|
||||
if len ( match_results.multipleMatches ) > 0:
|
||||
summary += u"Archives with multiple matches: {0}\n".format( len(match_results.multipleMatches))
|
||||
if len ( match_results.lowConfidenceMatches ) > 0:
|
||||
summary += u"Archives with one or more low-confidence matches: {0}\n".format( len(match_results.lowConfidenceMatches))
|
||||
if len ( match_results.noMatches ) > 0:
|
||||
summary += u"Archives with no matches: {0}\n".format( len(match_results.noMatches))
|
||||
if len ( match_results.fetchDataFailures ) > 0:
|
||||
@ -1656,15 +1702,17 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
summary += u"Archives that failed due to file writing errors: {0}\n".format( len(match_results.writeFailures))
|
||||
|
||||
self.autoTagLog( summary )
|
||||
|
||||
if len ( match_results.multipleMatches ) > 0:
|
||||
summary += u"\n\nDo you want to manually select the ones with multiple matches now?"
|
||||
|
||||
sum_selectable = len ( match_results.multipleMatches ) + len(match_results.lowConfidenceMatches)
|
||||
if sum_selectable > 0:
|
||||
summary += u"\n\nDo you want to manually select the ones with multiple matches and/or low-confidence matches now?"
|
||||
|
||||
reply = QtGui.QMessageBox.question(self,
|
||||
self.tr(u"Auto-Tag Summary"),
|
||||
self.tr(summary),
|
||||
QtGui.QMessageBox.Yes, QtGui.QMessageBox.No )
|
||||
|
||||
|
||||
match_results.multipleMatches.extend( match_results.lowConfidenceMatches )
|
||||
if reply == QtGui.QMessageBox.Yes:
|
||||
matchdlg = AutoTagMatchWindow( self, match_results.multipleMatches, style, self.actualIssueDataFetch)
|
||||
matchdlg.setModal( True )
|
||||
@ -1701,6 +1749,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.settings.last_main_window_y = self.y()
|
||||
self.settings.last_form_side_width = self.splitter.sizes()[0]
|
||||
self.settings.last_list_side_width = self.splitter.sizes()[1]
|
||||
self.settings.last_filelist_sorted_column, self.settings.last_filelist_sorted_order = self.fileSelectionList.getSorting()
|
||||
self.settings.save()
|
||||
|
||||
|
||||
@ -1789,4 +1838,37 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.actualLoadCurrentArchive()
|
||||
|
||||
def fileListCleared( self ):
|
||||
self.resetApp()
|
||||
self.resetApp()
|
||||
|
||||
def splitterMovedEvent( self, w1, w2 ):
|
||||
scrollbar_w = 0
|
||||
if self.scrollArea.verticalScrollBar().isVisible():
|
||||
scrollbar_w = self.scrollArea.verticalScrollBar().width()
|
||||
|
||||
new_w = self.scrollArea.width() - scrollbar_w - 5
|
||||
self.scrollAreaWidgetContents.resize( new_w, self.scrollAreaWidgetContents.height())
|
||||
|
||||
def resizeEvent( self, ev ):
|
||||
self.splitterMovedEvent( 0, 0)
|
||||
|
||||
def tabChanged( self, idx ):
|
||||
if idx == 0:
|
||||
self.splitterMovedEvent( 0, 0)
|
||||
|
||||
def checkLatestVersionOnline( self ):
|
||||
self.versionChecker = VersionChecker()
|
||||
self.versionChecker.versionRequestComplete.connect( self.versionCheckComplete )
|
||||
self.versionChecker.asyncGetLatestVersion( self.settings.install_id, self.settings.send_usage_stats )
|
||||
|
||||
def versionCheckComplete( self, new_version ):
|
||||
if ( new_version != self.version and
|
||||
new_version != self.settings.dont_notify_about_this_version):
|
||||
website = "http://code.google.com/p/comictagger"
|
||||
checked = OptionalMessageDialog.msg( self, "New version available!",
|
||||
"New version ({0}) available!<br>(You are currently running {1})<br><br>".format( new_version, self.version) +
|
||||
"Visit <a href='{0}'>{0}</a> for more info.<br><br>".format(website),
|
||||
QtCore.Qt.Unchecked,
|
||||
"Don't tell me about this version again")
|
||||
if checked:
|
||||
self.settings.dont_notify_about_this_version = new_version
|
||||
|
174
comictaggerlib/ui/autotagmatchwindow.ui
Normal file
@ -0,0 +1,174 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>dialogMatchSelect</class>
|
||||
<widget class="QDialog" name="dialogMatchSelect">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>943</width>
|
||||
<height>467</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Select Match</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QWidget" name="archiveCoverContainer" native="true">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>350</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>350</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="childrenCollapsible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>7</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="rowCount">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="columnCount">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Series</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Publisher</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Date</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Title</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
<widget class="QTextEdit" name="teDescription">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>3</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="altCoverContainer" native="true">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>350</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>350</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>dialogMatchSelect</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>dialogMatchSelect</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
242
comictaggerlib/ui/autotagstartwindow.ui
Normal file
@ -0,0 +1,242 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>dialogExport</class>
|
||||
<widget class="QDialog" name="dialogExport">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::NonModal</enum>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>607</width>
|
||||
<height>319</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Auto-Tag</string>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetFixedSize</enum>
|
||||
</property>
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxSaveOnLowConfidence">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Save on low confidence match</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxDontUseYear">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Don't use publication year in indentification process</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxAssumeIssueOne">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>If no issue number, assume "1"</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxIgnoreLeadingDigitsInFilename">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Ignore leading (sequence) numbers in filename</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxRemoveAfterSuccess">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Remove archives from list after successful tagging</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxSpecifySearchString">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Specify series search string for all selected archives</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<widget class="QLineEdit" name="leSearchString">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="1">
|
||||
<widget class="QLineEdit" name="leNameLengthMatchTolerance">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>50</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0" colspan="2">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Adjust Name Length Match Tolerance:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>dialogExport</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>346</x>
|
||||
<y>187</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>277</x>
|
||||
<y>104</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>dialogExport</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>346</x>
|
||||
<y>187</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>277</x>
|
||||
<y>104</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
123
comictaggerlib/ui/coverimagewidget.ui
Normal file
@ -0,0 +1,123 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>coverImageWidget</class>
|
||||
<widget class="QWidget" name="coverImageWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>292</width>
|
||||
<height>353</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<property name="horizontalSpacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="verticalSpacing">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="1" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="btnLeft">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="btnRight">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QFrame" name="frame">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Raised</enum>
|
||||
</property>
|
||||
<widget class="QLabel" name="lblImage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>60</x>
|
||||
<y>50</y>
|
||||
<width>91</width>
|
||||
<height>61</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Double-click to expand</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
@ -9,18 +9,24 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>515</width>
|
||||
<height>307</height>
|
||||
<width>533</width>
|
||||
<height>202</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Export to Zip Archive</string>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
38
comictaggerlib/ui/imagepopup.ui
Normal file
@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QDialog" name="Form">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::ApplicationModal</enum>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>817</width>
|
||||
<height>455</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<property name="windowOpacity">
|
||||
<double>1.000000000000000</double>
|
||||
</property>
|
||||
<widget class="QLabel" name="lblImage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>300</x>
|
||||
<y>120</y>
|
||||
<width>66</width>
|
||||
<height>17</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
162
comictaggerlib/ui/issueselectionwindow.ui
Normal file
@ -0,0 +1,162 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>dialogIssueSelect</class>
|
||||
<widget class="QDialog" name="dialogIssueSelect">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>872</width>
|
||||
<height>550</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Select Issue</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="childrenCollapsible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>7</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="rowCount">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="columnCount">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Issue</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Date</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Title</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
<widget class="QTextEdit" name="teDescription">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>3</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="coverImageContainer" native="true">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>300</width>
|
||||
<height>450</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>300</width>
|
||||
<height>450</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>dialogIssueSelect</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>dialogIssueSelect</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
174
comictaggerlib/ui/matchselectionwindow.ui
Normal file
@ -0,0 +1,174 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>dialogMatchSelect</class>
|
||||
<widget class="QDialog" name="dialogMatchSelect">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>943</width>
|
||||
<height>467</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Select Match</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QWidget" name="archiveCoverContainer" native="true">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>350</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>350</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="childrenCollapsible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>7</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="rowCount">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="columnCount">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Series</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Publisher</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Date</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Title</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
<widget class="QTextEdit" name="teDescription">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>3</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="altCoverContainer" native="true">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>350</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>350</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>dialogMatchSelect</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>dialogMatchSelect</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>429</width>
|
||||
<height>637</height>
|
||||
<width>369</width>
|
||||
<height>582</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
@ -20,10 +20,31 @@
|
||||
<string>Page Browser</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<property name="horizontalSpacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="verticalSpacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="lblPage">
|
||||
<widget class="QWidget" name="pageContainer" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
@ -36,37 +57,47 @@
|
||||
<height>300</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Box</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Sunken</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>20</number>
|
||||
</property>
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetMaximumSize</enum>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="btnPrev">
|
||||
<property name="text">
|
||||
<string><<</string>
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
@ -81,10 +112,26 @@
|
||||
<item>
|
||||
<widget class="QPushButton" name="btnNext">
|
||||
<property name="text">
|
||||
<string>>></string>
|
||||
<string/>
|
||||
</property>
|
||||
<property name="autoDefault">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
@ -98,32 +98,13 @@
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QFrame" name="frame">
|
||||
<widget class="QWidget" name="pageContainer" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>90</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::StyledPanel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Sunken</enum>
|
||||
</property>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>10</y>
|
||||
<width>151</width>
|
||||
<height>141</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
@ -35,7 +35,7 @@
|
||||
<string/>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_4">
|
||||
<item row="0" column="0">
|
||||
<item row="1" column="0">
|
||||
<widget class="QPushButton" name="btnResetSettings">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
@ -48,7 +48,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<item row="1" column="1">
|
||||
<widget class="QLabel" name="lblDefaultSettings">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
@ -64,7 +64,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<item row="2" column="0">
|
||||
<widget class="QPushButton" name="btnClearCache">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
@ -77,7 +77,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<item row="2" column="1">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
@ -93,6 +93,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxCheckForNewVersion">
|
||||
<property name="text">
|
||||
<string>Check for new version on startup</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
@ -263,7 +270,7 @@
|
||||
<string/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Name Length Delta Threshold:</string>
|
||||
<string>Default Name Length Match Tolerance:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@ -275,6 +282,12 @@
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>50</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string/>
|
||||
</property>
|
||||
@ -301,6 +314,24 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_6">
|
||||
<attribute name="title">
|
||||
<string>Filename Parser</string>
|
||||
</attribute>
|
||||
<widget class="QCheckBox" name="cbxParseScanInfo">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>30</x>
|
||||
<y>30</y>
|
||||
<width>421</width>
|
||||
<height>25</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Parse Scan Info From Filename (Experimental)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_3">
|
||||
<attribute name="title">
|
||||
<string>Comic Vine</string>
|
||||
@ -443,7 +474,20 @@
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="leRenameTemplate">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>The template for the new filename. Accepts the following variables:</p><p>%series%<br/>%issue%<br/>%volume%<br/>%issuecount%<br/>%year%<br/>%month%<br/>%month_name%<br/>%publisher%<br/>%title%</p><p>Examples:</p><p><span style=" font-style:italic;">%series% %issue% (%year%)</span><br/><span style=" font-style:italic;">%series% #%issue% - %title%</span></p></body></html></string>
|
||||
<string><html><head/><body><p>The template for the new filename. Accepts the following variables:</p><p>%series%<br/>%issue%<br/>%volume%<br/>%issuecount%<br/>%year%<br/>%month%<br/>%month_name%<br/>%publisher%<br/>%title%<br/>
|
||||
%genre%<br/>
|
||||
%language_code%<br/>
|
||||
%criticalrating%<br/>
|
||||
%alternateseries%<br/>
|
||||
%alternatenumber%<br/>
|
||||
%alternatecount%<br/>
|
||||
%imprint%<br/>
|
||||
%format%<br/>
|
||||
%maturityrating%<br/>
|
||||
%storyarc%<br/>
|
||||
%seriesgroup%<br/>
|
||||
%scaninfo%
|
||||
</p><p>Examples:</p><p><span style=" font-style:italic;">%series% %issue% (%year%)</span><br/><span style=" font-style:italic;">%series% #%issue% - %title%</span></p></body></html></string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
@ -7,7 +7,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1096</width>
|
||||
<height>571</height>
|
||||
<height>621</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
@ -91,23 +91,26 @@
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>220</width>
|
||||
<width>230</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>220</width>
|
||||
<width>230</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::StyledPanel</enum>
|
||||
<enum>QFrame::Panel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Raised</enum>
|
||||
<enum>QFrame::Sunken</enum>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<property name="margin">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="lblFilename">
|
||||
<property name="sizePolicy">
|
||||
@ -202,7 +205,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="lblCover">
|
||||
<widget class="QWidget" name="coverImageContainer" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
@ -211,31 +214,16 @@
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>220</width>
|
||||
<height>330</height>
|
||||
<width>230</width>
|
||||
<height>380</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>220</width>
|
||||
<height>330</height>
|
||||
<width>230</width>
|
||||
<height>380</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Panel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Sunken</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
@ -301,13 +289,13 @@
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>-64</y>
|
||||
<y>0</y>
|
||||
<width>470</width>
|
||||
<height>520</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
@ -1107,7 +1095,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1096</width>
|
||||
<height>25</height>
|
||||
<height>22</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuComicTagger">
|
@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>801</width>
|
||||
<height>470</height>
|
||||
<width>849</width>
|
||||
<height>476</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@ -20,7 +20,7 @@
|
||||
<item row="0" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="labelThumbnail">
|
||||
<widget class="QWidget" name="imageContainer" native="true">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>300</width>
|
||||
@ -33,18 +33,6 @@
|
||||
<height>450</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Panel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Sunken</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
@ -54,24 +42,22 @@
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="childrenCollapsible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
<verstretch>7</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>250</height>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>9</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
@ -119,22 +105,17 @@
|
||||
</widget>
|
||||
<widget class="QTextEdit" name="teDetails">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Maximum">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
<verstretch>3</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>200</height>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>9</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
@ -149,7 +130,7 @@
|
||||
<item>
|
||||
<widget class="QPushButton" name="btnAutoSelect">
|
||||
<property name="text">
|
||||
<string>Auto-Select</string>
|
||||
<string>Auto-Identify</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
@ -19,9 +19,63 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import platform
|
||||
import locale
|
||||
import codecs
|
||||
|
||||
class UtilsVars:
|
||||
already_fixed_encoding = False
|
||||
|
||||
def get_actual_preferred_encoding():
|
||||
preferred_encoding = locale.getpreferredencoding()
|
||||
if getattr(sys, 'frozen', None) and platform.system() == "Darwin":
|
||||
preferred_encoding = "utf-8"
|
||||
return preferred_encoding
|
||||
|
||||
def fix_output_encoding( ):
|
||||
if not UtilsVars.already_fixed_encoding:
|
||||
# this reads the environment and inits the right locale
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
|
||||
# try to make stdout/stderr encodings happy for unicode printing
|
||||
preferred_encoding = get_actual_preferred_encoding()
|
||||
sys.stdout = codecs.getwriter(preferred_encoding)(sys.stdout)
|
||||
sys.stderr = codecs.getwriter(preferred_encoding)(sys.stderr)
|
||||
UtilsVars.already_fixed_encoding = True
|
||||
|
||||
def get_recursive_filelist( pathlist ):
|
||||
"""
|
||||
Get a recursive list of of all files under all path items in the list
|
||||
"""
|
||||
filename_encoding = sys.getfilesystemencoding()
|
||||
filelist = []
|
||||
for p in pathlist:
|
||||
# if path is a folder, walk it recursivly, and all files underneath
|
||||
if type(p) == str:
|
||||
#make sure string is unicode
|
||||
p = p.decode(filename_encoding) #, 'replace')
|
||||
elif type(p) != unicode:
|
||||
#it's probably a QString
|
||||
p = unicode(p)
|
||||
|
||||
if os.path.isdir( p ):
|
||||
for root,dirs,files in os.walk( p ):
|
||||
for f in files:
|
||||
if type(f) == str:
|
||||
#make sure string is unicode
|
||||
f = f.decode(filename_encoding, 'replace')
|
||||
elif type(f) != unicode:
|
||||
#it's probably a QString
|
||||
f = unicode(f)
|
||||
filelist.append(os.path.join(root,f))
|
||||
else:
|
||||
filelist.append(p)
|
||||
|
||||
return filelist
|
||||
|
||||
def listToString( l ):
|
||||
string = ""
|
||||
if l is not None:
|
||||
@ -31,10 +85,16 @@ def listToString( l ):
|
||||
string += item
|
||||
return string
|
||||
|
||||
def addtopath( dir ):
|
||||
# TODO only add if not there already
|
||||
if dir is not None and dir != "":
|
||||
os.environ['PATH'] = dir + os.pathsep + os.environ['PATH']
|
||||
def addtopath( dirname ):
|
||||
if dirname is not None and dirname != "":
|
||||
|
||||
# verify that path doesn't already contain the given dirname
|
||||
tmpdirname = re.escape(dirname)
|
||||
pattern = r"{sep}{dir}$|^{dir}{sep}|{sep}{dir}{sep}|^{dir}$".format( dir=tmpdirname, sep=os.pathsep)
|
||||
|
||||
match = re.search(pattern, os.environ['PATH'])
|
||||
if not match:
|
||||
os.environ['PATH'] = dirname + os.pathsep + os.environ['PATH']
|
||||
|
||||
# returns executable path, if it exists
|
||||
def which(program):
|
||||
@ -520,3 +580,49 @@ def getLanguageFromISO( iso ):
|
||||
else:
|
||||
return lang_dict[ iso ]
|
||||
|
||||
|
||||
try:
|
||||
from PyQt4 import QtGui
|
||||
qt_available = True
|
||||
except ImportError:
|
||||
qt_available = False
|
||||
|
||||
if qt_available:
|
||||
def reduceWidgetFontSize( widget , delta = 2):
|
||||
f = widget.font()
|
||||
if f.pointSize() > 10:
|
||||
f.setPointSize( f.pointSize() - delta )
|
||||
widget.setFont( f )
|
||||
|
||||
def centerWindowOnScreen( window ):
|
||||
"""
|
||||
Center the window on screen. This implemention will handle the window
|
||||
being resized or the screen resolution changing.
|
||||
"""
|
||||
# Get the current screens' dimensions...
|
||||
screen = QtGui.QDesktopWidget().screenGeometry()
|
||||
# ... and get this windows' dimensions
|
||||
mysize = window.geometry()
|
||||
# The horizontal position is calulated as screenwidth - windowwidth /2
|
||||
hpos = ( screen.width() - window.width() ) / 2
|
||||
# And vertical position the same, but with the height dimensions
|
||||
vpos = ( screen.height() - window.height() ) / 2
|
||||
# And the move call repositions the window
|
||||
window.move(hpos, vpos)
|
||||
|
||||
def centerWindowOnParent( window ):
|
||||
|
||||
top_level = window
|
||||
while top_level.parent() is not None:
|
||||
top_level = top_level.parent()
|
||||
|
||||
# Get the current screens' dimensions...
|
||||
main_window_size = top_level.geometry()
|
||||
# ... and get this windows' dimensions
|
||||
mysize = window.geometry()
|
||||
# The horizontal position is calulated as screenwidth - windowwidth /2
|
||||
hpos = ( main_window_size.width() - window.width() ) / 2
|
||||
# And vertical position the same, but with the height dimensions
|
||||
vpos = ( main_window_size.height() - window.height() ) / 2
|
||||
# And the move call repositions the window
|
||||
window.move(hpos + main_window_size.left(), vpos + main_window_size.top())
|
91
comictaggerlib/versionchecker.py
Normal file
@ -0,0 +1,91 @@
|
||||
"""
|
||||
Version checker
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2013 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import platform
|
||||
import urllib,urllib2
|
||||
import ctversion
|
||||
|
||||
try:
|
||||
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
||||
from PyQt4.QtCore import QUrl, pyqtSignal, QObject, QByteArray
|
||||
except ImportError:
|
||||
# No Qt, so define a few dummy QObjects to help us compile
|
||||
class QObject():
|
||||
def __init__(self,*args):
|
||||
pass
|
||||
class pyqtSignal():
|
||||
def __init__(self,*args):
|
||||
pass
|
||||
def emit(a,b,c):
|
||||
pass
|
||||
|
||||
class VersionChecker(QObject):
|
||||
|
||||
def getRequestUrl( self, uuid, use_stats ):
|
||||
|
||||
base_url = "http://comictagger1.appspot.com/latest"
|
||||
args = ""
|
||||
|
||||
if use_stats:
|
||||
if platform.system() == "Windows":
|
||||
plat = "win"
|
||||
elif platform.system() == "Linux":
|
||||
plat = "lin"
|
||||
elif platform.system() == "Darwin":
|
||||
plat = "mac"
|
||||
else:
|
||||
plat = "other"
|
||||
args = "?uuid={0}&platform={1}&version={2}".format(uuid, plat, ctversion.version)
|
||||
if not getattr(sys, 'frozen', None):
|
||||
args += "&src=T"
|
||||
|
||||
return base_url+args
|
||||
|
||||
def getLatestVersion( self, uuid, use_stats=True):
|
||||
|
||||
try:
|
||||
resp = urllib2.urlopen( self.getRequestUrl(uuid, use_stats ))
|
||||
new_version = resp.read()
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
if new_version is None or new_version == "":
|
||||
return None
|
||||
return new_version.strip()
|
||||
|
||||
versionRequestComplete = pyqtSignal( str )
|
||||
|
||||
def asyncGetLatestVersion( self, uuid, use_stats ):
|
||||
|
||||
url = self.getRequestUrl( uuid, use_stats )
|
||||
|
||||
self.nam = QNetworkAccessManager()
|
||||
self.nam.finished.connect( self.asyncGetLatestVersionComplete )
|
||||
self.nam.get(QNetworkRequest(QUrl(str(url))))
|
||||
|
||||
def asyncGetLatestVersionComplete( self, reply ):
|
||||
# read in the response
|
||||
new_version = str(reply.readAll())
|
||||
|
||||
if new_version is None or new_version == "":
|
||||
return
|
||||
|
||||
self.versionRequestComplete.emit( new_version.strip() )
|
@ -34,6 +34,8 @@ from imagefetcher import ImageFetcher
|
||||
from progresswindow import IDProgressWindow
|
||||
from settings import ComicTaggerSettings
|
||||
from matchselectionwindow import MatchSelectionWindow
|
||||
from coverimagewidget import CoverImageWidget
|
||||
import utils
|
||||
|
||||
class SearchThread( QtCore.QThread):
|
||||
|
||||
@ -88,8 +90,20 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
def __init__(self, parent, series_name, issue_number, year, cover_index_list, comic_archive, settings, autoselect=False):
|
||||
super(VolumeSelectionWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'volumeselectionwindow.ui' ), self)
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('volumeselectionwindow.ui' ), self)
|
||||
|
||||
self.imageWidget = CoverImageWidget( self.imageContainer, CoverImageWidget.URLMode )
|
||||
gridlayout = QtGui.QGridLayout( self.imageContainer )
|
||||
gridlayout.addWidget( self.imageWidget )
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
|
||||
utils.reduceWidgetFontSize( self.teDetails, 1 )
|
||||
utils.reduceWidgetFontSize( self.twList )
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
QtCore.Qt.WindowSystemMenuHint |
|
||||
QtCore.Qt.WindowMaximizeButtonHint)
|
||||
|
||||
self.settings = settings
|
||||
self.series_name = series_name
|
||||
self.issue_number = issue_number
|
||||
@ -108,7 +122,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
self.btnAutoSelect.clicked.connect(self.autoSelect)
|
||||
|
||||
self.updateButtons()
|
||||
self.performQuery()
|
||||
self.performQuery()
|
||||
self.twList.selectRow(0)
|
||||
|
||||
def updateButtons( self ):
|
||||
@ -150,7 +164,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
|
||||
self.ii.setAdditionalMetadata( md )
|
||||
self.ii.onlyUseAdditionalMetaData = True
|
||||
print self.cover_index_list
|
||||
|
||||
self.ii.cover_page_index = int(self.cover_index_list[0])
|
||||
|
||||
self.id_thread = IdentifyThread( self.ii )
|
||||
@ -180,54 +194,43 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
result = self.ii.search_result
|
||||
match_index = 0
|
||||
|
||||
found_match = False
|
||||
found_match = None
|
||||
choices = False
|
||||
if result == self.ii.ResultNoMatches:
|
||||
QtGui.QMessageBox.information(self,"Auto-Select Result", " No matches found :-(")
|
||||
elif result == self.ii.ResultFoundMatchButBadCoverScore:
|
||||
QtGui.QMessageBox.information(self,"Auto-Select Result", " Found a match, but cover doesn't seem the same. Verify before commiting!")
|
||||
found_match = True
|
||||
found_match = matches[0]
|
||||
elif result == self.ii.ResultFoundMatchButNotFirstPage :
|
||||
QtGui.QMessageBox.information(self,"Auto-Select Result", " Found a match, but not with the first page of the archive.")
|
||||
found_match = True
|
||||
found_match = matches[0]
|
||||
elif result == self.ii.ResultMultipleMatchesWithBadImageScores:
|
||||
QtGui.QMessageBox.information(self,"Auto-Select Result", " Found some possibilities, but no confidence. Proceed manually.")
|
||||
choices = True
|
||||
elif result == self.ii.ResultOneGoodMatch:
|
||||
found_match = True
|
||||
found_match = matches[0]
|
||||
elif result == self.ii.ResultMultipleGoodMatches:
|
||||
QtGui.QMessageBox.information(self,"Auto-Select Result", " Found multiple likely matches. Please select.")
|
||||
choices = True
|
||||
|
||||
if choices:
|
||||
selector = MatchSelectionWindow( self, matches )
|
||||
selector = MatchSelectionWindow( self, matches, self.comic_archive )
|
||||
selector.setModal(True)
|
||||
|
||||
title = self.series_name
|
||||
title += " #" + self.issue_number
|
||||
if self.year is not None:
|
||||
title += " (" + str(self.year) + ")"
|
||||
title += " - "
|
||||
|
||||
selector.setWindowTitle( title + "Select Match")
|
||||
selector.exec_()
|
||||
if selector.result():
|
||||
#we should now have a list index
|
||||
found_match = True
|
||||
match_index = selector.current_row
|
||||
found_match = selector.currentMatch()
|
||||
|
||||
if found_match:
|
||||
if found_match is not None:
|
||||
self.iddialog.accept()
|
||||
|
||||
self.volume_id = matches[match_index]['volume_id']
|
||||
self.issue_number = matches[match_index]['issue_number']
|
||||
self.volume_id = found_match['volume_id']
|
||||
self.issue_number = found_match['issue_number']
|
||||
self.selectByID()
|
||||
self.showIssues()
|
||||
|
||||
def showIssues( self ):
|
||||
selector = IssueSelectionWindow( self, self.settings, self.volume_id, self.issue_number )
|
||||
selector.setModal(True)
|
||||
|
||||
title = ""
|
||||
for record in self.cv_search_results:
|
||||
if record['id'] == self.volume_id:
|
||||
@ -237,6 +240,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
break
|
||||
|
||||
selector.setWindowTitle( title + "Select Issue")
|
||||
selector.setModal( True )
|
||||
selector.exec_()
|
||||
if selector.result():
|
||||
#we should now have a volume ID
|
||||
@ -304,23 +308,27 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
|
||||
item_text = record['name']
|
||||
item = QtGui.QTableWidgetItem( item_text )
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setData( QtCore.Qt.UserRole ,record['id'])
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, item)
|
||||
|
||||
item_text = str(record['start_year'])
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 1, item)
|
||||
|
||||
item_text = record['count_of_issues']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item.setData(QtCore.Qt.DisplayRole, record['count_of_issues'])
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 2, item)
|
||||
|
||||
if record['publisher'] is not None:
|
||||
item_text = record['publisher']['name']
|
||||
item.setData( QtCore.Qt.ToolTipRole, item_text )
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 3, item)
|
||||
@ -361,23 +369,9 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
# list selection was changed, update the info on the volume
|
||||
for record in self.cv_search_results:
|
||||
if record['id'] == self.volume_id:
|
||||
|
||||
self.teDetails.setText ( record['description'] )
|
||||
|
||||
self.labelThumbnail.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
|
||||
url = record['image']['super_url']
|
||||
self.fetcher = ImageFetcher( )
|
||||
self.fetcher.fetchComplete.connect(self.finishRequest)
|
||||
self.fetcher.fetch( url, user_data=record['id'] )
|
||||
|
||||
|
||||
def finishRequest(self, image_data, user_data):
|
||||
# called when the image is done loading
|
||||
img = QtGui.QImage()
|
||||
img.loadFromData( image_data )
|
||||
self.setCover( img )
|
||||
|
||||
|
||||
def setCover( self, img ):
|
||||
self.labelThumbnail.setPixmap(QtGui.QPixmap(img))
|
||||
if record['description'] is None:
|
||||
self.teDetails.setText ( "" )
|
||||
else:
|
||||
self.teDetails.setText ( record['description'] )
|
||||
self.imageWidget.setURL( record['image']['super_url'] )
|
||||
break
|
@ -1,403 +0,0 @@
|
||||
"""
|
||||
A python class to manage communication with Comic Vine's REST API
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
|
||||
import json
|
||||
from pprint import pprint
|
||||
import urllib2, urllib
|
||||
import math
|
||||
import re
|
||||
import datetime
|
||||
import ctversion
|
||||
import sys
|
||||
|
||||
try:
|
||||
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
||||
from PyQt4.QtCore import QUrl, pyqtSignal, QObject, QByteArray
|
||||
except ImportError:
|
||||
# No Qt, so define a few dummy QObjects to help us compile
|
||||
class QObject():
|
||||
def __init__(self,*args):
|
||||
pass
|
||||
class pyqtSignal():
|
||||
def __init__(self,*args):
|
||||
pass
|
||||
def emit(a,b,c):
|
||||
pass
|
||||
|
||||
import utils
|
||||
from settings import ComicTaggerSettings
|
||||
from comicvinecacher import ComicVineCacher
|
||||
from genericmetadata import GenericMetadata
|
||||
from issuestring import IssueString
|
||||
|
||||
|
||||
class ComicVineTalkerException(Exception):
|
||||
pass
|
||||
|
||||
class ComicVineTalker(QObject):
|
||||
|
||||
def __init__(self, api_key=""):
|
||||
QObject.__init__(self)
|
||||
|
||||
# key that is registered to comictagger
|
||||
self.api_key = '27431e6787042105bd3e47e169a624521f89f3a4'
|
||||
|
||||
self.log_func = None
|
||||
|
||||
def setLogFunc( self , log_func ):
|
||||
self.log_func = log_func
|
||||
|
||||
def writeLog( self , text ):
|
||||
if self.log_func is None:
|
||||
sys.stdout.write(text.encode( errors='replace') )
|
||||
sys.stdout.flush()
|
||||
else:
|
||||
self.log_func( text )
|
||||
|
||||
def testKey( self ):
|
||||
|
||||
test_url = "http://api.comicvine.com/issue/1/?api_key=" + self.api_key + "&format=json&field_list=name"
|
||||
resp = urllib2.urlopen( test_url )
|
||||
content = resp.read()
|
||||
|
||||
cv_response = json.loads( content )
|
||||
|
||||
# Bogus request, but if the key is wrong, you get error 100: "Invalid API Key"
|
||||
return cv_response[ 'status_code' ] != 100
|
||||
|
||||
def getUrlContent( self, url ):
|
||||
try:
|
||||
resp = urllib2.urlopen( url )
|
||||
return resp.read()
|
||||
except Exception as e:
|
||||
self.writeLog( str(e) )
|
||||
raise ComicVineTalkerException("Network Error!")
|
||||
|
||||
def searchForSeries( self, series_name , callback=None, refresh_cache=False ):
|
||||
|
||||
# remove cruft from the search string
|
||||
series_name = utils.removearticles( series_name ).lower().strip()
|
||||
|
||||
# before we search online, look in our cache, since we might have
|
||||
# done this same search recently
|
||||
cvc = ComicVineCacher( )
|
||||
if not refresh_cache:
|
||||
cached_search_results = cvc.get_search_results( series_name )
|
||||
|
||||
if len (cached_search_results) > 0:
|
||||
return cached_search_results
|
||||
|
||||
original_series_name = series_name
|
||||
|
||||
series_name = urllib.quote_plus(series_name.encode("utf-8"))
|
||||
#series_name = urllib.quote_plus(unicode(series_name))
|
||||
search_url = "http://api.comicvine.com/search/?api_key=" + self.api_key + "&format=json&resources=volume&query=" + series_name + "&field_list=name,id,start_year,publisher,image,description,count_of_issues&sort=start_year"
|
||||
|
||||
content = self.getUrlContent(search_url)
|
||||
|
||||
cv_response = json.loads(content)
|
||||
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
self.writeLog( "Comic Vine query failed with error: [{0}]. \n".format( cv_response[ 'error' ] ))
|
||||
return None
|
||||
|
||||
search_results = list()
|
||||
|
||||
# see http://api.comicvine.com/documentation/#handling_responses
|
||||
|
||||
limit = cv_response['limit']
|
||||
current_result_count = cv_response['number_of_page_results']
|
||||
total_result_count = cv_response['number_of_total_results']
|
||||
|
||||
if callback is None:
|
||||
self.writeLog( "Found {0} of {1} results\n".format( cv_response['number_of_page_results'], cv_response['number_of_total_results']))
|
||||
search_results.extend( cv_response['results'])
|
||||
offset = 0
|
||||
|
||||
if callback is not None:
|
||||
callback( current_result_count, total_result_count )
|
||||
|
||||
# see if we need to keep asking for more pages...
|
||||
while ( current_result_count < total_result_count ):
|
||||
if callback is None:
|
||||
self.writeLog("getting another page of results {0} of {1}...\n".format( current_result_count, total_result_count))
|
||||
offset += limit
|
||||
content = self.getUrlContent(search_url + "&offset="+str(offset))
|
||||
|
||||
cv_response = json.loads(content)
|
||||
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
self.writeLog( "Comic Vine query failed with error: [{0}]. \n".format( cv_response[ 'error' ] ))
|
||||
return None
|
||||
search_results.extend( cv_response['results'])
|
||||
current_result_count += cv_response['number_of_page_results']
|
||||
|
||||
if callback is not None:
|
||||
callback( current_result_count, total_result_count )
|
||||
|
||||
|
||||
#for record in search_results:
|
||||
# print( "{0}: {1} ({2})".format(record['id'], smart_str(record['name']) , record['start_year'] ) )
|
||||
# print( "{0}: {1} ({2})".format(record['id'], record['name'] , record['start_year'] ) )
|
||||
|
||||
#print "{0}: {1} ({2})".format(search_results['results'][0]['id'], smart_str(search_results['results'][0]['name']) , search_results['results'][0]['start_year'] )
|
||||
|
||||
# cache these search results
|
||||
cvc.add_search_results( original_series_name, search_results )
|
||||
|
||||
return search_results
|
||||
|
||||
def fetchVolumeData( self, series_id ):
|
||||
|
||||
# before we search online, look in our cache, since we might already
|
||||
# have this info
|
||||
cvc = ComicVineCacher( )
|
||||
cached_volume_result = cvc.get_volume_info( series_id )
|
||||
|
||||
if cached_volume_result is not None:
|
||||
return cached_volume_result
|
||||
|
||||
|
||||
volume_url = "http://api.comicvine.com/volume/" + str(series_id) + "/?api_key=" + self.api_key + "&format=json"
|
||||
|
||||
content = self.getUrlContent(volume_url)
|
||||
cv_response = json.loads(content)
|
||||
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
print ( "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ))
|
||||
return None
|
||||
|
||||
volume_results = cv_response['results']
|
||||
|
||||
cvc.add_volume_info( volume_results )
|
||||
|
||||
return volume_results
|
||||
|
||||
|
||||
def fetchIssueData( self, series_id, issue_number, settings ):
|
||||
|
||||
volume_results = self.fetchVolumeData( series_id )
|
||||
|
||||
found = False
|
||||
for record in volume_results['issues']:
|
||||
if IssueString(issue_number).asFloat() is None:
|
||||
issue_number = 1
|
||||
if float(record['issue_number']) == float(issue_number):
|
||||
found = True
|
||||
break
|
||||
|
||||
if (found):
|
||||
issue_url = "http://api.comicvine.com/issue/" + str(record['id']) + "/?api_key=" + self.api_key + "&format=json"
|
||||
|
||||
content = self.getUrlContent(issue_url)
|
||||
cv_response = json.loads(content)
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
print ( "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ))
|
||||
return None
|
||||
issue_results = cv_response['results']
|
||||
|
||||
else:
|
||||
return None
|
||||
|
||||
# now, map the comicvine data to generic metadata
|
||||
return self.mapCVDataToMetadata( volume_results, issue_results, settings )
|
||||
|
||||
def fetchIssueDataByIssueID( self, issue_id, settings ):
|
||||
|
||||
issue_url = "http://api.comicvine.com/issue/" + str(issue_id) + "/?api_key=" + self.api_key + "&format=json"
|
||||
content = self.getUrlContent(issue_url)
|
||||
cv_response = json.loads(content)
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
print ( "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ))
|
||||
return None
|
||||
|
||||
issue_results = cv_response['results']
|
||||
|
||||
volume_results = self.fetchVolumeData( issue_results['volume']['id'] )
|
||||
|
||||
# now, map the comicvine data to generic metadata
|
||||
md = self.mapCVDataToMetadata( volume_results, issue_results, settings )
|
||||
md.isEmpty = False
|
||||
return md
|
||||
|
||||
def mapCVDataToMetadata(self, volume_results, issue_results, settings ):
|
||||
|
||||
# now, map the comicvine data to generic metadata
|
||||
metadata = GenericMetadata()
|
||||
|
||||
metadata.series = issue_results['volume']['name']
|
||||
|
||||
num_s = IssueString(issue_results['issue_number']).asString()
|
||||
|
||||
metadata.issue = num_s
|
||||
metadata.title = issue_results['name']
|
||||
metadata.publisher = volume_results['publisher']['name']
|
||||
metadata.month = issue_results['publish_month']
|
||||
metadata.year = issue_results['publish_year']
|
||||
#metadata.issueCount = volume_results['count_of_issues']
|
||||
metadata.comments = self.cleanup_html(issue_results['description'])
|
||||
if settings.use_series_start_as_volume:
|
||||
metadata.volume = volume_results['start_year']
|
||||
|
||||
metadata.notes = "Tagged with ComicTagger {0} using info from Comic Vine on {1}. [Issue ID {2}]".format(
|
||||
ctversion.version,
|
||||
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
issue_results['id'])
|
||||
#metadata.notes += issue_results['site_detail_url']
|
||||
|
||||
metadata.webLink = issue_results['site_detail_url']
|
||||
|
||||
person_credits = issue_results['person_credits']
|
||||
for person in person_credits:
|
||||
for role in person['roles']:
|
||||
# can we determine 'primary' from CV??
|
||||
role_name = role['role'].title()
|
||||
metadata.addCredit( person['name'], role['role'].title(), False )
|
||||
|
||||
character_credits = issue_results['character_credits']
|
||||
character_list = list()
|
||||
for character in character_credits:
|
||||
character_list.append( character['name'] )
|
||||
metadata.characters = utils.listToString( character_list )
|
||||
|
||||
team_credits = issue_results['team_credits']
|
||||
team_list = list()
|
||||
for team in team_credits:
|
||||
team_list.append( team['name'] )
|
||||
metadata.teams = utils.listToString( team_list )
|
||||
|
||||
location_credits = issue_results['location_credits']
|
||||
location_list = list()
|
||||
for location in location_credits:
|
||||
location_list.append( location['name'] )
|
||||
metadata.locations = utils.listToString( location_list )
|
||||
|
||||
story_arc_credits = issue_results['story_arc_credits']
|
||||
arc_list = []
|
||||
for arc in story_arc_credits:
|
||||
arc_list.append(arc['name'])
|
||||
if len(arc_list) > 0:
|
||||
metadata.storyArc = utils.listToString(arc_list)
|
||||
|
||||
return metadata
|
||||
|
||||
def cleanup_html( self, string):
|
||||
|
||||
# remove all newlines first
|
||||
string = string.replace("\n", "")
|
||||
|
||||
#put in our own
|
||||
string = string.replace("<br>", "\n")
|
||||
string = string.replace("</p>", "\n\n")
|
||||
string = string.replace("<h4>", "*")
|
||||
string = string.replace("</h4>", "*\n")
|
||||
|
||||
# now strip all other tags
|
||||
p = re.compile(r'<[^<]*?>')
|
||||
newstring = p.sub('',string)
|
||||
|
||||
newstring = newstring.replace(' ',' ')
|
||||
newstring = newstring.replace('&','&')
|
||||
|
||||
newstring = newstring.strip()
|
||||
|
||||
|
||||
return newstring
|
||||
|
||||
def fetchIssueDate( self, issue_id ):
|
||||
image_url, thumb_url, month,year = self.fetchIssueSelectDetails( issue_id )
|
||||
return month, year
|
||||
|
||||
def fetchIssueCoverURLs( self, issue_id ):
|
||||
image_url, thumb_url, month,year = self.fetchIssueSelectDetails( issue_id )
|
||||
return image_url, thumb_url
|
||||
|
||||
def fetchIssueSelectDetails( self, issue_id ):
|
||||
|
||||
cached_image_url,cached_thumb_url,cached_month,cached_year = self.fetchCachedIssueSelectDetails( issue_id )
|
||||
if cached_image_url is not None:
|
||||
return cached_image_url,cached_thumb_url, cached_month, cached_year
|
||||
|
||||
issue_url = "http://api.comicvine.com/issue/" + str(issue_id) + "/?api_key=" + self.api_key + "&format=json&field_list=image,publish_month,publish_year"
|
||||
|
||||
content = self.getUrlContent(issue_url)
|
||||
|
||||
cv_response = json.loads(content)
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
print ( "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ))
|
||||
return None, None,None,None
|
||||
|
||||
image_url = cv_response['results']['image']['super_url']
|
||||
thumb_url = cv_response['results']['image']['thumb_url']
|
||||
year = cv_response['results']['publish_year']
|
||||
month = cv_response['results']['publish_month']
|
||||
|
||||
if image_url is not None:
|
||||
self.cacheIssueSelectDetails( issue_id, image_url,thumb_url, month, year )
|
||||
return image_url,thumb_url,month,year
|
||||
|
||||
def fetchCachedIssueSelectDetails( self, issue_id ):
|
||||
|
||||
# before we search online, look in our cache, since we might already
|
||||
# have this info
|
||||
cvc = ComicVineCacher( )
|
||||
return cvc.get_issue_select_details( issue_id )
|
||||
|
||||
def cacheIssueSelectDetails( self, issue_id, image_url, thumb_url, month, year ):
|
||||
cvc = ComicVineCacher( )
|
||||
cvc.add_issue_select_details( issue_id, image_url, thumb_url, month, year )
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
urlFetchComplete = pyqtSignal( str , str, int)
|
||||
|
||||
def asyncFetchIssueCoverURLs( self, issue_id ):
|
||||
|
||||
self.issue_id = issue_id
|
||||
cached_image_url,cached_thumb_url,month,year = self.fetchCachedIssueSelectDetails( issue_id )
|
||||
if cached_image_url is not None:
|
||||
self.urlFetchComplete.emit( cached_image_url,cached_thumb_url, self.issue_id )
|
||||
return
|
||||
|
||||
issue_url = "http://api.comicvine.com/issue/" + str(issue_id) + "/?api_key=" + self.api_key + "&format=json&field_list=image,publish_month,publish_year"
|
||||
self.nam = QNetworkAccessManager()
|
||||
self.nam.finished.connect( self.asyncFetchIssueCoverURLComplete )
|
||||
self.nam.get(QNetworkRequest(QUrl(issue_url)))
|
||||
|
||||
def asyncFetchIssueCoverURLComplete( self, reply ):
|
||||
|
||||
# read in the response
|
||||
data = reply.readAll()
|
||||
cv_response = json.loads(str(data))
|
||||
if cv_response[ 'status_code' ] != 1:
|
||||
print ( "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ))
|
||||
return
|
||||
|
||||
image_url = cv_response['results']['image']['super_url']
|
||||
thumb_url = cv_response['results']['image']['thumb_url']
|
||||
year = cv_response['results']['publish_year']
|
||||
month = cv_response['results']['publish_month']
|
||||
|
||||
self.cacheIssueSelectDetails( self.issue_id, image_url, thumb_url, month, year )
|
||||
|
||||
self.urlFetchComplete.emit( image_url, thumb_url, self.issue_id )
|
||||
|
||||
|
1
current_version.txt
Normal file
@ -0,0 +1 @@
|
||||
1.1.7-beta
|
11
google/gadgets/social.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<Module>
|
||||
<ModulePrefs title="mygaget" />
|
||||
<Content type="html">
|
||||
<![CDATA[
|
||||
<a href="https://twitter.com/ComicTagger" class="twitter-follow-button" data-show-count="false" data-size="large">Follow @ComicTagger</a>
|
||||
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
|
||||
<iframe allowtransparency="true" frameborder="0" scrolling="no" src="http://www.facebook.com/plugins/likebox.php?href=http%3A%2F%2Fwww.facebook.com%2Fpages%2FComictagger/139615369550787&width=292&colorscheme=light&show_faces =false&border_color&stream=false&header=false&height=62" style="background-color: white; border-bottom-style: none; border-color: initial; border-left-style: none; border-right-style: none; border-top-style: none; border-width: initial; color: #333333; font-family: Verdana; font-size: 12px; height: 62px; line-height: 19px; overflow-x: hidden; overflow-y: hidden; text-align: -webkit-auto; width: 292px;"></iframe>
|
||||
]]>
|
||||
</Content>
|
||||
</Module>
|