diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py
index 33d43f9..d33941d 100644
--- a/comicapi/comicarchive.py
+++ b/comicapi/comicarchive.py
@@ -29,7 +29,7 @@ import io
#import shutil
from natsort import natsorted
-from PyPDF2 import PdfFileReader
+# from PyPDF2 import PdfFileReader
try:
from unrar import rarfile
from unrar import unrarlib
@@ -915,7 +915,7 @@ class ComicArchive:
# k = os.path.join(os.path.split(k)[0], "z" + basename)
return k.lower()
- files = natsorted(files, key=keyfunc, signed=False)
+ files = natsorted(files, key=keyfunc)
# make a sub-list of image files
self.page_list = []
diff --git a/comictagger.sublime-project b/comictagger.sublime-project
new file mode 100644
index 0000000..78978f5
--- /dev/null
+++ b/comictagger.sublime-project
@@ -0,0 +1,17 @@
+{
+ "build_systems":
+ [
+ {
+ "file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)",
+ "name": "Anaconda Python Builder",
+ "selector": "source.python",
+ "shell_cmd": "\"python3\" -u \"$file\""
+ }
+ ],
+ "folders":
+ [
+ {
+ "path": "."
+ }
+ ]
+}
diff --git a/comictagger.sublime-workspace b/comictagger.sublime-workspace
new file mode 100644
index 0000000..67586d4
--- /dev/null
+++ b/comictagger.sublime-workspace
@@ -0,0 +1,1195 @@
+{
+ "auto_complete":
+ {
+ "selected_items":
+ [
+ [
+ "dupe",
+ "dupe_set\tparam"
+ ],
+ [
+ "score",
+ "score_list\tstatement"
+ ],
+ [
+ "du",
+ "dupe_set\tparam"
+ ],
+ [
+ "nex",
+ "nextHash_list\tstatement"
+ ],
+ [
+ "ne",
+ "nextHash_list\tstatement"
+ ],
+ [
+ "a",
+ "add"
+ ],
+ [
+ "image",
+ "imageHash"
+ ],
+ [
+ "ima",
+ "imageHash\tstatement"
+ ],
+ [
+ "im",
+ "imageHash\tstatement"
+ ],
+ [
+ "dup",
+ "dupe_set"
+ ],
+ [
+ "sel",
+ "selection"
+ ],
+ [
+ "rec",
+ "recordName"
+ ],
+ [
+ "search",
+ "search_series_name"
+ ],
+ [
+ "wre",
+ "writeLog"
+ ],
+ [
+ "et",
+ "extras"
+ ],
+ [
+ "extra",
+ "extras"
+ ],
+ [
+ "dupli",
+ "duplicateImages"
+ ],
+ [
+ "all",
+ "allSameCount\tstatement"
+ ],
+ [
+ "file",
+ "fileCount"
+ ],
+ [
+ "al",
+ "allDeleteable\tstatement"
+ ],
+ [
+ "ext",
+ "extract\tfunction"
+ ],
+ [
+ "for",
+ "format\tfunction"
+ ],
+ [
+ "de",
+ "def\tFunction"
+ ],
+ [
+ "new",
+ "new_dupe_set\tstatement"
+ ],
+ [
+ "del",
+ "delete"
+ ],
+ [
+ "whe",
+ "while\tWhile Loop"
+ ],
+ [
+ "k",
+ "keeping"
+ ],
+ [
+ "ar",
+ "archiveFile"
+ ],
+ [
+ "ex",
+ "extension"
+ ],
+ [
+ "arch",
+ "archivedFile"
+ ],
+ [
+ "zip",
+ "zipfile"
+ ],
+ [
+ "def",
+ "def\tFunction"
+ ],
+ [
+ "me",
+ "metadata"
+ ],
+ [
+ "f",
+ "f"
+ ],
+ [
+ "try",
+ "try\tTry/Except"
+ ],
+ [
+ "non",
+ "None"
+ ],
+ [
+ "wor",
+ "word_list"
+ ],
+ [
+ "url",
+ "urllib"
+ ],
+ [
+ "plat",
+ "platform"
+ ],
+ [
+ "req",
+ "requests"
+ ],
+ [
+ "pars",
+ "parsedYear"
+ ],
+ [
+ "api",
+ "api_key"
+ ],
+ [
+ "cou",
+ "count_of_isssues"
+ ],
+ [
+ "start",
+ "start_year"
+ ],
+ [
+ "nam",
+ "name"
+ ],
+ [
+ "fie",
+ "field_list"
+ ],
+ [
+ "status",
+ "status_code"
+ ],
+ [
+ "series",
+ "seriesYear"
+ ],
+ [
+ "auto",
+ "auto_imprint"
+ ],
+ [
+ "pu",
+ "publisher"
+ ],
+ [
+ "form",
+ "formToMetadata"
+ ],
+ [
+ "cl",
+ "clicked"
+ ],
+ [
+ "pub",
+ "publisher"
+ ],
+ [
+ "p",
+ "publisher"
+ ],
+ [
+ "get",
+ "getPublisher"
+ ],
+ [
+ "Mar",
+ "MarvelImprints"
+ ],
+ [
+ "Imp",
+ "ImprintDict"
+ ],
+ [
+ "DC",
+ "DC_Comics"
+ ],
+ [
+ "Marv",
+ "Marvel"
+ ],
+ [
+ "fmt",
+ "fmtObj"
+ ],
+ [
+ "No",
+ "None"
+ ],
+ [
+ "T",
+ "True"
+ ],
+ [
+ "cbM",
+ "cbMaturityRating"
+ ],
+ [
+ "F",
+ "False"
+ ],
+ [
+ "i",
+ "if\tif … fi"
+ ],
+ [
+ "part",
+ "part2"
+ ],
+ [
+ "par",
+ "part2"
+ ],
+ [
+ "md",
+ "md5sum"
+ ],
+ [
+ "com",
+ "comicnumber"
+ ],
+ [
+ "comi",
+ "comic1"
+ ],
+ [
+ "e",
+ "ExitOnError\tflag.ErrorHandling ·Ɩ"
+ ],
+ [
+ "go",
+ "gopherType\tstring ·ν"
+ ],
+ [
+ "str",
+ "string\tstring ·ʈ"
+ ],
+ [
+ "int",
+ "int64\tint64 ·ʈ"
+ ],
+ [
+ "Int64",
+ "Int64Var\tfunc(p *int64, name string, value int64, usage string) ·ƒ"
+ ],
+ [
+ "fl",
+ "float64\tfloat64 ·ʈ"
+ ],
+ [
+ "P",
+ "Println\tfunc(a ...interface{}) int, error ·ƒ"
+ ],
+ [
+ "print",
+ "Printf\tfunc(format string, a ...interface{}) int, error ·ƒ"
+ ],
+ [
+ "pri",
+ "Println\tfunc(a ...interface{}) int, error ·ƒ"
+ ],
+ [
+ "op",
+ "OpenFile\tfunc(name string, flag int, perm os.FileMode) *os.File, error ·ƒ"
+ ],
+ [
+ "if",
+ "if err\terr != nil { return } ·ʂ"
+ ],
+ [
+ "Pla",
+ "PlayItem\tstruct ·ʈ"
+ ],
+ [
+ "uint",
+ "uint16\tuint16 ·ʈ"
+ ],
+ [
+ "Ui",
+ "uint64\tuint64 ·ʈ"
+ ],
+ [
+ "type",
+ "type struct\tstruct {} ·ʂ"
+ ],
+ [
+ "PrimaryIG",
+ "PrimaryIGStreams\t[]PrimaryStream ·ν"
+ ],
+ [
+ "ma",
+ "make\tfunc(Type, size IntegerType) Type ·ƒ"
+ ],
+ [
+ "Str",
+ "StreamID\t[]byte ·ν"
+ ],
+ [
+ "app",
+ "append\tappend(stnt.SecondaryAudioStreams, ...) ·ʂ"
+ ],
+ [
+ "func",
+ "func method SecondaryStream\t(SecondaryStream) method() {...} ·ʂ"
+ ],
+ [
+ "rea",
+ "reader\t*errReader ·ν"
+ ],
+ [
+ "tye",
+ "type struct\tstruct {} ·ʂ"
+ ],
+ [
+ "o",
+ "object\tiron.object.Object ·ν"
+ ],
+ [
+ "on",
+ "onKeyDown"
+ ],
+ [
+ "ke",
+ "KeyCode"
+ ],
+ [
+ "key",
+ "KeyCode\t·ʈ"
+ ],
+ [
+ "arm",
+ "armory\t package ·ρ"
+ ],
+ [
+ "sys",
+ "System\t·ʈ"
+ ],
+ [
+ "pro",
+ "projectPath\tstatement"
+ ],
+ [
+ "ha",
+ "haxeExec\tstatement"
+ ],
+ [
+ "no",
+ "normpath\tfunction"
+ ],
+ [
+ "none",
+ "None\tkeyword"
+ ],
+ [
+ "hxm",
+ "hxmlList\tstatement"
+ ],
+ [
+ "kha",
+ "khamakePath\tstatement"
+ ],
+ [
+ "node",
+ "nodePath"
+ ],
+ [
+ "path",
+ "pathEnd\tstatement"
+ ],
+ [
+ "Para",
+ "Paramaters\tstatement"
+ ],
+ [
+ "Com",
+ "CompletionTrigger"
+ ],
+ [
+ "xml",
+ "xmlItem\tparam"
+ ],
+ [
+ "Par",
+ "ParseMethod\tfunction"
+ ],
+ [
+ "C",
+ "Completion\tstatement"
+ ],
+ [
+ "param",
+ "ParamReturnList\tstatement"
+ ],
+ [
+ "Compl",
+ "CompletionHint\tstatement"
+ ],
+ [
+ "Conp",
+ "Completion\tstatement"
+ ],
+ [
+ "Ty",
+ "TypeClassThingyMajig\tstatement"
+ ],
+ [
+ "ty",
+ "TypeClassThingyMajig"
+ ],
+ [
+ "te",
+ "text"
+ ],
+ [
+ "co",
+ "CompletionTrigger"
+ ],
+ [
+ "HaxeE",
+ "haxeErr\tstatement"
+ ],
+ [
+ "Completion",
+ "CompletionTrigger\tstatement"
+ ],
+ [
+ "haxe",
+ "haxeType\tstatement"
+ ],
+ [
+ "comp",
+ "completions"
+ ],
+ [
+ "hax",
+ "haxeErr\tstatement"
+ ],
+ [
+ "su",
+ "substr"
+ ],
+ [
+ "compl",
+ "CompletionType\tstatement"
+ ],
+ [
+ "cla",
+ "classString\tstatement"
+ ],
+ [
+ "classi",
+ "classification"
+ ],
+ [
+ "loc",
+ "locations\tparam"
+ ]
+ ]
+ },
+ "buffers":
+ [
+ ],
+ "build_system": "",
+ "build_system_choices":
+ [
+ ],
+ "build_varint": "",
+ "command_palette":
+ {
+ "height": 128.0,
+ "last_filter": "",
+ "selected_items":
+ [
+ [
+ "mer",
+ "Sublime Merge: Open Repository"
+ ],
+ [
+ "ins",
+ "Package Control: Install Package"
+ ],
+ [
+ "rem",
+ "Package Control: Remove Package"
+ ],
+ [
+ "tr",
+ "Trimmer: Trim trailing whitespace."
+ ],
+ [
+ "ind",
+ "Indent XML"
+ ],
+ [
+ "pyth",
+ "Set Syntax: Python"
+ ],
+ [
+ "pla",
+ "Set Syntax: Plain Text Mono"
+ ],
+ [
+ "trim",
+ "Trimmer: Trim trailing whitespace."
+ ],
+ [
+ "insta",
+ "Package Control: Install Package"
+ ],
+ [
+ "inde",
+ "Indent XML"
+ ],
+ [
+ "sort",
+ "Sort Lines"
+ ],
+ [
+ "low",
+ "Convert Case: Lower Case"
+ ],
+ [
+ "pep",
+ "AutoPEP8: Preview Changes"
+ ],
+ [
+ "install",
+ "Package Control: Install Package"
+ ],
+ [
+ "indent",
+ "Auto indent"
+ ],
+ [
+ "un",
+ "Permute Lines: Unique"
+ ],
+ [
+ "sor",
+ "Sort Lines"
+ ],
+ [
+ "in",
+ "Indent XML"
+ ],
+ [
+ "rein",
+ "Indentation: Reindent Lines"
+ ],
+ [
+ "previe",
+ "Set Syntax: Man Page Preview"
+ ],
+ [
+ "man page",
+ "Set Syntax: Man Page (groff/troff)"
+ ],
+ [
+ "remo",
+ "Package Control: Remove Package"
+ ],
+ [
+ "manp",
+ "Set Syntax: Man Page Preview"
+ ],
+ [
+ "man",
+ "Manpage: Open manual page"
+ ],
+ [
+ "xml",
+ "Indent XML"
+ ],
+ [
+ "repo",
+ "Sublime Merge: Open Repository"
+ ],
+ [
+ "auto",
+ "AutoPEP8: Format Code"
+ ],
+ [
+ "istall",
+ "Package Control: Install Package"
+ ],
+ [
+ "case",
+ "Convert Case: Title Case"
+ ],
+ [
+ "bash",
+ "Set Syntax: Bourne Again Shell (bash)"
+ ],
+ [
+ "uni",
+ "Permute Lines: Unique"
+ ],
+ [
+ "up",
+ "Convert Case: Upper Case"
+ ],
+ [
+ "tax go",
+ "Set Syntax: GoSublime: Go (Recommended)"
+ ],
+ [
+ "tit",
+ "Convert Case: Title Case"
+ ],
+ [
+ "package",
+ "View Package File"
+ ],
+ [
+ "next",
+ "Anaconda: Next lint error"
+ ],
+ [
+ "xm",
+ "Set Syntax: XML"
+ ],
+ [
+ "sho",
+ "Anaconda: Show error list"
+ ],
+ [
+ "view",
+ "View Package File"
+ ],
+ [
+ "scope",
+ "Toggle ScopeAlways"
+ ],
+ [
+ "upper",
+ "Convert Case: Upper Case"
+ ],
+ [
+ "nex",
+ "Anaconda: Next lint error"
+ ],
+ [
+ "syntax: py",
+ "Set Syntax: Python"
+ ],
+ [
+ "theme",
+ "UI: Select Color Scheme"
+ ]
+ ],
+ "width": 479.0
+ },
+ "console":
+ {
+ "height": 340.0,
+ "history":
+ [
+ "clear",
+ "import sublime;sublime.platform()",
+ "htesp"
+ ]
+ },
+ "distraction_free":
+ {
+ "menu_visible": true,
+ "show_minimap": false,
+ "show_open_files": false,
+ "show_tabs": false,
+ "side_bar_visible": false,
+ "status_bar_visible": false
+ },
+ "expanded_folders":
+ [
+ "/home/timmy/build/source/comictagger-develop",
+ "/home/timmy/build/source/comictagger-develop/scripts"
+ ],
+ "file_history":
+ [
+ "/home/timmy/Videos/October Baby.tv",
+ "/home/timmy/Videos/all.tv",
+ "/home/timmy/Videos/Mrs_Doubtfire.tv",
+ "/home/timmy/build/source/comictagger-develop/comictaggerlib/issueselectionwindow.py",
+ "/home/timmy/build/source/comictagger-develop/scripts/find_dupes.py",
+ "/home/timmy/Videos/dupe_cmp/Absolute Carnage vs. Deadpool #002 (2019) (1).cbr/ComicInfo.xml",
+ "/home/timmy/build/source/comictagger-develop/comictaggerlib/imagehasher.py",
+ "/home/timmy/Downloads/amdgpu-pro-preinstall.sh",
+ "/home/timmy/comics/fix.sh",
+ "/home/timmy/build/source/comictagger-develop/comictaggerlib/main.py",
+ "/home/timmy/build/source/comictagger-develop/scripts/cmp.py",
+ "/home/timmy/build/source/comictagger-develop/comictaggerlib/renamewindow.py",
+ "/home/timmy/build/source/comictagger-develop/comictaggerlib/fileselectionlist.py",
+ "/home/timmy/build/source/comictagger-develop/comictaggerlib/pagelisteditor.py",
+ "/home/timmy/build/source/comictagger-develop/comictaggerlib/coverimagewidget.py",
+ "/home/timmy/comics/tmp/Final Crisis (DC Essential Edition) #001 - TPB (2019).cbr",
+ "/home/timmy/build/source/comictagger-develop/comictaggerlib/issueidentifier.py",
+ "/home/timmy/build/source/comictagger-develop/comictaggerlib/taggerwindow.py",
+ "/home/timmy/bin/videoCorrectAudio.sh",
+ "/home/timmy/build/source/comictagger-develop/comictaggerlib/comicvinetalker.py",
+ "/home/timmy/build/source/comictagger-develop/requirements.txt",
+ "/home/timmy/.config/sublime-text-3/Packages/Anaconda/Anaconda.sublime-settings",
+ "/home/timmy/.config/sublime-text-3/Packages/User/Anaconda.sublime-settings",
+ "/home/timmy/build/source/comictagger-develop/comicapi/utils.py",
+ "/home/timmy/.local/lib/python3.6/site-packages/unrar/unrarlib.py",
+ "/home/timmy/.local/lib/python3.6/site-packages/unrar/__init__.py",
+ "/home/timmy/.local/lib/python3.6/site-packages/unrar/constants.py",
+ "/home/timmy/.local/lib/python3.6/site-packages/unrar/rarfile.py",
+ "/home/timmy/comics/failed/Batman 086 (2020) (Digital) (Zone-Empire).cbr",
+ "/home/timmy/comics/complete/What If V2 086 ..The Scarlet Spider Had Killed Spider-Man.cbr",
+ "/home/timmy/comics/manual/01 Ara.cbr",
+ "/home/timmy/comics/manual/Blade of the Immortal v02 (1998) (Digital) (LuCaZ).cbz",
+ "/home/timmy/tmp.py",
+ "/home/timmy/bin/dup-comic.sh",
+ "/home/timmy/.local/lib/python3.6/site-packages/comictaggerlib/comicvinetalker.py",
+ "/home/timmy/build/source/comictagger-develop/comictaggerlib/comicvinecacher.py",
+ "/home/timmy/.local/lib/python3.6/site-packages/comicapi/utils.py",
+ "/home/timmy/.local/lib/python3.6/site-packages/comictaggerlib/utils.py",
+ "/home/timmy/.config/sublime-text-3/Packages/Anaconda/Default (Linux).sublime-keymap",
+ "/usr/lib/python2.7/shutil.py",
+ "/home/timmy/bin/file.py",
+ "/home/timmy/build/source/comictagger-develop/comicapi/comicarchive.py",
+ "/tmp/mozilla_timmy0/ImageCompare.py",
+ "/home/timmy/comics/complete/Venom 029.cbr",
+ "/home/timmy/build/source/comictagger-develop/comictaggerlib/filerenamer.py",
+ "/home/timmy/build/source/comictagger-develop/comictaggerlib/settingswindow.py",
+ "/home/timmy/build/source/comictagger-develop/comictaggerlib/ui/settingswindow.ui",
+ "/home/timmy/build/source/comictagger-develop/comictaggerlib/cli.py",
+ "/home/timmy/build/source/comictagger-develop/comictaggerlib/ui/TemplateHelp.ui",
+ "/home/timmy/Ansible/pippin.yaml",
+ "/home/timmy/Ansible/roles/void/tasks/main.yaml",
+ "/tmp/xa-HS698Z/hosts",
+ "/home/timmy/Downloads/GoLand-2019.2.2/bin/format.sh",
+ "/home/timmy/Downloads/GoLand-2019.2.2/Install-Linux-tar.txt",
+ "/home/timmy/Documents/shieldmaiden",
+ "/home/timmy/build/source/comictagger-develop/comictaggerlib/ui/taggerwindow.ui",
+ "/home/timmy/.config/sublime-text-3/Packages/User/AutoPep8.sublime-settings",
+ "/usr/lib/python3.6/string.py",
+ "/home/timmy/build/source/comictagger-develop/comicapi/filenameparser.py",
+ "/home/timmy/build/source/comictagger-develop/comicapi/comicinfoxml.py",
+ "/home/timmy/build/source/comictagger-develop/comicapi/genericmetadata.py",
+ "/home/timmy/build/source/comictagger-develop/comictaggerlib/imagefetcher.py",
+ "/home/timmy/build/source/comictagger-develop/comictaggerlib/versionchecker.py",
+ "/home/timmy/build/source/comictagger-develop/comictaggerlib/settings.py",
+ "/home/timmy/build/source/comictagger-develop/comicapi/comicbookinfo.py",
+ "/home/timmy/build/source/comictagger-develop/comictaggerlib/options.py",
+ "/home/timmy/.config/sublime-text-3/Packages/User/Default.sublime-keymap",
+ "/home/timmy/.config/sublime-text-3/Packages/SublimeBookmarks/Default.sublime-keymap",
+ "/home/timmy/build/source/comictagger-develop/comictaggerlib/autotagstartwindow.py",
+ "/home/timmy/build/source/comictagger-develop/comictaggerlib/ui/autotagstartwindow.ui",
+ "/usr/lib/python3.6/_collections_abc.py",
+ "/usr/lib/python3.6/collections/abc.py",
+ "/home/timmy/build/source/comictagger-develop/.git/config",
+ "/home/timmy/build/source/comictagger-develop/comicapi/issuestring.py",
+ "/home/timmy/http.xml",
+ "/home/timmy/Downloads/minidlna-1.2.1/minidlna.conf.5",
+ "/home/timmy/.config/sublime-text-3/Packages/SublimeManpage/SublimeManpage.sublime-settings",
+ "/home/timmy/Downloads/minidlna-1.2.1/LICENCE.miniupnpd",
+ "/home/timmy/Downloads/minidlna-1.2.1/ChangeLog",
+ "/home/timmy/Downloads/minidlna-1.2.1/NEWS",
+ "/home/timmy/Downloads/minidlna-1.2.1/ABOUT-NLS",
+ "/tmp/xa-MTA35Z/ContentDirectory1.SyntaxTests.xml",
+ "/tmp/xa-MTA35Z/ContentDirectory1.xml",
+ "/tmp/xa-MTA35Z/MediaServer3.xml",
+ "/tmp/xa-MTA35Z/MediaServer2.xml",
+ "/home/timmy/go/src/timmy.narnian.us/git/timmy/wsfmt/.gitignore",
+ "/home/timmy/build/source/comictagger-develop/comictaggerlib/volumeselectionwindow.py",
+ "/home/timmy/build/source/comictagger-develop/scripts/inventory.py",
+ "/home/timmy/build/source/comictagger-test/appveyor.yml",
+ "/home/timmy/build/source/comictagger-develop/comictaggerlib/ui/pagelisteditor.ui",
+ "/home/timmy/go/src/timmy.narnian.us/hid/keymaps/dvorak.json",
+ "/home/timmy/go/src/timmy.narnian.us/hid/keymaps/qwerty.json",
+ "/home/timmy/.config/massren/temp/12acfbeb-71fe-4f69-5469-ed012bb51e49.files.txt",
+ "/home/timmy/build/source/comictagger-test/scripts/find_dupes.py",
+ "/home/timmy/build/source/comictagger-test/comicapi/comicarchive.py",
+ "/home/timmy/build/source/comictagger-test/comictaggerlib/issueidentifier.py",
+ "/home/timmy/build/source/comictagger-test/comicapi/issuestring.py",
+ "/home/timmy/build/source/comictagger-test/comicapi/filenameparser.py",
+ "/home/timmy/build/source/comictagger-test/comictaggerlib/cli.py",
+ "/home/timmy/build/source/comictagger-test/comictaggerlib/filerenamer.py",
+ "/home/timmy/build/source/comictagger-test/comicapi/genericmetadata.py",
+ "/home/timmy/build/source/comictagger-test/comictaggerlib/options.py",
+ "/home/timmy/build/source/comictagger-test/comictaggerlib/comicvinetalker.py",
+ "/home/timmy/Ansible/roles/bombur/files/transmission.json",
+ "/home/timmy/Ansible/roles/bombur/tasks/main.yaml",
+ "/home/timmy/Ansible/roles/getmail/files/getmail",
+ "/home/timmy/.config/massren/temp/5e33246e-52ce-4b3f-53e4-958912a11275.files.txt",
+ "/home/timmy/Downloads/!FILES.STR",
+ "/tmp/mozilla_timmy0/Watch Dogs Complete Edition - CorePack.nfo",
+ "/home/timmy/Downloads/Stuxnet-Sourcecode/Stuxnet-Sourcecode/output/1E17D81979271CFA44D471430FE123A5/1E17D81979271CFA44D471430FE123A5.c",
+ "/home/timmy/comics/test/temp/out.html",
+ "/home/timmy/go/src/github.com/lordwelch/PresentationApp/image.go",
+ "/home/timmy/comics/test/out.html",
+ "/home/timmy/.config/massren/temp/45c104a8-f4d5-4793-4814-1c71801ea09a.files.txt",
+ "/home/timmy/.config/massren/temp/be36698d-56b5-48c4-4379-61b627bc4bb2.files.txt",
+ "/tmp/mozilla_timmy0/Old Man Quill 006 (2019) (Digital) (Zone-Empire)-3.log",
+ "/tmp/mozilla_timmy0/Old Man Quill 006 (2019) (Digital) (Zone-Empire).log",
+ "/tmp/mozilla_timmy0/EasySort.py",
+ "/tmp/mozilla_timmy0/Batman - Teenage Mutant Ninja Turtles III 02 (of 06) (2019) (Digital) (Zone-Empire).cbr.log",
+ "/tmp/mozilla_timmy0/Old Man Quill 006 (2019) (Digital) (Zone-Empire)-2.log",
+ "/tmp/mozilla_timmy0/Old Man Quill 006 (2019) (Digital) (Zone-Empire)-1.log",
+ "/home/timmy/comics/Stargate Universe 006 (2018) (Digital) (Kileko-Empire).cbz",
+ "/home/timmy/Comic/Stargate Universe 006 (2018) (Digital) (Kileko-Empire).cbr",
+ "/home/timmy/Calibre Library/Unknown/Stargate Universe 006 (2018) (Digital) (Kileko-Empire) (71)/Stargate Universe 006 (2018) (Digital) (Ki - Unknown.cbr",
+ "/home/timmy/Books/export/Fall of Gondolin, The - J. R. R. Tolkien.opf",
+ "/home/timmy/Books/export/Awakening a God - Troy Snyder.opf",
+ "/tmp/mozilla_timmy0/pfftn_3x.py",
+ "/tmp/mozilla_timmy0/pass-from-file-to-nzbget2.6.py"
+ ],
+ "find":
+ {
+ "height": 39.0
+ },
+ "find_in_files":
+ {
+ "height": 110.0,
+ "where_history":
+ [
+ "/home/timmy/build/source/comictagger-develop/",
+ "/home/timmy/build/source/comictagger-develop/comictaggerlib",
+ ""
+ ]
+ },
+ "find_state":
+ {
+ "case_sensitive": false,
+ "find_history":
+ [
+ "bash",
+ "compareDupe",
+ "settings",
+ "Settings",
+ "settings",
+ "new_name_item",
+ "old_name_item",
+ "folder_item",
+ "twList",
+ "currentItemChanged",
+ "selectionChanged",
+ "twList",
+ "pageListEditor",
+ "IDHashes",
+ "output_function",
+ "defaultWriteOutput",
+ "l",
+ "#s",
+ "sys",
+ "unarar",
+ "unrarlib",
+ "setHasherAlgorithm",
+ "i",
+ "dupe_i",
+ "xrange",
+ "dupe_i",
+ "$PWD",
+ "-pattern_type none ",
+ "'",
+ "$n",
+ "file:",
+ "://",
+ "'file",
+ "$f",
+ "file",
+ "rename",
+ "()))\n",
+ "[1]",
+ "series_name",
+ "record",
+ "last_result",
+ "search_series_name",
+ ">>>",
+ "^ \"name\": ",
+ " \"name\": ",
+ "extras",
+ "Extras",
+ "\"))\n",
+ "silent",
+ "x",
+ "dupe",
+ "//",
+ "min_score_distance",
+ "strong_score_thresh",
+ "setHasherAlgorithm",
+ "zipfile",
+ "rar",
+ "rarfile",
+ "constants",
+ "unrarlib",
+ "%s",
+ "filename",
+ "arg",
+ "File",
+ "$(basename \"",
+ "sudo",
+ " ",
+ "quot",
+ " ",
+ """,
+ "'",
+ "\t",
+ "date",
+ "'",
+ " ",
+ "string",
+ "} (",
+ " ",
+ "string",
+ "} (",
+ " ",
+ "settingsw",
+ "isVersionOf",
+ "characters",
+ "country",
+ "criticalRating",
+ "self",
+ "try",
+ "this",
+ "thisself",
+ "this",
+ "accept",
+ "formatter",
+ "determineName",
+ "| ",
+ "|",
+ "1978",
+ "Digital",
+ "(F)",
+ "\t",
+ "Star",
+ "\n\tStar",
+ "\t",
+ "cbz",
+ "select",
+ " isinstance(self.imprint, str) and",
+ "ImageHasher",
+ "()",
+ "plat",
+ "urllib",
+ "altUrlListFetchComplete",
+ "query_word_list",
+ "requests",
+ "version",
+ "request\\.",
+ "params",
+ "parsedYear",
+ "url",
+ "getCVContent",
+ "count_of_isssues",
+ "urllib",
+ "\")\n",
+ ")\n",
+ " or \"\"",
+ "str",
+ "\"\"",
+ "save",
+ "ResultMultipleMatchesWithBadImageScores"
+ ],
+ "highlight": true,
+ "in_selection": false,
+ "preserve_case": false,
+ "regex": true,
+ "replace_history":
+ [
+ "\\t",
+ "requests.",
+ "auto_imprint",
+ "utils.xlate",
+ "<",
+ ">",
+ "<",
+ ""
+ ],
+ "reverse": false,
+ "show_context": true,
+ "use_buffer2": true,
+ "whole_word": false,
+ "wrap": true
+ },
+ "groups":
+ [
+ {
+ "sheets":
+ [
+ ]
+ }
+ ],
+ "incremental_find":
+ {
+ "height": 29.0
+ },
+ "input":
+ {
+ "height": 42.0
+ },
+ "layout":
+ {
+ "cells":
+ [
+ [
+ 0,
+ 0,
+ 1,
+ 1
+ ]
+ ],
+ "cols":
+ [
+ 0.0,
+ 1.0
+ ],
+ "rows":
+ [
+ 0.0,
+ 1.0
+ ]
+ },
+ "menu_visible": true,
+ "output.9o:///home/timmy/GO/src/github.com/fogleman/gg":
+ {
+ "height": 108.0
+ },
+ "output.9o:///home/timmy/GO/src/github.com/limetext/qml-go/examples/itemmodel":
+ {
+ "height": 114.0
+ },
+ "output.9o:///home/timmy/GO/src/github.com/lordwelch/PresentationApp":
+ {
+ "height": 256.0
+ },
+ "output.9o:///home/timmy/GO/src/github.com/lordwelch/test":
+ {
+ "height": 114.0
+ },
+ "output.9o:///home/timmy/GO/src/github.com/therecipe/examples/advanced/qml_quick/treeview":
+ {
+ "height": 180.0
+ },
+ "output.9o:///home/timmy/GO/src/timmy.narnian.us/git/timmy/wsfmt":
+ {
+ "height": 115.0
+ },
+ "output.9o:///home/timmy/GO/src/timmy.narnian.us/git/timmy/wsfmt/lexCmp":
+ {
+ "height": 115.0
+ },
+ "output.9o:///home/timmy/GO/src/timmy.narnian.us/git/timmy/wsfmt/text/lex":
+ {
+ "height": 115.0
+ },
+ "output.9o://9o":
+ {
+ "height": 144.0
+ },
+ "output.GoGuru Output":
+ {
+ "height": 0.0
+ },
+ "output.GoRename Output":
+ {
+ "height": 232.0
+ },
+ "output.GoSublime-main-output":
+ {
+ "height": 114.0
+ },
+ "output.GoSublime/HUD":
+ {
+ "height": 0.0
+ },
+ "output.GsComplete.completion-hint-output":
+ {
+ "height": 114.0
+ },
+ "output.GsDoc-output-output":
+ {
+ "height": 354.0
+ },
+ "output.autopep8":
+ {
+ "height": 144.0
+ },
+ "output.find_results":
+ {
+ "height": 144.0
+ },
+ "pinned_build_system": "Packages/GoSublime/GoSublime.sublime-build",
+ "project": "comictagger.sublime-project",
+ "replace":
+ {
+ "height": 70.0
+ },
+ "save_all_on_build": true,
+ "select_file":
+ {
+ "height": 0.0,
+ "last_filter": "",
+ "selected_items":
+ [
+ ],
+ "width": 0.0
+ },
+ "select_project":
+ {
+ "height": 500.0,
+ "last_filter": "",
+ "selected_items":
+ [
+ [
+ "",
+ "~/Videos/bluray.sublime-project"
+ ]
+ ],
+ "width": 380.0
+ },
+ "select_symbol":
+ {
+ "height": 392.0,
+ "last_filter": "",
+ "selected_items":
+ [
+ ],
+ "width": 592.0
+ },
+ "selected_group": 0,
+ "settings":
+ {
+ },
+ "show_minimap": true,
+ "show_open_files": true,
+ "show_tabs": true,
+ "side_bar_visible": true,
+ "side_bar_width": 337.0,
+ "status_bar_visible": true,
+ "template_settings":
+ {
+ }
+}
diff --git a/comictaggerlib/coverimagewidget.py b/comictaggerlib/coverimagewidget.py
index 866c779..a2b90ce 100644
--- a/comictaggerlib/coverimagewidget.py
+++ b/comictaggerlib/coverimagewidget.py
@@ -313,7 +313,7 @@ class CoverImageWidget(QWidget):
# scale the pixmap to fit in the frame
scaled_pixmap = self.current_pixmap.scaled(
- new_w, new_h, Qt.KeepAspectRatio)
+ new_w, new_h, Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.lblImage.setPixmap(scaled_pixmap)
# move and resize the label to be centered in the fame
diff --git a/comictaggerlib/imagepopup.py b/comictaggerlib/imagepopup.py
index e216484..d16a2ce 100644
--- a/comictaggerlib/imagepopup.py
+++ b/comictaggerlib/imagepopup.py
@@ -54,7 +54,7 @@ class ImagePopup(QtWidgets.QDialog):
screen_size.height())
bg = QtGui.QPixmap(ComicTaggerSettings.getGraphic('popup_bg.png'))
self.clientBgPixmap = bg.scaled(
- screen_size.width(), screen_size.height())
+ screen_size.width(), screen_size.height(), QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation)
self.setMask(self.clientBgPixmap.mask())
self.applyImagePixmap()
@@ -77,7 +77,7 @@ class ImagePopup(QtWidgets.QDialog):
) > 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)
+ win_w, win_h, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
self.lblImage.setPixmap(display_pixmap)
else:
display_pixmap = self.imagePixmap
diff --git a/scripts/dupe.ui b/scripts/dupe.ui
new file mode 100644
index 0000000..68defcd
--- /dev/null
+++ b/scripts/dupe.ui
@@ -0,0 +1,158 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 729
+ 406
+
+
+
+ Form
+
+
+
+ QLayout::SetMinAndMaxSize
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ QAbstractItemView::ExtendedSelection
+
+
+ QAbstractItemView::SelectRows
+
+
+
+ -
+
+
+ true
+
+
+ Qt::Horizontal
+
+
+ false
+
+
+
+
+ 0
+ 0
+
+
+
+ QAbstractItemView::NoEditTriggers
+
+
+ false
+
+
+ QAbstractItemView::SingleSelection
+
+
+ QAbstractItemView::SelectRows
+
+
+ true
+
+
+ 3
+
+
+
+ name
+
+
+
+
+ score
+
+
+
+
+ dupe name
+
+
+
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
-
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
-
+
+
+
+
+ -
+
+
+ Delete Comic 1
+
+
+ Delete
+
+
+
+
+
+
+ -
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
-
+
+
+
+
+ -
+
+
+ Delete Comic 2
+
+
+ Delete
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scripts/find_dupes.py b/scripts/find_dupes.py
index 32a366d..da5a6bf 100755
--- a/scripts/find_dupes.py
+++ b/scripts/find_dupes.py
@@ -1,101 +1,783 @@
#!/usr/bin/python3
"""Find all duplicate comics"""
-import sys
-import json
+import argparse
+import ctypes
+import hashlib
+import platform
+import shutil
+import signal
+from operator import attrgetter
+from operator import itemgetter
+from typing import Dict, List
+
+import filetype
+import typing
+from PyQt5 import QtCore, QtGui, QtWidgets, uic
+
+from comictaggerlib.ui.qtutils import centerWindowOnParent
+
+try:
+ from unrar import unrarlib, rarfile
+
+ # monkey patch unrarlib to avoid segfaults on Win10
+ if platform.system() == 'Windows':
+ unrarlib.UNRARCALLBACK = ctypes.WINFUNCTYPE(
+ # return type
+ ctypes.c_int,
+ # msg
+ ctypes.c_uint,
+ # UserData
+ ctypes.c_long,
+ # MONKEY PATCH HERE -- use a pointer instead of a long, in unrar code: (LPARAM)(*byte),
+ # that is a pointer to byte casted to LPARAM
+ # On win10 64bit causes nasty segfaults when used from pyinstaller
+ ctypes.POINTER(ctypes.c_byte),
+ # size
+ ctypes.c_long
+ )
+ RARSetCallback = unrarlib._c_func(unrarlib.RARSetCallback, None,
+ [unrarlib.HANDLE, unrarlib.UNRARCALLBACK, ctypes.c_long])
+
+
+ def _rar_cb(self, msg, user_data, p1, p2):
+ if msg == constants.UCM_NEEDPASSWORD or msg == constants.UCM_NEEDPASSWORDW:
+ # This is a work around since libunrar doesn't
+ # properly return the error code when files are encrypted
+ self._missing_password = True
+ elif msg == constants.UCM_PROCESSDATA:
+ if self._data is None:
+ self._data = b''
+ chunk = ctypes.string_at(p1, p2)
+ self._data += chunk
+ return 1
+
+
+ rarfile._ReadIntoMemory._callback = _rar_cb
+ rarSupport = True
+except Exception as e:
+ rarSupport = False
+ print(e)
+ print("WARNING: cannot find libunrar, rar support is disabled")
+ pass
from comictaggerlib.comicarchive import *
from comictaggerlib.settings import *
-from comictaggerlib.issuestring import *
-import comictaggerlib.utils
-import subprocess
-import os
+from comictaggerlib.imagehasher import ImageHasher
+from comictaggerlib.filerenamer import FileRenamer
+
+root = 1 << 31 - 1
+something = 1 << 31 - 1
+
+
+class ImageMeta:
+ def __init__(self, name, file_hash, image_hash, image_type, score=-1, score_file_hash=""):
+ self.name = name
+ self.file_hash = file_hash
+ self.image_hash = image_hash
+ self.type = image_type
+ self.score = score
+ self.score_file_hash = score_file_hash
+
+
+class Duplicate:
+ """docstring for Duplicate"""
+ imageHashes: Dict[str, ImageMeta]
+
+ def __init__(self, path, metadata: GenericMetadata, cover):
+ self.path = path
+ self.digest = ""
+ self.metadata = metadata
+ self.imageHashes = dict()
+ self.duplicateImages = set()
+ self.extras = set()
+ self.extractedPath = ""
+ self.deletable = False
+ self.keeping = False
+ self.fileCount = 0 # Excluding comicinfo.xml
+ self.imageCount = 0
+ self.cover = cover
+ blake2b = hashlib.blake2b(digest_size=16)
+ for f in open(self.path, "rb"):
+ blake2b.update(f)
+
+ self.digest = blake2b.hexdigest()
+
+ def extract(self, directory):
+ archive_type = filetype.archive(self.path)
+ if archive_type is not None:
+ if archive_type.extension == 'zip':
+ archive = zipfile.ZipFile(self.path)
+ elif archive_type.extension == 'rar' and rarSupport:
+ archive = rarfile.RarFile(self.path)
+ archive.close = lambda: None
+ else:
+ return
+
+ if archive is not None:
+ self.extractedPath = directory
+ for fileinfo in archive.infolist():
+ if not isinstance(fileinfo, rarfile.RarInfo) and fileinfo.is_dir():
+ continue
+ filename = os.path.basename(fileinfo.filename)
+ archived_file = archive.open(fileinfo)
+ if filename.lower() in ["comicinfo.xml"]:
+ continue
+ self.fileCount += 1
+ file_bytes = archive.read(fileinfo)
+
+ image_type = filetype.image(archived_file)
+ if image_type is not None:
+ self.imageCount += 1
+ file_hash = hashlib.blake2b(file_bytes, digest_size=16).hexdigest().upper()
+ if file_hash in self.imageHashes.keys():
+ self.duplicateImages.add(filename)
+ else:
+ image_hash = ImageHasher(data=file_bytes, width=12, height=12).average_hash()
+ self.imageHashes[file_hash] = ImageMeta(os.path.join(self.extractedPath, filename), file_hash, image_hash,
+ image_type.extension)
+ else:
+ self.extras.add(filename)
+
+ os.makedirs(self.extractedPath, 0o777, True)
+ unarchived_file = open(os.path.join(self.extractedPath, filename), mode='wb')
+ archived_file.seek(0, io.SEEK_SET)
+ shutil.copyfileobj(archived_file, unarchived_file)
+ archived_file.close()
+ unarchived_file.close()
+ archive.close()
+
+ def clean(self):
+ shutil.rmtree(self.extractedPath, ignore_errors=True)
+
+ def delete(self):
+ if not self.keeping:
+ self.clean()
+ try:
+ os.remove(self.path)
+ except Exception:
+ pass
+ return not (os.path.exists(self.path) or os.path.exists(self.extractedPath))
+
+
+class Tree(QtCore.QAbstractListModel):
+ def __init__(self, item: List[List[Duplicate]]):
+ super(Tree, self).__init__()
+ self.rootItem = item
+
+ def rowCount(self, index: QtCore.QModelIndex = ...) -> int:
+ if not index.isValid():
+ return len(self.rootItem)
+
+ return 0
+
+ def columnCount(self, index: QtCore.QModelIndex = ...) -> int:
+ if index.isValid():
+ return 1
+
+ return 0
+
+ def data(self, index: QtCore.QModelIndex, role: int = ...) -> typing.Any:
+ if not index.isValid():
+ return QtCore.QVariant()
+
+ f = FileRenamer(self.rootItem[index.row()][0].metadata)
+ f.setTemplate("{series} #{issue} - {title} ({year})")
+ if role == QtCore.Qt.DisplayRole:
+ return f.determineName('')
+ elif role == QtCore.Qt.UserRole:
+ return f.determineName('')
+ return QtCore.QVariant()
+
+
+class MainWindow(QtWidgets.QMainWindow):
+ def __init__(self, file_list, settings, style, work_path, parent=None):
+ super().__init__(parent)
+ uic.loadUi('/home/timmy/build/source/comictagger-develop/scripts/mainwindow.ui', self)
+ self.dupes = []
+ self.firstRun = 0
+ self.dupe_set_list: List[List[Duplicate]] = list()
+ self.settings = settings
+ self.style = style
+ if work_path == "":
+ work_path = tempfile.mkdtemp()
+ self.work_path = work_path
+ self.initFiles = file_list
+ self.dupe_set_qlist.clicked.connect(self.dupe_set_clicked)
+ self.dupe_set_qlist.doubleClicked.connect(self.dupe_set_double_clicked)
+ self.actionCompare_Comic.triggered.connect(self.compare_action)
+
+ def comic_deleted(self, archive_path):
+ self.update_dupes()
+
+ def update_dupes(self):
+ print("updating duplicates")
+ new_set_list = list()
+ for dupe in self.dupe_set_list:
+ dupe_list = list()
+ for d in dupe:
+ QtCore.QCoreApplication.processEvents()
+ if os.path.exists(d.path):
+ dupe_list.append(d)
+ else:
+ d.clean()
+
+ if len(dupe_list) > 1:
+ new_set_list.append(dupe_list)
+ else:
+ dupe_list[0].clean()
+ self.dupe_set_list: List[List[Duplicate]] = new_set_list
+ self.dupe_set_qlist.setModel(Tree(self.dupe_set_list))
+
+ self.dupe_set_qlist.setSelection(QtCore.QRect(0, 0, 0, 1), QtCore.QItemSelectionModel.ClearAndSelect)
+ self.dupe_set_clicked(self.dupe_set_qlist.model().index(0, 0))
+
+ def compare(self, i):
+ if len(self.dupe_set_list) > i:
+ dw = DupeWindow(self.dupe_set_list[i], self.work_path, self)
+ dw.closed.connect(self.update_dupes)
+ dw.show()
+
+ def compare_action(self, b):
+ selection = self.dupe_set_qlist.selectedIndexes()
+ if len(selection) > 0:
+ self.compare(selection[0].row())
+
+ def dupe_set_double_clicked(self, index: QtCore.QModelIndex):
+ self.compare(index.row())
+
+ def dupe_set_clicked(self, index: QtCore.QModelIndex):
+ for f in self.dupe_list.children():
+ f.deleteLater()
+ self.dupe_set_list[index.row()].sort(key=lambda k: k.digest)
+ for i, f in enumerate(self.dupe_set_list[index.row()]):
+ color = "black"
+ if i > 0:
+ if self.dupe_set_list[index.row()][i - 1].digest == f.digest:
+ color = "green"
+ elif i == 0:
+ if len(self.dupe_set_list[index.row()]) > 1:
+ if self.dupe_set_list[index.row()][i + 1].digest == f.digest:
+ color = "green"
+ ql = DupeImage(duplicate=f, style=f".path {{color: black;}}.hash {{color: {color};}}", parent=self.dupe_list)
+ ql.deleted.connect(self.update_dupes)
+ ql.setMinimumWidth(300)
+ ql.setMinimumHeight(500)
+ self.dupe_list.layout().addWidget(ql)
+
+ def showEvent(self, event: QtGui.QShowEvent):
+ if self.firstRun == 0:
+ self.firstRun = 1
+
+ self.load_files(self.initFiles)
+ self.dupe_set_qlist.setSelection(QtCore.QRect(0, 0, 0, 1), QtCore.QItemSelectionModel.ClearAndSelect)
+ self.dupe_set_clicked(self.dupe_set_qlist.model().index(0, 0))
+
+ def load_files(self, file_list):
+ # Progress dialog on Linux flakes out for small range, so scale up
+ dialog = QtWidgets.QProgressDialog("", "Cancel", 0, len(file_list), parent=self)
+ dialog.setWindowTitle("Reading Comics")
+ dialog.setWindowModality(QtCore.Qt.ApplicationModal)
+ dialog.setMinimumDuration(300)
+ dialog.setMinimumWidth(400)
+ centerWindowOnParent(dialog)
+
+ comic_list = []
+ max_name_len = 2
+ for filename in file_list:
+ QtCore.QCoreApplication.processEvents()
+ if dialog.wasCanceled():
+ break
+ dialog.setValue(dialog.value() + 1)
+ dialog.setLabelText(filename)
+ ca = ComicArchive(filename, self.settings.rar_exe_path,
+ default_image_path='/home/timmy/build/source/comictagger-test/comictaggerlib/graphics/nocover.png')
+ if ca.seemsToBeAComicArchive() and ca.hasMetadata(self.style):
+ fmt_str = "{{0:{0}}}".format(max_name_len)
+ print(fmt_str.format(filename) + "\r", end='', file=sys.stderr)
+ sys.stderr.flush()
+ md = ca.readMetadata(self.style)
+ cover = ca.getPage(0)
+ comic_list.append((make_key(md), filename, md, cover))
+ max_name_len = len(filename)
+
+ comic_list.sort(key=itemgetter(0), reverse=False)
+
+ # look for duplicate blocks
+ dupe_set = list()
+ prev_key = ""
+
+ dialog.setWindowTitle("Finding Duplicates")
+ dialog.setMaximum(len(comic_list))
+ dialog.setValue(dialog.minimum())
+
+ set_list = list()
+ for new_key, filename, md, cover in comic_list:
+ dialog.setValue(dialog.value() + 1)
+ QtCore.QCoreApplication.processEvents()
+ if dialog.wasCanceled():
+ break
+ dialog.setLabelText(filename)
+
+ # if the new key same as the last, add to to dupe set
+ if new_key == prev_key:
+ dupe_set.append((filename, md, cover))
+ # else we're on a new potential block
+ else:
+ # only add if the dupe list has 2 or more
+ if len(dupe_set) > 1:
+ set_list.append(dupe_set)
+ dupe_set = list()
+ dupe_set.append((filename, md, cover))
+
+ prev_key = new_key
+
+ # Final dupe_set
+ if len(dupe_set) > 1:
+ set_list.append(dupe_set)
+
+ for d_set in set_list:
+ new_set = list()
+ for filename, md, cover in d_set:
+ new_set.append(Duplicate(filename, md, cover))
+ self.dupe_set_list.append(new_set)
+
+ self.dupe_set_qlist.setModel(Tree(self.dupe_set_list))
+ print("destroy")
+ dialog.close()
+
+ # def delete_hashes(self):
+ # working_dir = os.path.join(self.tmp, "working")
+ # s = False
+ # # while working and len(dupe_set) > 1:
+ # remaining = list()
+ # for dupe_set in self.dupe_set_list:
+ # not_deleted = True
+ # if os.path.exists(working_dir):
+ # shutil.rmtree(working_dir, ignore_errors=True)
+ #
+ # os.mkdir(working_dir)
+ # extract(dupe_set, working_dir)
+ # if mark_hashes(dupe_set):
+ # if s: # Auto delete if s flag or if there are not any non image extras
+ # dupe_set.sort(key=attrgetter("fileCount"))
+ # dupe_set.sort(key=lambda x: len(x.duplicateImages))
+ # dupe_set[0].keeping = True
+ # else:
+ # dupe_set[select_archive("Select archive to keep: ", dupe_set)].keeping = True
+ # else:
+ # # app.exec_()
+ # compare_dupe(dupe_set[0], dupe_set[1])
+ # for i, dupe in enumerate(dupe_set):
+ # print("{0}. {1}: {2.series} #{2.issue:0>3} {2.year}; extras: {3}; deletable: {4}".format(
+ # i,
+ # dupe.path,
+ # dupe.metadata,
+ # ", ".join(sorted(dupe.extras)), dupe.deletable))
+ # dupe_set = delete(dupe_set)
+ # if not_deleted:
+ # remaining.append(dupe_set)
+ # self.dupe_set_list = remaining
+
+
+class DupeWindow(QtWidgets.QWidget):
+ closed = QtCore.pyqtSignal()
+
+ def __init__(self, duplicates: List[Duplicate], tmp, parent=None):
+ super().__init__(parent, QtCore.Qt.Window)
+ uic.loadUi('/home/timmy/build/source/comictagger-develop/scripts/dupe.ui', self)
+
+ for f in self.comic1Image.children():
+ f.deleteLater()
+ for f in self.comic2Image.children():
+ f.deleteLater()
+ self.deleting = -1
+ self.duplicates = duplicates
+ self.dupe1 = -1
+ self.dupe2 = -1
+
+ self.tmp = tmp
+
+ self.setWindowTitle("ComicTagger Duplicate compare")
+
+ self.pageList.currentItemChanged.connect(self.current_item_changed)
+ self.comic1Delete.clicked.connect(self.delete_1)
+ self.comic2Delete.clicked.connect(self.delete_2)
+ self.dupeList.itemSelectionChanged.connect(self.show_dupe_list)
+ # self.dupeList = QtWidgets.QListWidget()
+ self.dupeList.setIconSize(QtCore.QSize(100, 50))
+
+ while self.pageList.rowCount() > 0:
+ self.pageList.removeRow(0)
+
+ self.pageList.setSortingEnabled(False)
+
+ if len(duplicates) < 2:
+ return
+ extract(duplicates, tmp)
+
+ tmp1 = DupeImage(self.duplicates[0])
+ tmp2 = DupeImage(self.duplicates[1])
+ self.comic1Data.layout().replaceWidget(self.comic1Image, tmp1)
+ self.comic2Data.layout().replaceWidget(self.comic2Image, tmp2)
+ self.comic1Image = tmp1
+ self.comic2Image = tmp2
+ self.comic1Image.deleted.connect(self.update_dupes)
+ self.comic2Image.deleted.connect(self.update_dupes)
+
+ def showEvent(self, event: QtGui.QShowEvent) -> None:
+ self.update_dupes()
+
+ def closeEvent(self, event: QtGui.QCloseEvent) -> None:
+ self.closed.emit()
+ event.accept()
+
+ def show_dupe_list(self):
+ dupes = self.dupeList.selectedItems()
+ if len(dupes) != 2:
+ return
+ self.dupe1 = int(dupes[0].data(QtCore.Qt.UserRole))
+ self.dupe2 = int(dupes[1].data(QtCore.Qt.UserRole))
+ if len(self.duplicates[self.dupe2].imageHashes) > len(self.duplicates[self.dupe1].imageHashes):
+ self.dupe1, self.dupe2 = self.dupe2, self.dupe1
+ compare_dupe(self.duplicates[self.dupe1].imageHashes, self.duplicates[self.dupe2].imageHashes)
+ self.display_dupe()
+
+ def update_dupes(self):
+ dupes: List[Duplicate] = list()
+ for f in self.duplicates:
+ if os.path.exists(f.path):
+ dupes.append(f)
+ else:
+ f.clean()
+ self.duplicates = dupes
+ if len(self.duplicates) < 2:
+ self.close()
+
+ for i, dupe in enumerate(self.duplicates):
+ item = QtWidgets.QListWidgetItem()
+ item.setText(dupe.path)
+ item.setToolTip(dupe.path)
+ pm = QtGui.QPixmap()
+ pm.loadFromData(dupe.cover)
+ item.setIcon(QtGui.QIcon(pm))
+ item.setData(QtCore.Qt.UserRole, i)
+ self.dupeList.addItem(item)
+ self.dupeList.setCurrentRow(0)
+ self.dupeList.setCurrentRow(1, QtCore.QItemSelectionModel.Select)
+
+ def delete_1(self):
+ self.duplicates[self.dupe1].delete()
+ self.update_dupes()
+
+ def delete_2(self):
+ self.duplicates[self.dupe2].delete()
+ self.update_dupes()
+
+ def display_dupe(self):
+ for f in range(self.pageList.rowCount()):
+ self.pageList.removeRow(0)
+ for h in self.duplicates[self.dupe1].imageHashes.values():
+ row = self.pageList.rowCount()
+ self.pageList.insertRow(row)
+ name = QtWidgets.QTableWidgetItem()
+ score = QtWidgets.QTableWidgetItem()
+ dupe_name = QtWidgets.QTableWidgetItem()
+
+ item_text = os.path.basename(h.name)
+ name.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+ name.setText(item_text)
+ name.setData(QtCore.Qt.UserRole, h.file_hash)
+ name.setData(QtCore.Qt.ToolTipRole, item_text)
+ self.pageList.setItem(row, 0, name)
+
+ item_text = str(h.score)
+ score.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+ score.setText(item_text)
+ score.setData(QtCore.Qt.UserRole, h.file_hash)
+ score.setData(QtCore.Qt.ToolTipRole, item_text)
+ self.pageList.setItem(row, 1, score)
+
+ item_text = os.path.basename(self.duplicates[self.dupe2].imageHashes[h.score_file_hash].name)
+ dupe_name.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+ dupe_name.setText(item_text)
+ dupe_name.setData(QtCore.Qt.UserRole, h.file_hash)
+ dupe_name.setData(QtCore.Qt.ToolTipRole, item_text)
+ self.pageList.setItem(row, 2, dupe_name)
+
+ self.pageList.resizeColumnsToContents()
+ self.pageList.selectRow(0)
+
+ def current_item_changed(self, curr, prev):
+
+ if curr is None:
+ return
+ if prev is not None and prev.row() == curr.row():
+ return
+
+ file_hash = str(self.pageList.item(curr.row(), 0).data(QtCore.Qt.UserRole))
+ image_hash = self.duplicates[self.dupe1].imageHashes[file_hash]
+ score_hash = self.duplicates[self.dupe2].imageHashes[image_hash.score_file_hash]
+
+ image1 = QtGui.QPixmap(image_hash.name)
+ image2 = QtGui.QPixmap(score_hash.name)
+
+ page_color = "red"
+ size_color = "red"
+ type_color = "red"
+ file_color = "black"
+ image_color = "black"
+ if image1.width() == image2.width() and image2.height() == image1.height():
+ size_color = "green"
+ if len(self.duplicates[self.dupe1].imageHashes) == len(self.duplicates[self.dupe2].imageHashes):
+ page_color = "green"
+ if image_hash.type == score_hash.type:
+ type_color = "green"
+ if image_hash.image_hash == score_hash.image_hash:
+ image_color = "green"
+ if image_hash.file_hash == score_hash.file_hash:
+ file_color = "green"
+ style = f"""
+.page {{
+color: {page_color};
+}}
+.size {{
+color: {size_color};
+}}
+.type {{
+color: {type_color};
+}}
+.file {{
+color: {file_color};
+}}
+.image {{
+color: {image_color};
+}}
+"""
+ text = "name: {{duplicate.path}}
" \
+ "page count: {len}
" \
+ "size/type: {{width}}x{{height}}/{meta.type}
" \
+ "file_hash: {meta.file_hash}
" \
+ "image_hash: {meta.image_hash}" \
+ .format(meta=image_hash, style=style, len=len(self.duplicates[self.dupe1].imageHashes))
+ self.comic1Image.setDuplicate(self.duplicates[self.dupe1])
+ self.comic1Image.setImage(image_hash.name)
+ self.comic1Image.setText(text)
+ self.comic1Image.setLabelStyle(style)
+
+ text = "name: {{duplicate.path}}
" \
+ "page count: {len}
" \
+ "size/type: {{width}}x{{height}}/{score.type}
" \
+ "file_hash: {score.file_hash}
" \
+ "image_hash: {score.image_hash}" \
+ .format(score=score_hash, style=style, len=len(self.duplicates[self.dupe2].imageHashes))
+ self.comic2Image.setDuplicate(self.duplicates[self.dupe2])
+ self.comic2Image.setImage(score_hash.name)
+ self.comic2Image.setText(text)
+ self.comic2Image.setLabelStyle(style)
+
+
+class QQlabel(QtWidgets.QLabel):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.image = None
+ self.setMinimumSize(1, 1)
+ self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
+
+ def setPixmap(self, pixmap: QtGui.QPixmap) -> None:
+ self.image = pixmap
+ self.setMaximumWidth(pixmap.width())
+ self.setMaximumHeight(pixmap.height())
+ super().setPixmap(self.image.scaled(self.width(), self.height(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation))
+
+ def resizeEvent(self, a0: QtGui.QResizeEvent) -> None:
+ if self.image is not None:
+ super().setPixmap(self.image.scaled(self.width(), self.height(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation))
+
+
+class DupeImage(QtWidgets.QWidget):
+ deleted = QtCore.pyqtSignal(str)
+
+ def __init__(self, duplicate: Duplicate, style=".path {color: black;}.hash {color: black;}", text="path: {duplicate.path}
hash: {duplicate.digest}", image="cover", parent=None):
+ super().__init__(parent)
+ self.setLayout(QtWidgets.QVBoxLayout())
+ self.image = QQlabel()
+ self.label = QtWidgets.QLabel()
+ self.duplicate = duplicate
+ self.text = text
+ self.labelStyle = style
+
+ self.iHeight = 0
+ self.iWidth = 0
+ self.setStyleSheet("color: black;")
+ self.label.setWordWrap(True)
+
+ self.setImage(image)
+ self.setLabelStyle(self.labelStyle)
+ self.setText(self.text)
+
+ # label.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
+ self.layout().addWidget(self.image)
+ self.layout().addWidget(self.label)
+
+ def contextMenuEvent(self, event: QtGui.QContextMenuEvent):
+ menu = QtWidgets.QMenu()
+ delete_action = menu.addAction("delete")
+ action = menu.exec(self.mapToGlobal(event.pos()))
+ if action == delete_action:
+ if self.duplicate.delete():
+ self.hide()
+ self.deleteLater()
+ print("signal emitted")
+ self.deleted.emit(self.duplicate.path)
+
+ def setDuplicate(self, duplicate: Duplicate):
+ self.duplicate = duplicate
+ self.setImage("cover")
+ self.label.setText(f"" + self.text.format(duplicate=self.duplicate, width=self.iWidth, height=self.iHeight))
+
+ def setText(self, text):
+ self.text = text
+ self.label.setText(f"" + self.text.format(duplicate=self.duplicate, width=self.iWidth, height=self.iHeight))
+
+ def setImage(self, image):
+ if self.duplicate is not None:
+ pm = QtGui.QPixmap()
+ if image == "cover":
+ pm.loadFromData(self.duplicate.cover)
+ else:
+ pm.load(image)
+ self.iHeight = pm.height()
+ self.iWidth = pm.width()
+ self.image.setPixmap(pm)
+
+ def setLabelStyle(self, style):
+ self.labelStyle = style
+ self.label.setText(f"" + self.text.format(duplicate=self.duplicate, width=self.iWidth, height=self.iHeight))
+
+
+def delete(dupe_set: List[Duplicate]) -> List[Duplicate]:
+ new_dupe_set = list()
+ for dupe in dupe_set:
+ if dupe.deletable and not dupe.keeping:
+ dupe.delete()
+ else:
+ new_dupe_set.append(dupe)
+ return new_dupe_set
+
+
+def select_archive(prompt, dupe_set: List[Duplicate]):
+ selection = -1
+ while selection < 0 or selection >= len(dupe_set):
+ print(len(dupe_set))
+ for i in range(len(dupe_set)):
+ print("{0}. {1}: {2.series} #{2.issue:0>3} {2.year}; extras: {3}".format(
+ i,
+ dupe_set[i].path,
+ dupe_set[i].metadata,
+ ", ".join(sorted(dupe_set[i].extras))))
+ sel = input(prompt)
+ if sel.isdigit():
+ selection = int(sel)
+ else:
+ selection = -1
+
+ return selection
+
+
+def extract(dupe_set, directory):
+ for dupe in dupe_set:
+ dupe.extract(unique_dir(os.path.join(directory, os.path.basename(dupe.path))))
+
+
+def compare_dupe(dupe1: Dict[str, ImageMeta], dupe2: Dict[str, ImageMeta]):
+ # if len(dupe1) > len(dupe2):
+ # hashes1 = dupe1
+ # hashes2 = dupe2
+ # else:
+ # hashes1 = dupe2
+ # hashes2 = dupe1
+
+ for k, image1 in dupe1.items():
+ score = sys.maxsize
+ file_hash = ""
+ for k2, image2 in dupe2.items():
+ tmp = ImageHasher.hamming_distance(image1.image_hash, image2.image_hash)
+ if tmp < score:
+ score = tmp
+ file_hash = image2.file_hash
+
+ dupe1[k].score = score
+ dupe1[k].score_file_hash = file_hash
+
+
+def mark_hashes(dupe_set: List[Duplicate]):
+ """Marks all comics that have identical hashes as deletable and returns true if all duplicate comics are identical"""
+ all_deletable = True
+ dupe_set[0].keeping = False
+ for i in range(1, len(dupe_set)):
+ dupe_set[i].keeping = False
+
+ # Comics are definitely the exact same
+ if dupe_set[i - 1].imageHashes.keys() == dupe_set[i].imageHashes.keys():
+ dupe_set[i - 1].deletable = True
+ dupe_set[i].deletable = True
+
+ if not dupe_set[i].deletable:
+ all_deletable = False
+
+ return all_deletable
+
+
+def make_key(x):
+ return "<" + str(x.series) + " #" + str(x.issue) + " - " + str(x.title) + " - " + str(x.year) + ">"
+
+
+def unique_dir(file_name):
+ counter = 1
+ file_name_parts = os.path.splitext(file_name)
+ while True:
+ if not os.path.lexists(file_name):
+ return file_name
+ file_name = file_name_parts[0] + ' (' + str(counter) + ')' + file_name_parts[1]
+ counter += 1
+
+
+app = None
def main():
-# utils.fix_output_encoding()
+ signal.signal(signal.SIGINT, sigint_handler)
+
+ parser = argparse.ArgumentParser(description='ComicTagger Duplicate comparison script')
+ parser.add_argument('-w', metavar='workdir', type=str, nargs=1, default=tempfile.mkdtemp(), help='work directory')
+ parser.add_argument('paths', metavar='PATH', type=str, nargs='+', help='Path(s) to search for duplicates')
+ args = parser.parse_args()
+
settings = ComicTaggerSettings()
-
style = MetaDataStyle.CIX
+ global workdir
+ global app
+ workdir = args.w
+ app = QtWidgets.QApplication(sys.argv)
+ file_list = utils.get_recursive_filelist(args.paths)
- if len(sys.argv) < 2:
- print("Usage: {0} [comic_folder]".format(sys.argv[0]))
- return
+ timer = QtCore.QTimer()
+ timer.start(50) # You may change this if you wish.
+ timer.timeout.connect(lambda: None) # Let the interpreter run each 500 ms.
- dupecmp = os.path.join(os.getcwd(), "dupecmp")
- if os.path.exists(dupecmp):
- subprocess.run(["bash", "-c", "rm -rf *"], cwd=dupecmp)
- else:
- os.mkdir(dupecmp)
- filelist = utils.get_recursive_filelist(sys.argv[1:])
-
- # first find all comics with metadata
- print("Reading in all comics...", file=sys.stderr)
- comic_list = []
- fmt_str = ""
- max_name_len = 2
- for filename in filelist:
- ca = ComicArchive(filename, settings.rar_exe_path, default_image_path='/home/timmy/build/source/comictagger-test/comictaggerlib/graphics/nocover.png')
- if ca.seemsToBeAComicArchive() and ca.hasMetadata(style):
- fmt_str = "{{0:{0}}}".format(max_name_len)
- print(fmt_str.format(filename) + "\r", end='', file=sys.stderr)
- sys.stderr.flush()
- comic_list.append((filename, ca.readMetadata(style)))
- max_name_len = len(filename)
-
- print("", file=sys.stderr)
- print("--------------------------------------------------------------------------", file=sys.stderr)
- print("Found {0} comics with {1} tags".format(len(comic_list), MetaDataStyle.name[style]), file=sys.stderr)
- print("--------------------------------------------------------------------------", file=sys.stderr)
-
- # sort the list by series+issue+year, to put all the dupes together
- def makeKey(x):
- return "<" + str(x[1].series) + " #" + \
- str(x[1].issue) + " - " + str(x[1].title) + " - " + str(x[1].year) + ">"
- comic_list.sort(key=makeKey, reverse=False)
-
- # look for duplicate blocks
- dupe_set_list = list()
- dupe_set = list()
- prev_key = ""
- for filename, md in comic_list:
- # sys.stderr.flush()
-
- new_key = makeKey((filename, md))
-
- # if the new key same as the last, add to to dupe set
- if new_key == prev_key:
- dupe_set.append(filename)
-
- # else we're on a new potential block
- else:
- # only add if the dupe list has 2 or more
- if len(dupe_set) > 1:
- dupe_set_list.append(dupe_set)
- dupe_set = list()
- dupe_set.append(filename)
-
- prev_key = new_key
-
- if len(dupe_set) > 1:
- dupe_set_list.append(dupe_set)
+ window = MainWindow(file_list, settings, style, workdir)
+ window.show()
+ app.exec()
+ shutil.rmtree(workdir, True)
- # print(json.dumps(dupe_set_list, indent=4))
- # print(fmt_str.format("") + "\r", end=' ', file=sys.stderr)
- # print("Found {0} duplicate sets".format(len(dupe_set_list)))
+def sigint_handler(*args):
+ """Handler for the SIGINT signal."""
+ sys.stderr.write('\r')
+ QtWidgets.QApplication.quit()
- for dupe_set in dupe_set_list:
- subprocess.run(["cp"] + dupe_set + [dupecmp])
- subprocess.run(["dup-comic.sh"], cwd=dupecmp)
-
- # ca = ComicArchive(dupe_set[0], settings.rar_exe_path)
- # md = ca.readMetadata(style)
- # print("{0} #{1} ({2})".format(md.series, md.issue, md.year))
- # for filename in dupe_set:
- # print("------>{0}".format(filename))
-
-#if __name__ == '__main__':
-main()
+if __name__ == '__main__':
+ main()
diff --git a/scripts/mainwindow.ui b/scripts/mainwindow.ui
new file mode 100644
index 0000000..db95df1
--- /dev/null
+++ b/scripts/mainwindow.ui
@@ -0,0 +1,92 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 800
+ 600
+
+
+
+ MainWindow
+
+
+
+
+ QLayout::SetMinAndMaxSize
+
+ -
+
+
+ true
+
+
+ true
+
+
+ Qt::Horizontal
+
+
+ false
+
+
+
+
+
+ 400
+ 0
+
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 396
+ 520
+
+
+
+
+
+
+
+
+
+
+
+
+ toolBar
+
+
+ TopToolBarArea
+
+
+ false
+
+
+
+
+
+ Compare Comic
+
+
+
+
+
+
diff --git a/unrar/makefile b/unrar/makefile
index 4f46c2a..9abd09f 100644
--- a/unrar/makefile
+++ b/unrar/makefile
@@ -8,7 +8,7 @@ LIBFLAGS=-fPIC
DEFINES=-D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE -DRAR_SMP
STRIP=strip
AR=ar
-LDFLAGS=-pthread
+LDFLAGS=-pthread -Wl,-soname=libunrar.so
DESTDIR=/usr
# Linux using LCC