Compare commits
54 Commits
1.1.7-beta
...
1.1.12-bet
Author | SHA1 | Date | |
---|---|---|---|
3eca25db34 | |||
c8a5a89369 | |||
ff578ea819 | |||
1c730c25d5 | |||
35b7b39b86 | |||
719c711484 | |||
afbbc9d00c | |||
b8e0a45fc8 | |||
b7360dd33e | |||
d9f1956426 | |||
b5c7f36410 | |||
0b0663d935 | |||
eee1f65436 | |||
9a8d4149f2 | |||
b02a205668 | |||
57284dfbed | |||
afcbde7fc6 | |||
151fac5bf1 | |||
57c1efdab9 | |||
6b272cef87 | |||
1cdc732739 | |||
d1b00d162d | |||
3dd3980bc1 | |||
cbf475eb26 | |||
ac8b575659 | |||
ac8ef286a4 | |||
f567dc37be | |||
15c5fc5258 | |||
cc985b52a5 | |||
910b0386be | |||
0fece23405 | |||
eee320e0c7 | |||
accabf8e21 | |||
acc253d35c | |||
ede0154efe | |||
5b805b1428 | |||
2e6b2a89db | |||
c028bb4ddc | |||
b70beb5684 | |||
128af4521b | |||
43cf7a80c8 | |||
3223ed190c | |||
9e2817c037 | |||
6e7bd10fb9 | |||
c099205779 | |||
47d8da0e80 | |||
0f7e88e58c | |||
65902a15b1 | |||
a68b2babeb | |||
4098802e43 | |||
9c14258e9f | |||
33bdbe8be8 | |||
a76864c109 | |||
cb68d07751 |
8
Makefile
8
Makefile
@ -1,4 +1,4 @@
|
||||
TAGGER_BASE := $(HOME)/Dropbox/tagger/comictagger
|
||||
TAGGER_BASE ?= $(HOME)/Dropbox/tagger/comictagger
|
||||
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)
|
||||
@ -49,9 +49,9 @@ remove_test_install:
|
||||
# #-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"
|
||||
#$(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:
|
||||
|
@ -1,4 +1,4 @@
|
||||
ComicTagger is a multi-platform app for writing metadata to comic archives, written in Python and PyQt.
|
||||
ComicTagger is a multi-platform app for writing metadata to digital comics, written in Python and PyQt.
|
||||
|
||||
Features:
|
||||
|
||||
|
@ -33,7 +33,7 @@ similar to the C interface provided by UnRAR. There is also a
|
||||
higher level interface which makes some common operations easier.
|
||||
"""
|
||||
|
||||
__version__ = '0.99.2'
|
||||
__version__ = '0.99.3'
|
||||
|
||||
try:
|
||||
WindowsError
|
||||
|
@ -12,13 +12,11 @@ def cleanup(dir='test'):
|
||||
os.removedirs(os.path.join(path, dir))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# reuse RarArchive object, en
|
||||
# basic test
|
||||
cleanup()
|
||||
rarc = UnRAR2.RarFile('test.rar')
|
||||
rarc.infolist()
|
||||
assert rarc.comment == "This is a test."
|
||||
for info in rarc.infoiter():
|
||||
saveinfo = info
|
||||
assert (str(info)=="""<RarInfo "test" in "test.rar">""")
|
||||
@ -98,7 +96,8 @@ list(UnRAR2.RarFile('test_nulls.rar').infoiter())
|
||||
|
||||
# extract files from an archive with protected files
|
||||
cleanup()
|
||||
UnRAR2.RarFile('test_protected_files.rar', password="protected").extract()
|
||||
rarc = UnRAR2.RarFile('test_protected_files.rar', password="protected")
|
||||
rarc.extract()
|
||||
assert os.path.exists('test'+os.sep+'top_secret_xxx_file.txt')
|
||||
cleanup()
|
||||
errored = False
|
||||
|
@ -33,6 +33,7 @@ from rar_exceptions import *
|
||||
class UnpackerNotInstalled(Exception): pass
|
||||
|
||||
rar_executable_cached = None
|
||||
rar_executable_version = None
|
||||
|
||||
def call_unrar(params):
|
||||
"Calls rar/unrar command line executable, returns stdout pipe"
|
||||
@ -59,10 +60,10 @@ def call_unrar(params):
|
||||
class RarFileImplementation(object):
|
||||
|
||||
def init(self, password=None):
|
||||
global rar_executable_version
|
||||
self.password = password
|
||||
|
||||
|
||||
|
||||
stdoutdata, stderrdata = self.call('v', []).communicate()
|
||||
|
||||
for line in stderrdata.splitlines():
|
||||
@ -73,18 +74,42 @@ class RarFileImplementation(object):
|
||||
accum = []
|
||||
source = iter(stdoutdata.splitlines())
|
||||
line = ''
|
||||
while not (line.startswith('Comment:') or line.startswith('Pathname/Comment')):
|
||||
if line.strip().endswith('is not RAR archive'):
|
||||
raise InvalidRARArchive
|
||||
while not (line.startswith('UNRAR')):
|
||||
line = source.next()
|
||||
while not line.startswith('Pathname/Comment'):
|
||||
accum.append(line.rstrip('\n'))
|
||||
signature = line
|
||||
# The code below is mighty flaky
|
||||
# and will probably crash on localized versions of RAR
|
||||
# but I see no safe way to rewrite it using a CLI tool
|
||||
if signature.startswith("UNRAR 4"):
|
||||
rar_executable_version = 4
|
||||
while not (line.startswith('Comment:') or line.startswith('Pathname/Comment')):
|
||||
if line.strip().endswith('is not RAR archive'):
|
||||
raise InvalidRARArchive
|
||||
line = source.next()
|
||||
while not line.startswith('Pathname/Comment'):
|
||||
accum.append(line.rstrip('\n'))
|
||||
line = source.next()
|
||||
if len(accum):
|
||||
accum[0] = accum[0][9:] # strip out "Comment:" part
|
||||
self.comment = '\n'.join(accum[:-1])
|
||||
else:
|
||||
self.comment = None
|
||||
elif signature.startswith("UNRAR 5"):
|
||||
rar_executable_version = 5
|
||||
line = source.next()
|
||||
if len(accum):
|
||||
accum[0] = accum[0][9:]
|
||||
self.comment = '\n'.join(accum[:-1])
|
||||
while not line.startswith('Archive:'):
|
||||
if line.strip().endswith('is not RAR archive'):
|
||||
raise InvalidRARArchive
|
||||
accum.append(line.rstrip('\n'))
|
||||
line = source.next()
|
||||
if len(accum):
|
||||
self.comment = '\n'.join(accum[:-1]).strip()
|
||||
else:
|
||||
self.comment = None
|
||||
else:
|
||||
self.comment = None
|
||||
raise UnpackerNotInstalled("Unsupported RAR version, expected 4.x or 5.x, found: "
|
||||
+ signature.split(" ")[1])
|
||||
|
||||
|
||||
def escaped_password(self):
|
||||
return '-' if self.password == None else self.password
|
||||
@ -97,7 +122,8 @@ class RarFileImplementation(object):
|
||||
|
||||
def infoiter(self):
|
||||
|
||||
stdoutdata, stderrdata = self.call('v', ['c-']).communicate()
|
||||
command = "v" if rar_executable_version == 4 else "l"
|
||||
stdoutdata, stderrdata = self.call(command, ['c-']).communicate()
|
||||
|
||||
for line in stderrdata.splitlines():
|
||||
if line.strip().startswith("Cannot open"):
|
||||
@ -106,33 +132,48 @@ class RarFileImplementation(object):
|
||||
accum = []
|
||||
source = iter(stdoutdata.splitlines())
|
||||
line = ''
|
||||
while not line.startswith('--------------'):
|
||||
while not line.startswith('-----------'):
|
||||
if line.strip().endswith('is not RAR archive'):
|
||||
raise InvalidRARArchive
|
||||
if line.find("CRC failed")>=0:
|
||||
if line.startswith("CRC failed") or line.startswith("Checksum error"):
|
||||
raise IncorrectRARPassword
|
||||
line = source.next()
|
||||
line = source.next()
|
||||
i = 0
|
||||
re_spaces = re.compile(r"\s+")
|
||||
while not line.startswith('--------------'):
|
||||
accum.append(line)
|
||||
if len(accum)==2:
|
||||
if rar_executable_version == 4:
|
||||
while not line.startswith('-----------'):
|
||||
accum.append(line)
|
||||
if len(accum)==2:
|
||||
data = {}
|
||||
data['index'] = i
|
||||
# asterisks mark password-encrypted files
|
||||
data['filename'] = accum[0].strip().lstrip("*") # asterisks marks password-encrypted files
|
||||
fields = re_spaces.split(accum[1].strip())
|
||||
data['size'] = int(fields[0])
|
||||
attr = fields[5]
|
||||
data['isdir'] = 'd' in attr.lower()
|
||||
data['datetime'] = time.strptime(fields[3]+" "+fields[4], '%d-%m-%y %H:%M')
|
||||
data['comment'] = None
|
||||
yield data
|
||||
accum = []
|
||||
i += 1
|
||||
line = source.next()
|
||||
elif rar_executable_version == 5:
|
||||
while not line.startswith('-----------'):
|
||||
fields = line.strip().lstrip("*").split()
|
||||
data = {}
|
||||
data['index'] = i
|
||||
#!!!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]
|
||||
data['filename'] = " ".join(fields[4:])
|
||||
data['size'] = int(fields[1])
|
||||
attr = fields[0]
|
||||
data['isdir'] = 'd' in attr.lower()
|
||||
data['datetime'] = time.strptime(info[3]+" "+info[4], '%d-%m-%y %H:%M')
|
||||
data['datetime'] = time.strptime(fields[2]+" "+fields[3], '%d-%m-%y %H:%M')
|
||||
data['comment'] = None
|
||||
yield data
|
||||
accum = []
|
||||
i += 1
|
||||
line = source.next()
|
||||
line = source.next()
|
||||
|
||||
|
||||
def read_files(self, checker):
|
||||
res = []
|
||||
@ -153,7 +194,7 @@ class RarFileImplementation(object):
|
||||
if overwrite:
|
||||
options.append('o+')
|
||||
else:
|
||||
options.append('o-')
|
||||
options.append('o-')
|
||||
if not path.endswith(os.sep):
|
||||
path += os.sep
|
||||
names = []
|
||||
@ -167,7 +208,7 @@ class RarFileImplementation(object):
|
||||
names.append(path)
|
||||
proc = self.call(command, options, names)
|
||||
stdoutdata, stderrdata = proc.communicate()
|
||||
if stderrdata.find("CRC failed")>=0:
|
||||
if stderrdata.find("CRC failed")>=0 or stderrdata.find("Checksum error")>=0:
|
||||
raise IncorrectRARPassword
|
||||
return res
|
||||
|
||||
|
@ -3,7 +3,7 @@ A PyQT4 dialog to select from automated issue matches
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -3,7 +3,7 @@ A PyQT4 dialog to show ID log and progress
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -22,6 +22,7 @@ import sys
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
import os
|
||||
from settings import ComicTaggerSettings
|
||||
from coverimagewidget import CoverImageWidget
|
||||
import utils
|
||||
|
||||
class AutoTagProgressWindow(QtGui.QDialog):
|
||||
@ -31,8 +32,17 @@ class AutoTagProgressWindow(QtGui.QDialog):
|
||||
super(AutoTagProgressWindow, self).__init__(parent)
|
||||
|
||||
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.archiveCoverWidget = CoverImageWidget( self.archiveCoverContainer, CoverImageWidget.DataMode, False )
|
||||
gridlayout = QtGui.QGridLayout( self.archiveCoverContainer )
|
||||
gridlayout.addWidget( self.archiveCoverWidget )
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
|
||||
self.testCoverWidget = CoverImageWidget( self.testCoverContainer, CoverImageWidget.DataMode, False )
|
||||
gridlayout = QtGui.QGridLayout( self.testCoverContainer )
|
||||
gridlayout.addWidget( self.testCoverWidget )
|
||||
gridlayout.setContentsMargins(0,0,0,0)
|
||||
|
||||
self.isdone = False
|
||||
|
||||
self.setWindowFlags(self.windowFlags() |
|
||||
@ -42,23 +52,16 @@ class AutoTagProgressWindow(QtGui.QDialog):
|
||||
utils.reduceWidgetFontSize( self.textEdit )
|
||||
|
||||
def setArchiveImage( self, img_data):
|
||||
self.setCoverImage( img_data, self.lblArchive )
|
||||
self.setCoverImage( img_data, self.archiveCoverWidget)
|
||||
|
||||
def setTestImage( self, img_data):
|
||||
self.setCoverImage( img_data, self.lblTest )
|
||||
self.setCoverImage( img_data, self.testCoverWidget)
|
||||
|
||||
def setCoverImage( self, img_data , label):
|
||||
if img_data is not None:
|
||||
img = QtGui.QImage()
|
||||
img.loadFromData( img_data )
|
||||
label.setPixmap(QtGui.QPixmap(img))
|
||||
label.setScaledContents(True)
|
||||
else:
|
||||
label.setPixmap(QtGui.QPixmap(ComicTaggerSettings.getGraphic('nocover.png')))
|
||||
label.setScaledContents(True)
|
||||
def setCoverImage( self, img_data , widget):
|
||||
widget.setImageData( img_data )
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
|
||||
def reject(self):
|
||||
QtGui.QDialog.reject(self)
|
||||
self.isdone = True
|
||||
|
@ -3,7 +3,7 @@ A PyQT4 dialog to confirm and set options for auto-tag
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -3,7 +3,7 @@ Class to manage modifying metadata specifically for CBL/CBI
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -3,7 +3,7 @@ A python class to encapsulate CoMet data
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -3,7 +3,7 @@ A python class to represent a single comic, be it file or folder of images
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -72,15 +72,16 @@ class ZipArchiver:
|
||||
def readArchiveFile( self, archive_file ):
|
||||
data = ""
|
||||
zf = zipfile.ZipFile( self.path, 'r' )
|
||||
|
||||
try:
|
||||
data = zf.read( archive_file )
|
||||
except zipfile.BadZipfile as e:
|
||||
print >> sys.stderr, "bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file)
|
||||
print >> sys.stderr, u"bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file)
|
||||
zf.close()
|
||||
raise IOError
|
||||
except Exception as e:
|
||||
zf.close()
|
||||
print >> sys.stderr, "bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file)
|
||||
print >> sys.stderr, u"bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file)
|
||||
raise IOError
|
||||
finally:
|
||||
zf.close()
|
||||
@ -109,12 +110,16 @@ class ZipArchiver:
|
||||
except:
|
||||
return False
|
||||
|
||||
def getArchiveFilenameList( self ):
|
||||
zf = zipfile.ZipFile( self.path, 'r' )
|
||||
namelist = zf.namelist()
|
||||
zf.close()
|
||||
return namelist
|
||||
|
||||
def getArchiveFilenameList( self ):
|
||||
try:
|
||||
zf = zipfile.ZipFile( self.path, 'r' )
|
||||
namelist = zf.namelist()
|
||||
zf.close()
|
||||
return namelist
|
||||
except Exception as e:
|
||||
print >> sys.stderr, u"Unable to get zipfile list [{0}]: {1}".format(e, self.path)
|
||||
return []
|
||||
|
||||
# zip helper func
|
||||
def rebuildZipFile( self, exclude_list ):
|
||||
|
||||
@ -222,7 +227,7 @@ class ZipArchiver:
|
||||
if not self.writeZipComment( self.path, comment ):
|
||||
return False
|
||||
except Exception as e:
|
||||
print >> sys.stderr, "Error while copying to {0}: {1}".format(self.path, e)
|
||||
print >> sys.stderr, u"Error while copying to {0}: {1}".format(self.path, e)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
@ -301,22 +306,22 @@ class RarArchiver:
|
||||
entries = rarc.read_files( archive_file )
|
||||
|
||||
if entries[0][0].size != len(entries[0][1]):
|
||||
print >> sys.stderr, "readArchiveFile(): [file is not expected size: {0} vs {1}] {2}:{3} [attempt # {4}]".format(
|
||||
print >> sys.stderr, u"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 >> sys.stderr, "readArchiveFile(): [{0}] {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries)
|
||||
print >> sys.stderr, u"readArchiveFile(): [{0}] {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries)
|
||||
time.sleep(1)
|
||||
except Exception as e:
|
||||
print >> sys.stderr, "Unexpected exception in readArchiveFile(): [{0}] for {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries)
|
||||
print >> sys.stderr, u"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 >> sys.stderr, "Attempted read_files() {0} times".format(tries)
|
||||
print >> sys.stderr, u"Attempted read_files() {0} times".format(tries)
|
||||
if (len(entries) == 1):
|
||||
return entries[0][1]
|
||||
else:
|
||||
@ -392,7 +397,7 @@ class RarArchiver:
|
||||
namelist.append( item.filename )
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
print >> sys.stderr, "getArchiveFilenameList(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries)
|
||||
print >> sys.stderr, u"getArchiveFilenameList(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries)
|
||||
time.sleep(1)
|
||||
|
||||
else:
|
||||
@ -410,7 +415,7 @@ class RarArchiver:
|
||||
rarc = UnRAR2.RarFile( self.path )
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
print >> sys.stderr, "getRARObj(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries)
|
||||
print >> sys.stderr, u"getRARObj(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries)
|
||||
time.sleep(1)
|
||||
|
||||
else:
|
||||
@ -512,19 +517,20 @@ class ComicArchive:
|
||||
|
||||
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
|
||||
self.archiver = ZipArchiver( self.path )
|
||||
|
||||
elif self.rarTest():
|
||||
if self.rarTest():
|
||||
self.archive_type = self.ArchiveType.Rar
|
||||
self.archiver = RarArchiver( self.path, settings )
|
||||
|
||||
elif self.zipTest():
|
||||
self.archive_type = self.ArchiveType.Zip
|
||||
self.archiver = ZipArchiver( self.path )
|
||||
|
||||
elif os.path.isdir( self.path ):
|
||||
self.archive_type = self.ArchiveType.Folder
|
||||
self.archiver = FolderArchiver( self.path )
|
||||
@ -609,7 +615,7 @@ class ComicArchive:
|
||||
if (
|
||||
( self.isZip() or self.isRar() ) #or self.isFolder() )
|
||||
and
|
||||
( self.getNumberOfPages() > 2)
|
||||
( self.getNumberOfPages() > 0)
|
||||
|
||||
):
|
||||
return True
|
||||
@ -670,7 +676,7 @@ class ComicArchive:
|
||||
try:
|
||||
image_data = self.archiver.readArchiveFile( filename )
|
||||
except IOError:
|
||||
print >> sys.stderr, "Error reading in page. Substituting logo page."
|
||||
print >> sys.stderr, u"Error reading in page. Substituting logo page."
|
||||
image_data = ComicArchive.logo_data
|
||||
|
||||
return image_data
|
||||
@ -921,7 +927,7 @@ class ComicArchive:
|
||||
try:
|
||||
raw_comet = self.archiver.readArchiveFile( self.comet_filename )
|
||||
except IOError:
|
||||
print >> sys.stderr, "Error reading in raw CoMet!"
|
||||
print >> sys.stderr, u"Error reading in raw CoMet!"
|
||||
raw_comet = ""
|
||||
return raw_comet
|
||||
|
||||
@ -972,7 +978,7 @@ class ComicArchive:
|
||||
data = self.archiver.readArchiveFile( n )
|
||||
except:
|
||||
data = ""
|
||||
print >> sys.stderr, "Error reading in Comet XML for validation!"
|
||||
print >> sys.stderr, u"Error reading in Comet XML for validation!"
|
||||
if CoMet().validateString( data ):
|
||||
# since we found it, save it!
|
||||
self.comet_filename = n
|
||||
|
@ -3,7 +3,7 @@ A python class to encapsulate the ComicBookInfo data
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -3,7 +3,7 @@ A python class to encapsulate ComicRack's ComicInfo.xml data
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -260,12 +260,14 @@ class ComicInfoXml:
|
||||
n.tag == 'Letterer' or
|
||||
n.tag == 'Editor'
|
||||
):
|
||||
for name in n.text.split(','):
|
||||
metadata.addCredit( name.strip(), n.tag )
|
||||
if n.text is not None:
|
||||
for name in n.text.split(','):
|
||||
metadata.addCredit( name.strip(), n.tag )
|
||||
|
||||
if n.tag == 'CoverArtist':
|
||||
for name in n.text.split(','):
|
||||
metadata.addCredit( name.strip(), "Cover" )
|
||||
if n.text is not None:
|
||||
for name in n.text.split(','):
|
||||
metadata.addCredit( name.strip(), "Cover" )
|
||||
|
||||
# parse page data now
|
||||
pages_node = root.find( "Pages" )
|
||||
|
@ -3,7 +3,7 @@ A python class to manage caching of data from Comic Vine
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -3,7 +3,7 @@ A python class to manage communication with Comic Vine's REST API
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -24,6 +24,7 @@ from pprint import pprint
|
||||
import urllib2, urllib
|
||||
import math
|
||||
import re
|
||||
import time
|
||||
import datetime
|
||||
import ctversion
|
||||
import sys
|
||||
@ -106,12 +107,28 @@ class ComicVineTalker(QObject):
|
||||
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!")
|
||||
# connect to server:
|
||||
# if there is a 500 error, try a few more times before giving up
|
||||
# any other error, just bail
|
||||
#print "ATB---", url
|
||||
for tries in range(3):
|
||||
try:
|
||||
resp = urllib2.urlopen( url )
|
||||
return resp.read()
|
||||
except urllib2.HTTPError as e:
|
||||
if e.getcode() == 500:
|
||||
self.writeLog( "Try #{0}: ".format(tries+1) )
|
||||
time.sleep(1)
|
||||
self.writeLog( str(e) + "\n" )
|
||||
|
||||
if e.getcode() != 500:
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
self.writeLog( str(e) + "\n" )
|
||||
raise ComicVineTalkerException("Network Error!")
|
||||
|
||||
raise ComicVineTalkerException("Error on Comic Vine server")
|
||||
|
||||
def searchForSeries( self, series_name , callback=None, refresh_cache=False ):
|
||||
|
||||
@ -128,11 +145,23 @@ class ComicVineTalker(QObject):
|
||||
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)
|
||||
|
||||
# We need to make the series name into an "AND"ed query list
|
||||
query_word_list = series_name.split()
|
||||
and_list = ['AND'] * (len(query_word_list)-1)
|
||||
and_list.append('')
|
||||
# zipper up the two lists
|
||||
query_list = zip(query_word_list, and_list)
|
||||
# flatten the list
|
||||
query_list = [ item for sublist in query_list for item in sublist]
|
||||
# convert back to a string
|
||||
query_string = " ".join( query_list ).strip()
|
||||
#print "Query string = ", query_string
|
||||
|
||||
query_string = urllib.quote_plus(query_string.encode("utf-8"))
|
||||
|
||||
search_url = self.api_base_url + "/search/?api_key=" + self.api_key + "&format=json&resources=volume&query=" + query_string + "&field_list=name,id,start_year,publisher,image,description,count_of_issues"
|
||||
content = self.getUrlContent(search_url + "&page=1")
|
||||
|
||||
cv_response = json.loads(content)
|
||||
|
||||
@ -574,7 +603,7 @@ class ComicVineTalker(QObject):
|
||||
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"
|
||||
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"
|
||||
self.nam = QNetworkAccessManager()
|
||||
self.nam.finished.connect( self.asyncFetchIssueCoverURLComplete )
|
||||
self.nam.get(QNetworkRequest(QUrl(issue_url)))
|
||||
|
@ -1,9 +1,11 @@
|
||||
"""
|
||||
A PyQt4 widget display cover images from either local archive, or from ComicVine
|
||||
|
||||
(TODO: This should be re-factored using subclasses!)
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -59,8 +61,9 @@ class CoverImageWidget(QWidget):
|
||||
ArchiveMode = 0
|
||||
AltCoverMode = 1
|
||||
URLMode = 1
|
||||
DataMode = 3
|
||||
|
||||
def __init__(self, parent, mode ):
|
||||
def __init__(self, parent, mode, expand_on_click = True ):
|
||||
super(CoverImageWidget, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('coverimagewidget.ui' ), self)
|
||||
@ -78,7 +81,10 @@ class CoverImageWidget(QWidget):
|
||||
self.btnLeft.clicked.connect( self.decrementImage )
|
||||
self.btnRight.clicked.connect( self.incrementImage )
|
||||
self.resetWidget()
|
||||
clickable(self.lblImage).connect(self.showPopup)
|
||||
if expand_on_click:
|
||||
clickable(self.lblImage).connect(self.showPopup)
|
||||
else:
|
||||
self.lblImage.setToolTip( "" )
|
||||
|
||||
self.updateContent()
|
||||
|
||||
@ -93,11 +99,12 @@ class CoverImageWidget(QWidget):
|
||||
self.page_loader = None
|
||||
self.imageIndex = -1
|
||||
self.imageCount = 1
|
||||
self.imageData = None
|
||||
|
||||
def clear( self ):
|
||||
self.resetWidget()
|
||||
self.updateContent()
|
||||
|
||||
|
||||
def incrementImage( self ):
|
||||
self.imageIndex += 1
|
||||
if self.imageIndex == self.imageCount:
|
||||
@ -138,7 +145,19 @@ class CoverImageWidget(QWidget):
|
||||
self.comicVine = ComicVineTalker()
|
||||
self.comicVine.urlFetchComplete.connect( self.primaryUrlFetchComplete )
|
||||
self.comicVine.asyncFetchIssueCoverURLs( int(self.issue_id) )
|
||||
|
||||
|
||||
def setImageData( self, image_data ):
|
||||
if self.mode == CoverImageWidget.DataMode:
|
||||
self.resetWidget()
|
||||
|
||||
if image_data is None:
|
||||
self.imageIndex = -1
|
||||
else:
|
||||
self.imageIndex = 0
|
||||
self.imageData = image_data
|
||||
|
||||
self.updateContent()
|
||||
|
||||
def primaryUrlFetchComplete( self, primary_url, thumb_url, issue_id ):
|
||||
self.url_list.append(str(primary_url))
|
||||
self.imageIndex = 0
|
||||
@ -179,11 +198,13 @@ class CoverImageWidget(QWidget):
|
||||
self.loadDefault()
|
||||
elif self.mode in [ CoverImageWidget.AltCoverMode, CoverImageWidget.URLMode ]:
|
||||
self.loadURL()
|
||||
elif self.mode == CoverImageWidget.DataMode:
|
||||
self.coverRemoteFetchComplete( self.imageData, 0 )
|
||||
else:
|
||||
self.loadPage()
|
||||
|
||||
def updateControls( self ):
|
||||
if not self.showControls:
|
||||
if not self.showControls or self.mode == CoverImageWidget.DataMode:
|
||||
self.btnLeft.hide()
|
||||
self.btnRight.hide()
|
||||
self.label.hide()
|
||||
@ -288,3 +309,4 @@ class CoverImageWidget(QWidget):
|
||||
|
||||
def showPopup( self ):
|
||||
self.popup = ImagePopup(self, self.current_pixmap)
|
||||
|
||||
|
@ -3,7 +3,7 @@ A PyQT4 dialog to edit credits
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -1,3 +1,3 @@
|
||||
# This file should contan only these comments, and the line below.
|
||||
# Used by packaging makefiles and app
|
||||
version="1.1.7-beta"
|
||||
version="1.1.12-beta"
|
||||
|
@ -3,7 +3,7 @@ A PyQT4 dialog to confirm and set options for export to zip
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -6,7 +6,7 @@ This should probably be re-written, but, well, it mostly works!
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -30,31 +30,25 @@ import os
|
||||
from urllib import unquote
|
||||
|
||||
class FileNameParser:
|
||||
|
||||
def repl(self, m):
|
||||
return ' ' * len(m.group())
|
||||
|
||||
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()
|
||||
|
||||
# check for silly .1 or .5 style issue strings
|
||||
# allow up to 5 chars total
|
||||
def isPointIssue( self, word ):
|
||||
ret = False
|
||||
try:
|
||||
float(word)
|
||||
if (len(word) < 5 and not word.isdigit()):
|
||||
ret = True
|
||||
except ValueError:
|
||||
pass
|
||||
return ret
|
||||
string = re.sub(ph, self.repl, string )
|
||||
return string #.strip()
|
||||
|
||||
|
||||
def getIssueCount( self,filename ):
|
||||
def getIssueCount( self,filename, issue_end ):
|
||||
|
||||
count = ""
|
||||
filename = filename[issue_end:]
|
||||
|
||||
# replace any name seperators with spaces
|
||||
tmpstr = self.fixSpaces(filename)
|
||||
found = False
|
||||
@ -74,113 +68,150 @@ class FileNameParser:
|
||||
count = count.lstrip("0")
|
||||
|
||||
return count
|
||||
|
||||
|
||||
|
||||
def getIssueNumber( self, filename ):
|
||||
|
||||
# Returns a tuple of issue number string, and start and end indexs in the filename
|
||||
# (The indexes will be used to split the string up for further parsing)
|
||||
|
||||
found = False
|
||||
issue = ''
|
||||
start = 0
|
||||
end = 0
|
||||
|
||||
# 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:
|
||||
filename = re.sub("--.*", self.repl, filename)
|
||||
|
||||
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 = re.sub("__.*", self.repl, filename)
|
||||
|
||||
filename = filename.replace("+", " ")
|
||||
|
||||
# remove parenthetical phrases
|
||||
filename = re.sub( "\(.*\)", "", filename)
|
||||
filename = re.sub( "\[.*\]", "", filename)
|
||||
|
||||
# guess based on position
|
||||
# replace parenthetical phrases with spaces
|
||||
filename = re.sub( "\(.*?\)", self.repl, filename)
|
||||
filename = re.sub( "\[.*?\]", self.repl, filename)
|
||||
|
||||
# replace any name seperators with spaces
|
||||
tmpstr = self.fixSpaces(filename)
|
||||
word_list = tmpstr.split(' ')
|
||||
filename = self.fixSpaces(filename)
|
||||
|
||||
# remove any "of NN" phrase with spaces (problem: this could break on some titles)
|
||||
filename = re.sub( "of [\d]+", self.repl, filename)
|
||||
|
||||
#print u"[{0}]".format(filename)
|
||||
|
||||
#before we search, remove any kind of likely "of X" phrase
|
||||
for i in range(0, len(word_list)-2):
|
||||
if ( word_list[i].isdigit() and
|
||||
word_list[i+1] == "of" and
|
||||
word_list[i+2].isdigit() ):
|
||||
word_list[i+1] ="XXX"
|
||||
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
|
||||
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)
|
||||
if issnum:
|
||||
issue = issnum.group()
|
||||
# we should now have a cleaned up filename version with all the words in
|
||||
# the same positions as original filename
|
||||
|
||||
# make a list of each word and its position
|
||||
word_list = list()
|
||||
for m in re.finditer("\S+", filename):
|
||||
word_list.append( (m.group(0), m.start(), m.end()) )
|
||||
|
||||
# remove the first word, since it can't be the issue number
|
||||
if len(word_list) > 1:
|
||||
word_list = word_list[1:]
|
||||
else:
|
||||
#only one word?? just bail.
|
||||
return issue, start, end
|
||||
|
||||
# Now try to search for the likely issue number word in the list
|
||||
|
||||
# first look for a word with "#" followed by digits with optional sufix
|
||||
# this is almost certainly the issue number
|
||||
for w in reversed(word_list):
|
||||
if re.match("#[-]?(([0-9]*\.[0-9]+|[0-9]+)(\w*))", w[0]):
|
||||
found = True
|
||||
#print 'Got the issue using regex. Issue is ' + issue
|
||||
break
|
||||
|
||||
# same as above but w/o a '#', and only look at the last word in the list
|
||||
if not found:
|
||||
w = word_list[-1]
|
||||
if re.match("[-]?(([0-9]*\.[0-9]+|[0-9]+)(\w*))", w[0]):
|
||||
found = True
|
||||
|
||||
# now try to look for a # followed by any characters
|
||||
if not found:
|
||||
for w in reversed(word_list):
|
||||
if re.match("#\S+", w[0]):
|
||||
found = True
|
||||
break
|
||||
|
||||
if found:
|
||||
issue = w[0]
|
||||
start = w[1]
|
||||
end = w[2]
|
||||
if issue[0] == '#':
|
||||
issue = issue[1:]
|
||||
|
||||
return issue, start, end
|
||||
|
||||
return issue.strip()
|
||||
|
||||
def getSeriesName(self, filename, issue ):
|
||||
|
||||
# use the issue number string to split the filename string
|
||||
# assume first element of list is the series name, plus cruft
|
||||
#!!! this could fail in the case of small numerics in the series name!!!
|
||||
|
||||
# TODO: we really should pass in the *INDEX* of the issue, that makes
|
||||
# finding it easier
|
||||
def getSeriesName(self, filename, issue_start ):
|
||||
|
||||
# use the issue number string index to split the filename string
|
||||
|
||||
if issue_start != 0:
|
||||
filename = filename[:issue_start]
|
||||
|
||||
# in case there is no issue number, remove some obvious stuff
|
||||
if "--" in filename:
|
||||
# the pattern seems to be that anything to left of the first "--" is the series name followed by issue
|
||||
filename = re.sub("--.*", self.repl, filename)
|
||||
|
||||
elif "__" in filename:
|
||||
# the pattern seems to be that anything to left of the first "__" is the series name followed by issue
|
||||
filename = re.sub("__.*", self.repl, 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("#", " ")
|
||||
|
||||
if issue != "":
|
||||
# assume that issue substr has at least one space before it
|
||||
issue_str = " " + str(issue)
|
||||
series = tmpstr.split(issue_str)[0]
|
||||
else:
|
||||
# no issue to work off of
|
||||
#!!! TODO we should look for the year, and split from that
|
||||
# and if that doesn't exist, remove parenthetical phrases
|
||||
series = tmpstr
|
||||
series = re.sub( "\(.*\)", "", tmpstr)
|
||||
|
||||
series = tmpstr
|
||||
volume = ""
|
||||
|
||||
#save the last word
|
||||
try:
|
||||
last_word = series.split()[-1]
|
||||
except:
|
||||
last_word = ""
|
||||
|
||||
series = series.rstrip("#")
|
||||
# remove any parenthetical phrases
|
||||
series = re.sub( "\(.*?\)", "", series)
|
||||
|
||||
# search for volume number
|
||||
match = re.search('(.+)([vV]|[Vv][oO][Ll]\.?\s?)(\d+)\s*$', series)
|
||||
if match:
|
||||
series = match.group(1)
|
||||
volume = match.group(3)
|
||||
|
||||
return series.strip(), volume.strip()
|
||||
|
||||
# if a volume wasn't found, see if the last word is a year in parentheses
|
||||
# since that's a common way to designate the volume
|
||||
if volume == "":
|
||||
#match either (YEAR), (YEAR-), or (YEAR-YEAR2)
|
||||
match = re.search("(\()(\d{4})(-(\d{4}|)|)(\))", last_word)
|
||||
if match:
|
||||
volume = match.group(2)
|
||||
|
||||
def getYear( self,filename):
|
||||
series = series.strip()
|
||||
|
||||
# if we don't have an issue number (issue_start==0), look
|
||||
# for hints i.e. "TPB", "one-shot", "OS", "OGN", etc that might
|
||||
# be removed to help search online
|
||||
if issue_start == 0:
|
||||
one_shot_words = [ "tpb", "os", "one-shot", "ogn", "gn" ]
|
||||
try:
|
||||
last_word = series.split()[-1]
|
||||
if last_word.lower() in one_shot_words:
|
||||
series = series.rsplit(' ', 1)[0]
|
||||
except:
|
||||
pass
|
||||
|
||||
return series, volume.strip()
|
||||
|
||||
def getYear( self,filename, issue_end):
|
||||
|
||||
filename = filename[issue_end:]
|
||||
|
||||
year = ""
|
||||
# look for four digit number with "(" ")" or "--" around it
|
||||
@ -191,17 +222,17 @@ 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
|
||||
def getRemainder( self, filename, year, count, issue_end ):
|
||||
|
||||
#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]
|
||||
elif issue_end != 0:
|
||||
remainder = filename[issue_end:]
|
||||
|
||||
remainder = self.fixSpaces(remainder, remove_dashes=False)
|
||||
if year != "":
|
||||
@ -231,11 +262,11 @@ class FileNameParser:
|
||||
filename = filename.replace("_28", "(")
|
||||
filename = filename.replace("_29", ")")
|
||||
|
||||
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 )
|
||||
self.issue, issue_start, issue_end = self.getIssueNumber(filename)
|
||||
self.series, self.volume = self.getSeriesName(filename, issue_start)
|
||||
self.year = self.getYear(filename, issue_end)
|
||||
self.issue_count = self.getIssueCount(filename, issue_end)
|
||||
self.remainder = self.getRemainder( filename, self.year, self.issue_count, issue_end )
|
||||
|
||||
if self.issue != "":
|
||||
# strip off leading zeros
|
||||
@ -244,4 +275,3 @@ class FileNameParser:
|
||||
self.issue = "0"
|
||||
if self.issue[0] == ".":
|
||||
self.issue = "0" + self.issue
|
||||
|
||||
|
@ -3,7 +3,7 @@ Functions for renaming files based on metadata
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -118,12 +118,20 @@ class FileRenamer:
|
||||
new_name = re.sub("\[\s*[-:]*\s*\]", "", new_name )
|
||||
new_name = re.sub("\{\s*[-:]*\s*\}", "", new_name )
|
||||
|
||||
# remove remove duplicate -, _,
|
||||
new_name = re.sub("[-_]+\s+", "- ", new_name )
|
||||
new_name = re.sub("(\s-)+", " -", new_name )
|
||||
|
||||
# remove duplicate spaces
|
||||
new_name = u" ".join(new_name.split())
|
||||
|
||||
# remove remove duplicate -, _,
|
||||
new_name = re.sub("[-_]{2,}\s+", "-- ", new_name )
|
||||
new_name = re.sub("(\s--)+", " --", new_name )
|
||||
new_name = re.sub("(\s-)+", " -", new_name )
|
||||
|
||||
# remove dash or double dash at end of line
|
||||
new_name = re.sub("[-]{1,2}\s*$", "", new_name )
|
||||
|
||||
# remove duplicate spaces (again!)
|
||||
new_name = u" ".join(new_name.split())
|
||||
|
||||
|
||||
if ext is None:
|
||||
ext = os.path.splitext( filename )[1]
|
||||
@ -132,7 +140,9 @@ class FileRenamer:
|
||||
|
||||
# some tweaks to keep various filesystems happy
|
||||
new_name = new_name.replace("/", "-")
|
||||
new_name = new_name.replace(":", "-")
|
||||
new_name = new_name.replace(" :", " -")
|
||||
new_name = new_name.replace(": ", " - ")
|
||||
new_name = new_name.replace(":", "-")
|
||||
new_name = new_name.replace("?", "")
|
||||
|
||||
return new_name
|
||||
|
@ -4,7 +4,7 @@ A PyQt4 widget for managing list of comic archive files
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -162,6 +162,9 @@ class FileSelectionList(QWidget):
|
||||
self.twList.currentItemChanged.connect( self.currentItemChangedCB )
|
||||
|
||||
if self.twList.rowCount() > 0:
|
||||
# since on a removal, we select row 0, make sure callback occurs if we're already there
|
||||
if self.twList.currentRow() == 0:
|
||||
self.currentItemChangedCB( self.twList.currentItem(), None)
|
||||
self.twList.selectRow(0)
|
||||
else:
|
||||
self.listCleared.emit()
|
||||
@ -195,7 +198,13 @@ class FileSelectionList(QWidget):
|
||||
progdialog.close()
|
||||
if firstAdded is not None:
|
||||
self.twList.selectRow(firstAdded)
|
||||
else:
|
||||
if len(pathlist) == 1 and os.path.isfile(pathlist[0]):
|
||||
QMessageBox.information(self, self.tr("File Open"), self.tr("Selected file doesn't seem to be a comic archive."))
|
||||
else:
|
||||
QMessageBox.information(self, self.tr("File/Folder Open"), self.tr("No comic archives were found."))
|
||||
|
||||
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
# Adjust column size
|
||||
|
@ -8,7 +8,7 @@
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -31,7 +31,7 @@ class PageType:
|
||||
InnerCover = "InnerCover"
|
||||
Roundup = "Roundup"
|
||||
Story = "Story"
|
||||
Advertisment = "Advertisment"
|
||||
Advertisment = "Advertisement"
|
||||
Editorial = "Editorial"
|
||||
Letters = "Letters"
|
||||
Preview = "Preview"
|
||||
|
@ -3,7 +3,7 @@ A python class to manage fetching and caching of images by URL
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -3,7 +3,7 @@ A PyQT4 widget to display a popup image
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -3,7 +3,7 @@ A python class to automatically identify a comic archive
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -157,6 +157,7 @@ class IssueIdentifier:
|
||||
search_keys['issue_number'] = None
|
||||
search_keys['month'] = None
|
||||
search_keys['year'] = None
|
||||
search_keys['issue_count'] = None
|
||||
|
||||
if ca is None:
|
||||
return
|
||||
@ -166,6 +167,7 @@ class IssueIdentifier:
|
||||
search_keys['issue_number'] = self.additional_metadata.issue
|
||||
search_keys['year'] = self.additional_metadata.year
|
||||
search_keys['month'] = self.additional_metadata.month
|
||||
search_keys['issue_count'] = self.additional_metadata.issueCount
|
||||
return search_keys
|
||||
|
||||
# see if the archive has any useful meta data for searching with
|
||||
@ -211,6 +213,13 @@ class IssueIdentifier:
|
||||
search_keys['month'] = internal_metadata.month
|
||||
else:
|
||||
search_keys['month'] = md_from_filename.month
|
||||
|
||||
if self.additional_metadata.issueCount is not None:
|
||||
search_keys['issue_count'] = self.additional_metadata.issueCount
|
||||
elif internal_metadata.issueCount is not None:
|
||||
search_keys['issue_count'] = internal_metadata.issueCount
|
||||
else:
|
||||
search_keys['issue_count'] = md_from_filename.issueCount
|
||||
|
||||
return search_keys
|
||||
|
||||
@ -360,6 +369,8 @@ class IssueIdentifier:
|
||||
self.log_msg( "Going to search for:" )
|
||||
self.log_msg( "\tSeries: " + keys['series'] )
|
||||
self.log_msg( "\tIssue : " + keys['issue_number'] )
|
||||
if keys['issue_count'] is not None:
|
||||
self.log_msg( "\tCount : " + str(keys['issue_count']) )
|
||||
if keys['year'] is not None:
|
||||
self.log_msg( "\tYear : " + str(keys['year']) )
|
||||
if keys['month'] is not None:
|
||||
@ -409,11 +420,6 @@ class IssueIdentifier:
|
||||
|
||||
if length_approved and publisher_approved and date_approved:
|
||||
series_second_round_list.append(item)
|
||||
|
||||
# if we don't think it's an issue number 1, remove any series' that are one-shots
|
||||
#if keys['issue_number'] not in [ '1', '0', '0.1' ]:
|
||||
# #self.log_msg( "Removing one-shots" )
|
||||
# series_second_round_list[:] = [x for x in series_second_round_list if not x['count_of_issues'] == 1]
|
||||
|
||||
self.log_msg( "Searching in " + str(len(series_second_round_list)) +" series" )
|
||||
|
||||
@ -423,7 +429,6 @@ class IssueIdentifier:
|
||||
# now sort the list by name length
|
||||
series_second_round_list.sort(key=lambda x: len(x['name']), reverse=False)
|
||||
|
||||
#--------new way---------------
|
||||
#build a list of volume IDs
|
||||
volume_id_list = list()
|
||||
for series in series_second_round_list:
|
||||
@ -446,47 +451,6 @@ class IssueIdentifier:
|
||||
shortlist.append( (series, issue) )
|
||||
break
|
||||
|
||||
#--------new way---------------
|
||||
|
||||
"""
|
||||
#--vvvv---old way---------------
|
||||
# Now we've got a list of series that we can dig into look for matching issue number
|
||||
counter = 0
|
||||
shortlist = []
|
||||
for series in series_second_round_list:
|
||||
if self.callback is not None:
|
||||
self.callback( counter, len(series_second_round_list)*3)
|
||||
counter += 1
|
||||
|
||||
self.log_msg( u"Fetching info for ID: {0} {1} ({2}) ...".format(
|
||||
series['id'],
|
||||
series['name'],
|
||||
series['start_year']), newline=True )
|
||||
|
||||
try:
|
||||
issue_list = comicVine.fetchIssuesByVolume( series['id'] )
|
||||
|
||||
except ComicVineTalkerException:
|
||||
self.log_msg( "Network issue while searching for series details. Aborting...")
|
||||
return []
|
||||
|
||||
for issue in issue_list:
|
||||
num_s = IssueString(issue['issue_number']).asString()
|
||||
|
||||
# look for a matching issue number
|
||||
if num_s.lower() == keys['issue_number'].lower():
|
||||
|
||||
# now, if we have an issue year key given, reject this one if not a match
|
||||
month, year = comicVine.fetchIssueDate( issue['id'] )
|
||||
if keys['year'] is not None:
|
||||
if unicode(keys['year']) != unicode(year):
|
||||
break
|
||||
|
||||
# found a matching issue number! add it to short list
|
||||
shortlist.append( (series, issue) )
|
||||
#--^^^^---old way---------------
|
||||
"""
|
||||
|
||||
if keys['year'] is None:
|
||||
self.log_msg( u"Found {0} series that have an issue #{1}".format(len(shortlist), keys['issue_number']) )
|
||||
else:
|
||||
@ -528,6 +492,7 @@ class IssueIdentifier:
|
||||
match['series'] = u"{0} ({1})".format(series['name'], series['start_year'])
|
||||
match['distance'] = score_item['score']
|
||||
match['issue_number'] = keys['issue_number']
|
||||
match['cv_issue_count'] = series['count_of_issues']
|
||||
match['url_image_hash'] = score_item['hash']
|
||||
match['issue_title'] = issue['name']
|
||||
match['issue_id'] = issue['id']
|
||||
@ -640,6 +605,19 @@ class IssueIdentifier:
|
||||
if item['distance'] > best_score + self.min_score_distance:
|
||||
self.match_list.remove(item)
|
||||
|
||||
# One more test for the case choosing limited series first issue vs a trade with the same cover:
|
||||
# if we have a given issue count > 1 and the volume from CV has count==1, remove it from match list
|
||||
if len(self.match_list) >= 2 and keys['issue_count'] is not None and keys['issue_count'] != 1:
|
||||
new_list = list()
|
||||
for match in self.match_list:
|
||||
if match['cv_issue_count'] != 1:
|
||||
new_list.append(match)
|
||||
else:
|
||||
self.log_msg("Removing volume {0} [{1}] from consideration (only 1 issue)".format(match['series'], match['volume_id']))
|
||||
|
||||
if len(new_list) > 0:
|
||||
self.match_list = new_list
|
||||
|
||||
if len(self.match_list) == 1:
|
||||
self.log_msg( u"--------------------------------------------------")
|
||||
print_match(self.match_list[0])
|
||||
@ -652,6 +630,7 @@ class IssueIdentifier:
|
||||
self.log_msg( u"--------------------------------------------------")
|
||||
self.search_result = self.ResultNoMatches
|
||||
else:
|
||||
# we've got multiple good matches:
|
||||
self.log_msg( "More than one likley candiate." )
|
||||
self.search_result = self.ResultMultipleGoodMatches
|
||||
self.log_msg( u"--------------------------------------------------")
|
||||
|
@ -3,7 +3,7 @@ A PyQT4 dialog to select specific issue from list
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -8,11 +8,12 @@ e.g.:
|
||||
"0"
|
||||
"-1"
|
||||
"5AU"
|
||||
"100-2"
|
||||
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -34,31 +35,57 @@ import re
|
||||
class IssueString:
|
||||
def __init__(self, text):
|
||||
|
||||
if text is None:
|
||||
self.num = None
|
||||
self.suffix = ""
|
||||
return
|
||||
|
||||
self.text = unicode(text)
|
||||
#strip out non float-y stuff
|
||||
tmp_num_str = re.sub('[^0-9.-]',"", self.text )
|
||||
# break up the issue number string into 2 parts: the numeric and suffix string.
|
||||
# ( assumes that the numeric portion is always first )
|
||||
|
||||
self.num = None
|
||||
self.suffix = ""
|
||||
|
||||
if tmp_num_str == "":
|
||||
self.num = None
|
||||
self.suffix = self.text
|
||||
|
||||
if text is None:
|
||||
return
|
||||
|
||||
text = unicode(text)
|
||||
|
||||
#skip the minus sign if it's first
|
||||
if text[0] == '-':
|
||||
start = 1
|
||||
else:
|
||||
if tmp_num_str.count(".") > 1:
|
||||
#make sure it's a valid float or int.
|
||||
parts = tmp_num_str.split('.')
|
||||
self.num = float( parts[0] + '.' + parts[1] )
|
||||
else:
|
||||
self.num = float( tmp_num_str )
|
||||
|
||||
self.suffix = ""
|
||||
parts = self.text.split(tmp_num_str)
|
||||
if len( parts ) > 1 :
|
||||
self.suffix = parts[1]
|
||||
start = 0
|
||||
|
||||
# if it's still not numeric at start skip it
|
||||
if text[start].isdigit() or text[start] == ".":
|
||||
# walk through the string, look for split point (the first non-numeric)
|
||||
decimal_count = 0
|
||||
for idx in range( start, len(text) ):
|
||||
if text[idx] not in "0123456789.":
|
||||
break
|
||||
# special case: also split on second "."
|
||||
if text[idx] == ".":
|
||||
decimal_count += 1
|
||||
if decimal_count > 1:
|
||||
break
|
||||
else:
|
||||
idx = len(text)
|
||||
|
||||
# move trailing numeric decimal to suffix
|
||||
# (only if there is other junk after )
|
||||
if text[idx-1] == "." and len(text) != idx:
|
||||
idx = idx -1
|
||||
|
||||
# if there is no numeric after the minus, make the minus part of the suffix
|
||||
if idx == 1 and start == 1:
|
||||
idx = 0
|
||||
|
||||
part1 = text[0:idx]
|
||||
part2 = text[idx:len(text)]
|
||||
|
||||
if part1 != "":
|
||||
self.num = float( part1 )
|
||||
self.suffix = part2
|
||||
else:
|
||||
self.suffix = text
|
||||
|
||||
#print "num: {0} suf: {1}".format(self.num, self.suffix)
|
||||
|
||||
def asString( self, pad = 0 ):
|
||||
#return the float, left side zero-padded, with suffix attached
|
||||
|
@ -3,7 +3,7 @@ A PyQT4 dialog to a text file or log
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -3,7 +3,7 @@ A python app to (automatically) tag comic archives
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -3,7 +3,7 @@ A PyQT4 dialog to select from automated issue matches
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -3,7 +3,7 @@ A PyQt4 dialog to show a message and let the user check a box
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -3,7 +3,7 @@ CLI options class for comictagger app
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -25,6 +25,11 @@ import os
|
||||
import traceback
|
||||
import ctversion
|
||||
import utils
|
||||
try:
|
||||
import argparse
|
||||
except:
|
||||
pass
|
||||
|
||||
from genericmetadata import GenericMetadata
|
||||
from comicarchive import MetaDataStyle
|
||||
from versionchecker import VersionChecker
|
||||
@ -175,6 +180,41 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/
|
||||
#print md
|
||||
return md
|
||||
|
||||
def launch_script(self, scriptfile):
|
||||
# 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(scriptfile):
|
||||
print "Can't find {0}".format( scriptfile )
|
||||
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(scriptfile)
|
||||
module_name = os.path.splitext(os.path.basename(scriptfile))[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)
|
||||
|
||||
def parseCmdLineArgs(self):
|
||||
|
||||
if platform.system() == "Darwin" and hasattr(sys, "frozen") and sys.frozen == 1:
|
||||
@ -183,6 +223,15 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/
|
||||
else:
|
||||
input_args = sys.argv[1:]
|
||||
|
||||
# first check if we're launching a script:
|
||||
for n in range(len(input_args)):
|
||||
if ( input_args[n] in [ "-S", "--script" ] and
|
||||
n+1 < len(input_args)):
|
||||
# insert a "--" which will cause getopt to ignore the remaining args
|
||||
# so they will be passed to the script
|
||||
input_args.insert(n+2, "--")
|
||||
break
|
||||
|
||||
# parse command line options
|
||||
try:
|
||||
opts, args = getopt.getopt( input_args,
|
||||
@ -255,10 +304,10 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/
|
||||
if o == "--nooverwrite":
|
||||
self.no_overwrite = True
|
||||
if o == "--version":
|
||||
print "ComicTagger {0}: Copyright (c) 2012-2013 Anthony Beville".format(ctversion.version)
|
||||
print "ComicTagger {0}: Copyright (c) 2012-2014 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:
|
||||
if new_version is not None and new_version != ctversion.version:
|
||||
print "----------------------------------------"
|
||||
print "New version available online: {0}".format(new_version)
|
||||
print "----------------------------------------"
|
||||
@ -289,40 +338,8 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/
|
||||
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)
|
||||
|
||||
self.launch_script( self.script )
|
||||
|
||||
if len(args) > 0:
|
||||
if platform.system() == "Windows":
|
||||
# no globbing on windows shell, so do it for them
|
||||
|
@ -3,7 +3,7 @@ A PyQT4 dialog to show pages of a comic archive
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -3,7 +3,7 @@ A PyQt4 widget for editing the page list info
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -3,7 +3,7 @@ A PyQT4 class to load a page image from a ComicArchive in a background thread
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -3,7 +3,7 @@ A PyQT4 dialog to show ID log and progress
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -3,7 +3,7 @@ A PyQT4 dialog to confirm rename
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -3,7 +3,7 @@ Settings class for comictagger app
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -3,7 +3,7 @@ A PyQT4 dialog to enter app settings
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -4,7 +4,7 @@ The main window of the ComicTagger app
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -220,7 +220,8 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.settings.send_usage_stats = True
|
||||
self.settings.ask_about_usage_stats = False
|
||||
|
||||
self.checkLatestVersionOnline()
|
||||
if self.settings.check_for_new_version:
|
||||
self.checkLatestVersionOnline()
|
||||
|
||||
def sigint_handler(self, *args):
|
||||
# defer the actual close in the app loop thread
|
||||
@ -330,7 +331,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.actionApplyCBLTransform.setStatusTip( 'Modify tags specifically for CBL format' )
|
||||
self.actionApplyCBLTransform.triggered.connect( self.applyCBLTransform )
|
||||
|
||||
#self.actionClearEntryForm.setShortcut( 'Ctrl+C' )
|
||||
self.actionClearEntryForm.setShortcut( 'Ctrl+Shift+C' )
|
||||
self.actionClearEntryForm.setStatusTip( 'Clear all the data on the screen' )
|
||||
self.actionClearEntryForm.triggered.connect( self.clearForm )
|
||||
|
||||
@ -472,7 +473,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
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>"
|
||||
+ "(c)2014 Anthony Beville<br><br>"
|
||||
+ "<a href='{0}'>{0}</a><br><br>".format(website)
|
||||
+ "<a href='mailto:{0}'>{0}</a><br><br>".format(email)
|
||||
+ "License: <a href='{0}'>{1}</a>".format(license_link, license_name) )
|
||||
@ -926,14 +927,17 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
else:
|
||||
QtGui.QMessageBox.information(self, self.tr("Online Search"), self.tr("Need to enter a series name to search."))
|
||||
return
|
||||
|
||||
|
||||
|
||||
year = str(self.lePubYear.text()).strip()
|
||||
if year == "":
|
||||
year = None
|
||||
|
||||
issue_count = str(self.leIssueCount.text()).strip()
|
||||
if issue_count == "":
|
||||
issue_count = None
|
||||
|
||||
cover_index_list = self.metadata.getCoverPageIndexList()
|
||||
selector = VolumeSelectionWindow( self, series_name, issue_number, year, cover_index_list, self.comic_archive, self.settings, autoselect )
|
||||
selector = VolumeSelectionWindow( self, series_name, issue_number, year, issue_count, cover_index_list, self.comic_archive, self.settings, autoselect )
|
||||
|
||||
title = "Search: '" + series_name + "' - "
|
||||
selector.setWindowTitle( title + "Select Series")
|
||||
|
@ -21,6 +21,44 @@
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="1">
|
||||
<widget class="QWidget" name="archiveCoverContainer">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="4">
|
||||
<widget class="QWidget" name="testCoverContainer">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QProgressBar" name="progressBar">
|
||||
@ -66,50 +104,6 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="lblArchive">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="lblTest">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
|
@ -5,7 +5,7 @@ Some generic utilities
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -126,9 +126,13 @@ def removearticles( text ):
|
||||
|
||||
# now get rid of some other junk
|
||||
newText = newText.replace(":", "")
|
||||
newText = newText.replace(".", "")
|
||||
newText = newText.replace(",", "")
|
||||
newText = newText.replace("-", " ")
|
||||
|
||||
# since the CV api changed, searches for series names with periods
|
||||
# now explicity require the period to be in the search key,
|
||||
# so the line below is removed (for now)
|
||||
#newText = newText.replace(".", "")
|
||||
|
||||
return newText
|
||||
|
||||
|
@ -24,7 +24,7 @@ import urllib,urllib2
|
||||
import ctversion
|
||||
|
||||
try:
|
||||
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
||||
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
|
||||
from PyQt4.QtCore import QUrl, pyqtSignal, QObject, QByteArray
|
||||
except ImportError:
|
||||
# No Qt, so define a few dummy QObjects to help us compile
|
||||
@ -82,6 +82,9 @@ class VersionChecker(QObject):
|
||||
self.nam.get(QNetworkRequest(QUrl(str(url))))
|
||||
|
||||
def asyncGetLatestVersionComplete( self, reply ):
|
||||
if (reply.error() != QNetworkReply.NoError):
|
||||
return
|
||||
|
||||
# read in the response
|
||||
new_version = str(reply.readAll())
|
||||
|
||||
|
@ -3,7 +3,7 @@ A PyQT4 dialog to select specific series/volume from list
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
Copyright 2012-2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -87,7 +87,7 @@ class IdentifyThread( QtCore.QThread):
|
||||
|
||||
class VolumeSelectionWindow(QtGui.QDialog):
|
||||
|
||||
def __init__(self, parent, series_name, issue_number, year, cover_index_list, comic_archive, settings, autoselect=False):
|
||||
def __init__(self, parent, series_name, issue_number, year, issue_count, cover_index_list, comic_archive, settings, autoselect=False):
|
||||
super(VolumeSelectionWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('volumeselectionwindow.ui' ), self)
|
||||
@ -108,6 +108,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
self.series_name = series_name
|
||||
self.issue_number = issue_number
|
||||
self.year = year
|
||||
self.issue_count = issue_count
|
||||
self.volume_id = 0
|
||||
self.comic_archive = comic_archive
|
||||
self.immediate_autoselect = autoselect
|
||||
@ -161,6 +162,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
md.series = self.series_name
|
||||
md.issue = self.issue_number
|
||||
md.year = self.year
|
||||
md.issueCount = self.issue_count
|
||||
|
||||
self.ii.setAdditionalMetadata( md )
|
||||
self.ii.onlyUseAdditionalMetaData = True
|
||||
|
@ -1,6 +1,6 @@
|
||||
#PYINSTALLER_CMD := VERSIONER_PYTHON_PREFER_32_BIT=yes arch -i386 python $(HOME)/pyinstaller-2.0/pyinstaller.py
|
||||
PYINSTALLER_CMD := python $(HOME)/pyinstaller-2.0/pyinstaller.py
|
||||
TAGGER_BASE := $(HOME)/Dropbox/tagger/comictagger
|
||||
TAGGER_BASE ?= $(HOME)/Dropbox/tagger/comictagger
|
||||
TAGGER_SRC := $(TAGGER_BASE)/comictaggerlib
|
||||
|
||||
APP_NAME := ComicTagger
|
||||
|
@ -1,3 +1,36 @@
|
||||
---------------------------------
|
||||
1.1.12-beta - 23-Mar-2014
|
||||
---------------------------------
|
||||
* Fixed noisy version update error
|
||||
|
||||
---------------------------------
|
||||
1.1.11-beta - 23-Mar-2014
|
||||
---------------------------------
|
||||
* Updated unrar library to hand Rar tools 5.0 and greater
|
||||
* Other misc bug fixes
|
||||
|
||||
---------------------------------
|
||||
1.1.10-beta - 30-Jan-2014
|
||||
---------------------------------
|
||||
* Updated series query to match changes on Comic Vine side
|
||||
* Added a message when not able to open a file or folder
|
||||
* Fixed an issue where series names with periods would fail on search
|
||||
* Other misc bug fixes
|
||||
|
||||
---------------------------------
|
||||
1.1.9-beta - 8-May-2013
|
||||
---------------------------------
|
||||
* Filename parser and identification enhancements
|
||||
* Misc bug fixes
|
||||
|
||||
---------------------------------
|
||||
1.1.8-beta - 21-Apr-2013
|
||||
---------------------------------
|
||||
* Handle occasional error 500 from Comic Vine by retrying a few times
|
||||
* Nicer handling of colon (":") in file rename
|
||||
* Fixed command-line option parsing issue for add-on scripts
|
||||
* Misc bug fixes
|
||||
|
||||
---------------------------------
|
||||
1.1.7-beta - 12-Apr-2013
|
||||
---------------------------------
|
||||
|
151
scripts/name_fixer.py
Executable file
151
scripts/name_fixer.py
Executable file
@ -0,0 +1,151 @@
|
||||
#!/usr/bin/python
|
||||
"""
|
||||
fix the comic file names using a list of transforms
|
||||
"""
|
||||
|
||||
"""
|
||||
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 re
|
||||
import argparse
|
||||
import json
|
||||
|
||||
from comictaggerlib.comicarchive import *
|
||||
from comictaggerlib.settings import *
|
||||
from comictaggerlib.filerenamer import *
|
||||
import comictaggerlib.utils
|
||||
|
||||
def parse_args():
|
||||
|
||||
input_args = sys.argv[1:]
|
||||
|
||||
parser = argparse.ArgumentParser(description='a script to rename comic files')
|
||||
parser.add_argument('-t', '--transforms', metavar='xformfile', help="the file with transforms")
|
||||
parser.add_argument('-n', '--noconfirm', action='store_true', help="don't confirm before rename")
|
||||
parser.add_argument('paths', metavar='PATH', type=str, nargs='+', help='path to look for comic files')
|
||||
parsed_args = parser.parse_args(input_args)
|
||||
|
||||
return parsed_args
|
||||
|
||||
def caclulate_rename(ca, md, settings):
|
||||
|
||||
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( "%series% V%volume% #%issue% (of %issuecount%) (%year%) %scaninfo%" )
|
||||
renamer.setIssueZeroPadding( 0 )
|
||||
renamer.setSmartCleanup( settings.rename_use_smart_string_cleanup )
|
||||
|
||||
return renamer.determineName( ca.path, ext=new_ext )
|
||||
|
||||
def perform_rename(filelist):
|
||||
for old_name,new_name in filelist:
|
||||
folder = os.path.dirname( os.path.abspath( old_name ) )
|
||||
new_abs_path = utils.unique_file( os.path.join( folder, new_name ) )
|
||||
|
||||
os.rename( old_name, new_abs_path )
|
||||
print u"renamed '{0}' -> '{1}'".format(os.path.basename(old_name), new_name)
|
||||
|
||||
def main():
|
||||
|
||||
default_xform_list = [
|
||||
[ "^2000AD$", "2000 AD" ],
|
||||
[ "^G\.{0,1}I\.{0,1}Joe$", "G.I. Joe" ],
|
||||
]
|
||||
|
||||
utils.fix_output_encoding()
|
||||
settings = ComicTaggerSettings()
|
||||
|
||||
style = MetaDataStyle.CIX
|
||||
|
||||
parsed_args = parse_args()
|
||||
|
||||
#parsed_args.noconfirm
|
||||
if parsed_args.transforms is not None:
|
||||
print "Reading in transforms from:", parsed_args.transforms
|
||||
json_data=open(parsed_args.transforms).read()
|
||||
data = json.loads(json_data)
|
||||
xform_list = data['xforms']
|
||||
else:
|
||||
xform_list = default_xform_list
|
||||
|
||||
#pprint( xform_list, indent=4)
|
||||
|
||||
filelist = utils.get_recursive_filelist( parsed_args.paths )
|
||||
|
||||
#first find all comics
|
||||
print "reading in all comics..."
|
||||
comic_list = []
|
||||
max_name_len = 2
|
||||
fmt_str = ""
|
||||
for filename in filelist:
|
||||
ca = ComicArchive(filename, settings )
|
||||
# do we care if it already has metadata?
|
||||
if ca.seemsToBeAComicArchive() and not ca.hasMetadata( style ):
|
||||
|
||||
comic_list.append(ca)
|
||||
|
||||
max_name_len = max ( max_name_len, len(filename))
|
||||
fmt_str = u"{{0:{0}}}".format(max_name_len)
|
||||
print >> sys.stderr, fmt_str.format( filename ) + "\r",
|
||||
sys.stderr.flush()
|
||||
|
||||
print >> sys.stderr, fmt_str.format( "" )
|
||||
print "Found {0} comics.".format( len(comic_list))
|
||||
|
||||
modify_list = list()
|
||||
# walk through the comic list fix the filenames
|
||||
for ca in comic_list:
|
||||
|
||||
# 1. parse the filename into a MD object
|
||||
md = ca.metadataFromFilename()
|
||||
# 2. walk thru list of transforms
|
||||
if md.series is not None and md.series != "":
|
||||
for pattern, replacement in xform_list:
|
||||
# apply each transform
|
||||
new_series = re.sub( pattern, replacement, md.series )
|
||||
if new_series != md.series:
|
||||
md.series = new_series
|
||||
new_name = caclulate_rename(ca, md, settings)
|
||||
|
||||
#found a match. add to proposed list, and bail on this file
|
||||
modify_list.append( (ca.path, new_name ) )
|
||||
break
|
||||
|
||||
print "{0} filenames to modify".format(len(modify_list))
|
||||
if len(modify_list) > 0:
|
||||
if parsed_args.noconfirm:
|
||||
print "Not confirming before rename"
|
||||
else:
|
||||
for old_name, new_name in modify_list:
|
||||
print u"'{0}' -> '{1}'".format(os.path.basename(old_name), new_name)
|
||||
|
||||
i = raw_input("Do you want to proceed with rename? [y/N] ")
|
||||
if i.lower() not in ('y', 'yes'):
|
||||
print "exiting without rename."
|
||||
sys.exit(0)
|
||||
|
||||
perform_rename(modify_list)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
8
scripts/xforms
Normal file
8
scripts/xforms
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"_comment": "This file contains JSON data. Any backslashes should be escaped with another backslash",
|
||||
"xforms":
|
||||
[
|
||||
[ "^2000AD$", "2000 AD" ],
|
||||
[ "^G\\.{0,1}I\\.{0,1}Joe$", "G.I. Joe" ]
|
||||
]
|
||||
}
|
3
setup.py
3
setup.py
@ -9,7 +9,8 @@ setup(name = "comictagger",
|
||||
author = "Anthony Beville",
|
||||
author_email = "comictagger@gmail.com",
|
||||
url = "http://code.google.com/p/comictagger/",
|
||||
download_url = "http://comictagger.googlecode.com/files/comictagger-{0}.zip".format(comictaggerlib.ctversion.version),
|
||||
#download_url = "http://comictagger.googlecode.com/files/comictagger-{0}.zip".format(comictaggerlib.ctversion.version),
|
||||
download_url = "https://drive.google.com/uc?export=download&id=0Bw4IursaqWhhTjZ6UDB1aEx1am8",
|
||||
packages = [ "comictaggerlib", "comictaggerlib/UnRAR2" ] ,
|
||||
package_data = {
|
||||
'comictaggerlib': ['ui/*.ui', 'graphics/*'] ,
|
||||
|
34
todo.txt
34
todo.txt
@ -4,13 +4,26 @@ Features
|
||||
Rename dialog:
|
||||
check-box for rows?
|
||||
manual edit the preview?
|
||||
|
||||
|
||||
Maybe replace configparser -- seems to be causing all sorts of problems
|
||||
|
||||
Feature Requests:
|
||||
Move CBR to other folder after conversion to ZIP
|
||||
|
||||
NO - AUto-rename on auto-tag
|
||||
NO- Re-zip (to remove compression)
|
||||
Pre-process series name before identification
|
||||
(using a list of regex transforms)
|
||||
(GC #28) Save auto-tag options
|
||||
(GC #24) Multiple options for -t i.e. "-t cr,cbl"
|
||||
(GC #18 ) Option for handling colon in rename
|
||||
(GC #31 ) Specify CV Series ID for auto-tag
|
||||
Re-org - move to new folder based on template
|
||||
repair incorrect extension
|
||||
|
||||
Denied Requests (for now):
|
||||
Auto-rename on auto-tag
|
||||
Re-zip (to remove compression)
|
||||
|
||||
|
||||
Selective fields on CLI print (use -m option. Maybe internally remove all but specified fields in MD object before print )
|
||||
|
||||
Docs:
|
||||
Auto-Tagging Tips:
|
||||
@ -20,18 +33,18 @@ Docs:
|
||||
Bugs
|
||||
-----------------------------------------------------
|
||||
|
||||
Non-numeric issues?? Filename parsing... can we only rely on '#'??
|
||||
|
||||
|
||||
Zip flakes out when filename differs from index (or whatever) i.e "\" vs "/". Python issue
|
||||
|
||||
|
||||
|
||||
-----------------------------------------------------
|
||||
Big Future Features
|
||||
-----------------------------------------------------
|
||||
Support for ACBF metatdata in CBZ
|
||||
|
||||
GCD scraper or DB reader
|
||||
|
||||
Batch Edit
|
||||
(GC #29) Batch Edit
|
||||
Form Mode: Single vs Batch
|
||||
|
||||
-----------------------------------------------------
|
||||
@ -74,7 +87,10 @@ Release Process
|
||||
Make dmg on Mac
|
||||
Make zip on Mac or Linux
|
||||
Tag the repository
|
||||
Upload packages
|
||||
Manually upload release packages to Google Drive
|
||||
Update the Downloads wiki page with direct links
|
||||
Edit setup.py to add direct link to zip
|
||||
"make upload"
|
||||
Announce on Forum and Main Page and Twitter
|
||||
MacUpdate
|
||||
Update appspot value
|
||||
|
@ -2,7 +2,7 @@
|
||||
# rm, cp, grep, cut, cat
|
||||
|
||||
HOMEPATH ?= $(HOME)
|
||||
TAGGER_BASE:= $(HOMEPATH)/Dropbox/tagger/comictagger
|
||||
TAGGER_BASE?= $(HOMEPATH)/Dropbox/tagger/comictagger
|
||||
TAGGER_SRC := $(TAGGER_BASE)/comictaggerlib
|
||||
DIST_DIR := $(TAGGER_BASE)\windows\dist
|
||||
NSIS_CMD := "C:\Program Files (x86)\NSIS\makensis.exe"
|
||||
|
@ -1,4 +1,4 @@
|
||||
ComicTagger - Copyright 2012 Anthony Beville
|
||||
ComicTagger - Copyright 2014 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
Reference in New Issue
Block a user