mirror of
https://github.com/arsenetar/dupeguru.git
synced 2026-01-22 14:41:39 +00:00
Added tox configuration
... and fixed pep8 warnings. There's a lot of them that are still ignored, but that's because it's too much of a step to take at once.
This commit is contained in:
39
core/app.py
39
core/app.py
@@ -38,8 +38,10 @@ DEBUG_MODE_PREFERENCE = 'DebugMode'
|
||||
|
||||
MSG_NO_MARKED_DUPES = tr("There are no marked duplicates. Nothing has been done.")
|
||||
MSG_NO_SELECTED_DUPES = tr("There are no selected duplicates. Nothing has been done.")
|
||||
MSG_MANY_FILES_TO_OPEN = tr("You're about to open many files at once. Depending on what those "
|
||||
"files are opened with, doing so can create quite a mess. Continue?")
|
||||
MSG_MANY_FILES_TO_OPEN = tr(
|
||||
"You're about to open many files at once. Depending on what those "
|
||||
"files are opened with, doing so can create quite a mess. Continue?"
|
||||
)
|
||||
|
||||
class DestType:
|
||||
Direct = 0
|
||||
@@ -265,8 +267,10 @@ class DupeGuru(Broadcaster):
|
||||
return None
|
||||
|
||||
def _get_export_data(self):
|
||||
columns = [col for col in self.result_table.columns.ordered_columns
|
||||
if col.visible and col.name != 'marked']
|
||||
columns = [
|
||||
col for col in self.result_table.columns.ordered_columns
|
||||
if col.visible and col.name != 'marked'
|
||||
]
|
||||
colnames = [col.display for col in columns]
|
||||
rows = []
|
||||
for group_id, group in enumerate(self.results.groups):
|
||||
@@ -278,8 +282,10 @@ class DupeGuru(Broadcaster):
|
||||
return colnames, rows
|
||||
|
||||
def _results_changed(self):
|
||||
self.selected_dupes = [d for d in self.selected_dupes
|
||||
if self.results.get_group_of_duplicate(d) is not None]
|
||||
self.selected_dupes = [
|
||||
d for d in self.selected_dupes
|
||||
if self.results.get_group_of_duplicate(d) is not None
|
||||
]
|
||||
self.notify('results_changed')
|
||||
|
||||
def _start_job(self, jobid, func, args=()):
|
||||
@@ -287,7 +293,10 @@ class DupeGuru(Broadcaster):
|
||||
try:
|
||||
self.progress_window.run(jobid, title, func, args=args)
|
||||
except job.JobInProgressError:
|
||||
msg = tr("A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again.")
|
||||
msg = tr(
|
||||
"A previous action is still hanging in there. You can't start a new one yet. Wait "
|
||||
"a few seconds, then try again."
|
||||
)
|
||||
self.view.show_message(msg)
|
||||
|
||||
def _job_completed(self, jobid):
|
||||
@@ -439,8 +448,10 @@ class DupeGuru(Broadcaster):
|
||||
return
|
||||
if not self.deletion_options.show(self.results.mark_count):
|
||||
return
|
||||
args = [self.deletion_options.link_deleted, self.deletion_options.use_hardlinks,
|
||||
self.deletion_options.direct]
|
||||
args = [
|
||||
self.deletion_options.link_deleted, self.deletion_options.use_hardlinks,
|
||||
self.deletion_options.direct
|
||||
]
|
||||
logging.debug("Starting deletion job with args %r", args)
|
||||
self._start_job(JobType.Delete, self._do_delete, args=args)
|
||||
|
||||
@@ -550,8 +561,10 @@ class DupeGuru(Broadcaster):
|
||||
# If no group was changed, however, we don't touch the selection.
|
||||
if not self.result_table.power_marker:
|
||||
if changed_groups:
|
||||
self.selected_dupes = [d for d in self.selected_dupes
|
||||
if self.results.get_group_of_duplicate(d).ref is d]
|
||||
self.selected_dupes = [
|
||||
d for d in self.selected_dupes
|
||||
if self.results.get_group_of_duplicate(d).ref is d
|
||||
]
|
||||
self.notify('results_changed')
|
||||
else:
|
||||
# If we're in "Dupes Only" mode (previously called Power Marker), things are a bit
|
||||
@@ -604,7 +617,7 @@ class DupeGuru(Broadcaster):
|
||||
def purge_ignore_list(self):
|
||||
"""Remove files that don't exist from :attr:`ignore_list`.
|
||||
"""
|
||||
self.scanner.ignore_list.Filter(lambda f,s:op.exists(f) and op.exists(s))
|
||||
self.scanner.ignore_list.Filter(lambda f, s: op.exists(f) and op.exists(s))
|
||||
self.ignore_list_dialog.refresh()
|
||||
|
||||
def remove_directories(self, indexes):
|
||||
@@ -641,7 +654,7 @@ class DupeGuru(Broadcaster):
|
||||
msg = tr("You are about to remove %d files from results. Continue?")
|
||||
if not self.view.ask_yes_no(msg % self.results.mark_count):
|
||||
return
|
||||
self.results.perform_on_marked(lambda x:None, True)
|
||||
self.results.perform_on_marked(lambda x: None, True)
|
||||
self._results_changed()
|
||||
|
||||
def remove_selected(self):
|
||||
|
||||
@@ -62,10 +62,10 @@ class Directories:
|
||||
return True
|
||||
return False
|
||||
|
||||
def __delitem__(self,key):
|
||||
def __delitem__(self, key):
|
||||
self._dirs.__delitem__(key)
|
||||
|
||||
def __getitem__(self,key):
|
||||
def __getitem__(self, key):
|
||||
return self._dirs.__getitem__(key)
|
||||
|
||||
def __len__(self):
|
||||
@@ -95,7 +95,8 @@ class Directories:
|
||||
file.is_ref = state == DirectoryState.Reference
|
||||
filepaths.add(file.path)
|
||||
yield file
|
||||
# it's possible that a folder (bundle) gets into the file list. in that case, we don't want to recurse into it
|
||||
# it's possible that a folder (bundle) gets into the file list. in that case, we don't
|
||||
# want to recurse into it
|
||||
subfolders = [p for p in from_path.listdir() if not p.islink() and p.isdir() and p not in filepaths]
|
||||
for subfolder in subfolders:
|
||||
for file in self._get_files(subfolder, j):
|
||||
@@ -144,7 +145,7 @@ class Directories:
|
||||
"""
|
||||
try:
|
||||
subpaths = [p for p in path.listdir() if p.isdir()]
|
||||
subpaths.sort(key=lambda x:x.name.lower())
|
||||
subpaths.sort(key=lambda x: x.name.lower())
|
||||
return subpaths
|
||||
except EnvironmentError:
|
||||
return []
|
||||
|
||||
@@ -17,9 +17,11 @@ from hscommon.util import flatten, multi_replace
|
||||
from hscommon.trans import tr
|
||||
from hscommon.jobprogress import job
|
||||
|
||||
(WEIGHT_WORDS,
|
||||
MATCH_SIMILAR_WORDS,
|
||||
NO_FIELD_ORDER) = range(3)
|
||||
(
|
||||
WEIGHT_WORDS,
|
||||
MATCH_SIMILAR_WORDS,
|
||||
NO_FIELD_ORDER,
|
||||
) = range(3)
|
||||
|
||||
JOB_REFRESH_RATE = 100
|
||||
|
||||
@@ -259,6 +261,7 @@ def getmatches_by_contents(files, sizeattr='size', partial=False, j=job.nulljob)
|
||||
filesize = getattr(file, sizeattr)
|
||||
if filesize:
|
||||
size2files[filesize].add(file)
|
||||
del files
|
||||
possible_matches = [files for files in size2files.values() if len(files) > 1]
|
||||
del size2files
|
||||
result = []
|
||||
@@ -495,7 +498,10 @@ def get_groups(matches, j=job.nulljob):
|
||||
matched_files = set(flatten(groups))
|
||||
orphan_matches = []
|
||||
for group in groups:
|
||||
orphan_matches += set(m for m in group.discard_matches() if not any(obj in matched_files for obj in [m.first, m.second]))
|
||||
orphan_matches += {
|
||||
m for m in group.discard_matches()
|
||||
if not any(obj in matched_files for obj in [m.first, m.second])
|
||||
}
|
||||
if groups and orphan_matches:
|
||||
groups += get_groups(orphan_matches) # no job, as it isn't supposed to take a long time
|
||||
return groups
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2006/09/16
|
||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
#
|
||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
import os.path as op
|
||||
@@ -19,56 +19,56 @@ MAIN_TEMPLATE = """
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
|
||||
<title>dupeGuru Results</title>
|
||||
<style type="text/css">
|
||||
<title>dupeGuru Results</title>
|
||||
<style type="text/css">
|
||||
BODY
|
||||
{
|
||||
background-color:white;
|
||||
background-color:white;
|
||||
}
|
||||
|
||||
BODY,A,P,UL,TABLE,TR,TD
|
||||
{
|
||||
font-family:Tahoma,Arial,sans-serif;
|
||||
font-size:10pt;
|
||||
color: #4477AA;
|
||||
font-family:Tahoma,Arial,sans-serif;
|
||||
font-size:10pt;
|
||||
color: #4477AA;
|
||||
}
|
||||
|
||||
TABLE
|
||||
{
|
||||
background-color: #225588;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 90%;
|
||||
background-color: #225588;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
TR
|
||||
TR
|
||||
{
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
TH
|
||||
{
|
||||
font-weight: bold;
|
||||
color: black;
|
||||
background-color: #C8D6E5;
|
||||
TH
|
||||
{
|
||||
font-weight: bold;
|
||||
color: black;
|
||||
background-color: #C8D6E5;
|
||||
}
|
||||
|
||||
TH TD
|
||||
TH TD
|
||||
{
|
||||
color:black;
|
||||
}
|
||||
|
||||
TD
|
||||
TD
|
||||
{
|
||||
padding-left: 2pt;
|
||||
}
|
||||
|
||||
TD.rightelem
|
||||
{
|
||||
text-align:right;
|
||||
/*padding-left:0pt;*/
|
||||
padding-right: 2pt;
|
||||
width: 17%;
|
||||
text-align:right;
|
||||
/*padding-left:0pt;*/
|
||||
padding-right: 2pt;
|
||||
width: 17%;
|
||||
}
|
||||
|
||||
TD.indented
|
||||
@@ -78,19 +78,19 @@ TD.indented
|
||||
|
||||
H1
|
||||
{
|
||||
font-family:"Courier New",monospace;
|
||||
color:#6699CC;
|
||||
font-size:18pt;
|
||||
color:#6da500;
|
||||
border-color: #70A0CF;
|
||||
border-width: 1pt;
|
||||
border-style: solid;
|
||||
margin-top: 16pt;
|
||||
margin-left: 5%;
|
||||
margin-right: 5%;
|
||||
padding-top: 2pt;
|
||||
padding-bottom:2pt;
|
||||
text-align: center;
|
||||
font-family:"Courier New",monospace;
|
||||
color:#6699CC;
|
||||
font-size:18pt;
|
||||
color:#6da500;
|
||||
border-color: #70A0CF;
|
||||
border-width: 1pt;
|
||||
border-style: solid;
|
||||
margin-top: 16pt;
|
||||
margin-left: 5%;
|
||||
margin-right: 5%;
|
||||
padding-top: 2pt;
|
||||
padding-bottom:2pt;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
63
core/fs.py
63
core/fs.py
@@ -1,9 +1,9 @@
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-10-22
|
||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
#
|
||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
# This is a fork from hsfs. The reason for this fork is that hsfs has been designed for musicGuru
|
||||
@@ -32,6 +32,7 @@ NOT_SET = object()
|
||||
|
||||
class FSError(Exception):
|
||||
cls_message = "An error has occured on '{name}' in '{parent}'"
|
||||
|
||||
def __init__(self, fsobject, parent=None):
|
||||
message = self.cls_message
|
||||
if isinstance(fsobject, str):
|
||||
@@ -42,7 +43,7 @@ class FSError(Exception):
|
||||
name = ''
|
||||
parentname = str(parent) if parent is not None else ''
|
||||
Exception.__init__(self, message.format(name=name, parent=parentname))
|
||||
|
||||
|
||||
|
||||
class AlreadyExistsError(FSError):
|
||||
"The directory or file name we're trying to add already exists"
|
||||
@@ -57,7 +58,7 @@ class InvalidDestinationError(FSError):
|
||||
cls_message = "'{name}' is an invalid destination for this operation."
|
||||
|
||||
class OperationError(FSError):
|
||||
"""A copy/move/delete operation has been called, but the checkup after the
|
||||
"""A copy/move/delete operation has been called, but the checkup after the
|
||||
operation shows that it didn't work."""
|
||||
cls_message = "Operation on '{name}' failed."
|
||||
|
||||
@@ -74,15 +75,15 @@ class File:
|
||||
# files, I saved 35% memory usage with "unread" files (no _read_info() call) and gains become
|
||||
# even greater when we take into account read attributes (70%!). Yeah, it's worth it.
|
||||
__slots__ = ('path', 'is_ref', 'words') + tuple(INITIAL_INFO.keys())
|
||||
|
||||
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
for attrname in self.INITIAL_INFO:
|
||||
setattr(self, attrname, NOT_SET)
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return "<{} {}>".format(self.__class__.__name__, str(self.path))
|
||||
|
||||
|
||||
def __getattribute__(self, attrname):
|
||||
result = object.__getattribute__(self, attrname)
|
||||
if result is NOT_SET:
|
||||
@@ -94,12 +95,12 @@ class File:
|
||||
if result is NOT_SET:
|
||||
result = self.INITIAL_INFO[attrname]
|
||||
return result
|
||||
|
||||
|
||||
#This offset is where we should start reading the file to get a partial md5
|
||||
#For audio file, it should be where audio data starts
|
||||
def _get_md5partial_offset_and_size(self):
|
||||
return (0x4000, 0x4000) #16Kb
|
||||
|
||||
|
||||
def _read_info(self, field):
|
||||
if field in ('size', 'mtime'):
|
||||
stats = self.path.stat()
|
||||
@@ -129,24 +130,24 @@ class File:
|
||||
fp.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _read_all_info(self, attrnames=None):
|
||||
"""Cache all possible info.
|
||||
|
||||
|
||||
If `attrnames` is not None, caches only attrnames.
|
||||
"""
|
||||
if attrnames is None:
|
||||
attrnames = self.INITIAL_INFO.keys()
|
||||
for attrname in attrnames:
|
||||
getattr(self, attrname)
|
||||
|
||||
|
||||
#--- Public
|
||||
@classmethod
|
||||
def can_handle(cls, path):
|
||||
"""Returns whether this file wrapper class can handle ``path``.
|
||||
"""
|
||||
return not path.islink() and path.isfile()
|
||||
|
||||
|
||||
def rename(self, newname):
|
||||
if newname == self.name:
|
||||
return
|
||||
@@ -160,42 +161,42 @@ class File:
|
||||
if not destpath.exists():
|
||||
raise OperationError(self)
|
||||
self.path = destpath
|
||||
|
||||
|
||||
def get_display_info(self, group, delta):
|
||||
"""Returns a display-ready dict of dupe's data.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
#--- Properties
|
||||
@property
|
||||
def extension(self):
|
||||
return get_file_ext(self.name)
|
||||
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.path.name
|
||||
|
||||
|
||||
@property
|
||||
def folder_path(self):
|
||||
return self.path.parent()
|
||||
|
||||
|
||||
|
||||
class Folder(File):
|
||||
"""A wrapper around a folder path.
|
||||
|
||||
|
||||
It has the size/md5 info of a File, but it's value are the sum of its subitems.
|
||||
"""
|
||||
__slots__ = File.__slots__ + ('_subfolders', )
|
||||
|
||||
|
||||
def __init__(self, path):
|
||||
File.__init__(self, path)
|
||||
self._subfolders = None
|
||||
|
||||
|
||||
def _all_items(self):
|
||||
folders = self.subfolders
|
||||
files = get_files(self.path)
|
||||
return folders + files
|
||||
|
||||
|
||||
def _read_info(self, field):
|
||||
if field in {'size', 'mtime'}:
|
||||
size = sum((f.size for f in self._all_items()), 0)
|
||||
@@ -208,31 +209,31 @@ class Folder(File):
|
||||
# different md5 if a file gets moved in a different subdirectory.
|
||||
def get_dir_md5_concat():
|
||||
items = self._all_items()
|
||||
items.sort(key=lambda f:f.path)
|
||||
items.sort(key=lambda f: f.path)
|
||||
md5s = [getattr(f, field) for f in items]
|
||||
return b''.join(md5s)
|
||||
|
||||
|
||||
md5 = hashlib.md5(get_dir_md5_concat())
|
||||
digest = md5.digest()
|
||||
setattr(self, field, digest)
|
||||
|
||||
|
||||
@property
|
||||
def subfolders(self):
|
||||
if self._subfolders is None:
|
||||
subfolders = [p for p in self.path.listdir() if not p.islink() and p.isdir()]
|
||||
self._subfolders = [self.__class__(p) for p in subfolders]
|
||||
return self._subfolders
|
||||
|
||||
|
||||
@classmethod
|
||||
def can_handle(cls, path):
|
||||
return not path.islink() and path.isdir()
|
||||
|
||||
|
||||
|
||||
def get_file(path, fileclasses=[File]):
|
||||
"""Wraps ``path`` around its appropriate :class:`File` class.
|
||||
|
||||
|
||||
Whether a class is "appropriate" is decided by :meth:`File.can_handle`
|
||||
|
||||
|
||||
:param Path path: path to wrap
|
||||
:param fileclasses: List of candidate :class:`File` classes
|
||||
"""
|
||||
@@ -242,7 +243,7 @@ def get_file(path, fileclasses=[File]):
|
||||
|
||||
def get_files(path, fileclasses=[File]):
|
||||
"""Returns a list of :class:`File` for each file contained in ``path``.
|
||||
|
||||
|
||||
:param Path path: path to scan
|
||||
:param fileclasses: List of candidate :class:`File` classes
|
||||
"""
|
||||
|
||||
@@ -12,4 +12,5 @@ either Cocoa's ``NSTableView`` or Qt's ``QTableView``. It tells them which cell
|
||||
blue, which is supposed to be orange, does the sorting logic, holds selection, etc..
|
||||
|
||||
.. _cross-toolkit: http://www.hardcoded.net/articles/cross-toolkit-software
|
||||
"""
|
||||
"""
|
||||
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2010-02-06
|
||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
#
|
||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
from hscommon.notify import Listener
|
||||
from hscommon.gui.base import NoopGUI
|
||||
|
||||
class DupeGuruGUIObject(Listener):
|
||||
def __init__(self, app):
|
||||
Listener.__init__(self, app)
|
||||
self.app = app
|
||||
|
||||
|
||||
def directories_changed(self):
|
||||
pass
|
||||
|
||||
|
||||
def dupes_selected(self):
|
||||
pass
|
||||
|
||||
|
||||
def marking_changed(self):
|
||||
pass
|
||||
|
||||
|
||||
def results_changed(self):
|
||||
pass
|
||||
|
||||
|
||||
def results_changed_but_keep_selection(self):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2011-09-06
|
||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
#
|
||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
from hscommon.gui.base import GUIObject
|
||||
@@ -13,7 +13,7 @@ class CriterionCategoryList(GUISelectableList):
|
||||
def __init__(self, dialog):
|
||||
self.dialog = dialog
|
||||
GUISelectableList.__init__(self, [c.NAME for c in dialog.categories])
|
||||
|
||||
|
||||
def _update_selection(self):
|
||||
self.dialog.select_category(self.dialog.categories[self.selected_index])
|
||||
GUISelectableList._update_selection(self)
|
||||
@@ -22,10 +22,10 @@ class PrioritizationList(GUISelectableList):
|
||||
def __init__(self, dialog):
|
||||
self.dialog = dialog
|
||||
GUISelectableList.__init__(self)
|
||||
|
||||
|
||||
def _refresh_contents(self):
|
||||
self[:] = [crit.display for crit in self.dialog.prioritizations]
|
||||
|
||||
|
||||
def move_indexes(self, indexes, dest_index):
|
||||
indexes.sort()
|
||||
prilist = self.dialog.prioritizations
|
||||
@@ -34,7 +34,7 @@ class PrioritizationList(GUISelectableList):
|
||||
del prilist[i]
|
||||
prilist[dest_index:dest_index] = selected
|
||||
self._refresh_contents()
|
||||
|
||||
|
||||
def remove_selected(self):
|
||||
prilist = self.dialog.prioritizations
|
||||
for i in sorted(self.selected_indexes, reverse=True):
|
||||
@@ -51,15 +51,15 @@ class PrioritizeDialog(GUIObject):
|
||||
self.criteria_list = GUISelectableList()
|
||||
self.prioritizations = []
|
||||
self.prioritization_list = PrioritizationList(self)
|
||||
|
||||
|
||||
#--- Override
|
||||
def _view_updated(self):
|
||||
self.category_list.select(0)
|
||||
|
||||
|
||||
#--- Private
|
||||
def _sort_key(self, dupe):
|
||||
return tuple(crit.sort_key(dupe) for crit in self.prioritizations)
|
||||
|
||||
|
||||
#--- Public
|
||||
def select_category(self, category):
|
||||
self.criteria = category.criteria_list()
|
||||
@@ -71,10 +71,11 @@ class PrioritizeDialog(GUIObject):
|
||||
return
|
||||
crit = self.criteria[self.criteria_list.selected_index]
|
||||
self.prioritizations.append(crit)
|
||||
del crit
|
||||
self.prioritization_list[:] = [crit.display for crit in self.prioritizations]
|
||||
|
||||
|
||||
def remove_selected(self):
|
||||
self.prioritization_list.remove_selected()
|
||||
|
||||
|
||||
def perform_reprioritization(self):
|
||||
self.app.reprioritize_groups(self._sort_key)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2006/05/02
|
||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
#
|
||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
from xml.etree import ElementTree as ET
|
||||
@@ -12,7 +12,7 @@ from hscommon.util import FileOrPath
|
||||
|
||||
class IgnoreList:
|
||||
"""An ignore list implementation that is iterable, filterable and exportable to XML.
|
||||
|
||||
|
||||
Call Ignore to add an ignore list entry, and AreIgnore to check if 2 items are in the list.
|
||||
When iterated, 2 sized tuples will be returned, the tuples containing 2 items ignored together.
|
||||
"""
|
||||
@@ -20,43 +20,43 @@ class IgnoreList:
|
||||
def __init__(self):
|
||||
self._ignored = {}
|
||||
self._count = 0
|
||||
|
||||
|
||||
def __iter__(self):
|
||||
for first,seconds in self._ignored.items():
|
||||
for first, seconds in self._ignored.items():
|
||||
for second in seconds:
|
||||
yield (first,second)
|
||||
|
||||
yield (first, second)
|
||||
|
||||
def __len__(self):
|
||||
return self._count
|
||||
|
||||
|
||||
#---Public
|
||||
def AreIgnored(self,first,second):
|
||||
def do_check(first,second):
|
||||
def AreIgnored(self, first, second):
|
||||
def do_check(first, second):
|
||||
try:
|
||||
matches = self._ignored[first]
|
||||
return second in matches
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
return do_check(first,second) or do_check(second,first)
|
||||
|
||||
|
||||
return do_check(first, second) or do_check(second, first)
|
||||
|
||||
def Clear(self):
|
||||
self._ignored = {}
|
||||
self._count = 0
|
||||
|
||||
def Filter(self,func):
|
||||
|
||||
def Filter(self, func):
|
||||
"""Applies a filter on all ignored items, and remove all matches where func(first,second)
|
||||
doesn't return True.
|
||||
"""
|
||||
filtered = IgnoreList()
|
||||
for first,second in self:
|
||||
if func(first,second):
|
||||
filtered.Ignore(first,second)
|
||||
for first, second in self:
|
||||
if func(first, second):
|
||||
filtered.Ignore(first, second)
|
||||
self._ignored = filtered._ignored
|
||||
self._count = filtered._count
|
||||
|
||||
def Ignore(self,first,second):
|
||||
if self.AreIgnored(first,second):
|
||||
|
||||
def Ignore(self, first, second):
|
||||
if self.AreIgnored(first, second):
|
||||
return
|
||||
try:
|
||||
matches = self._ignored[first]
|
||||
@@ -70,7 +70,7 @@ class IgnoreList:
|
||||
matches.add(second)
|
||||
self._ignored[first] = matches
|
||||
self._count += 1
|
||||
|
||||
|
||||
def remove(self, first, second):
|
||||
def inner(first, second):
|
||||
try:
|
||||
@@ -85,14 +85,14 @@ class IgnoreList:
|
||||
return False
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
|
||||
if not inner(first, second):
|
||||
if not inner(second, first):
|
||||
raise ValueError()
|
||||
|
||||
|
||||
def load_from_xml(self, infile):
|
||||
"""Loads the ignore list from a XML created with save_to_xml.
|
||||
|
||||
|
||||
infile can be a file object or a filename.
|
||||
"""
|
||||
try:
|
||||
@@ -109,10 +109,10 @@ class IgnoreList:
|
||||
subfile_path = sfn.get('path')
|
||||
if subfile_path:
|
||||
self.Ignore(file_path, subfile_path)
|
||||
|
||||
|
||||
def save_to_xml(self, outfile):
|
||||
"""Create a XML file that can be used by load_from_xml.
|
||||
|
||||
|
||||
outfile can be a file object or a filename.
|
||||
"""
|
||||
root = ET.Element('ignore_list')
|
||||
@@ -125,5 +125,5 @@ class IgnoreList:
|
||||
tree = ET.ElementTree(root)
|
||||
with FileOrPath(outfile, 'wb') as fp:
|
||||
tree.write(fp, encoding='utf-8')
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -96,7 +96,10 @@ class Results(Markable):
|
||||
self.__dupes = flatten(group.dupes for group in self.groups)
|
||||
if None in self.__dupes:
|
||||
# This is debug logging to try to figure out #44
|
||||
logging.warning("There is a None value in the Results' dupe list. dupes: %r groups: %r", self.__dupes, self.groups)
|
||||
logging.warning(
|
||||
"There is a None value in the Results' dupe list. dupes: %r groups: %r",
|
||||
self.__dupes, self.groups
|
||||
)
|
||||
if self.__filtered_dupes:
|
||||
self.__dupes = [dupe for dupe in self.__dupes if dupe in self.__filtered_dupes]
|
||||
sd = self.__dupes_sort_descriptor
|
||||
@@ -249,7 +252,8 @@ class Results(Markable):
|
||||
second_file = dupes[int(attrs['second'])]
|
||||
percentage = int(attrs['percentage'])
|
||||
group.add_match(engine.Match(first_file, second_file, percentage))
|
||||
except (IndexError, KeyError, ValueError): # Covers missing attr, non-int values and indexes out of bounds
|
||||
except (IndexError, KeyError, ValueError):
|
||||
# Covers missing attr, non-int values and indexes out of bounds
|
||||
pass
|
||||
if (not group.matches) and (len(dupes) >= 2):
|
||||
do_match(dupes[0], dupes[1:], group)
|
||||
@@ -393,7 +397,7 @@ class Results(Markable):
|
||||
self.__get_dupe_list()
|
||||
keyfunc = lambda d: self.app._get_dupe_sort_key(d, lambda: self.get_group_of_duplicate(d), key, delta)
|
||||
self.__dupes.sort(key=keyfunc, reverse=not asc)
|
||||
self.__dupes_sort_descriptor = (key,asc,delta)
|
||||
self.__dupes_sort_descriptor = (key, asc, delta)
|
||||
|
||||
def sort_groups(self, key, asc=True):
|
||||
"""Sort :attr:`groups` according to ``key``.
|
||||
@@ -405,9 +409,10 @@ class Results(Markable):
|
||||
"""
|
||||
keyfunc = lambda g: self.app._get_group_sort_key(g, key)
|
||||
self.groups.sort(key=keyfunc, reverse=not asc)
|
||||
self.__groups_sort_descriptor = (key,asc)
|
||||
self.__groups_sort_descriptor = (key, asc)
|
||||
|
||||
#---Properties
|
||||
dupes = property(__get_dupe_list)
|
||||
groups = property(__get_groups, __set_groups)
|
||||
dupes = property(__get_dupe_list)
|
||||
groups = property(__get_groups, __set_groups)
|
||||
stat_line = property(__get_stat_line)
|
||||
|
||||
|
||||
@@ -81,7 +81,9 @@ class Scanner:
|
||||
files = [f for f in files if f.size >= self.size_threshold]
|
||||
if self.scan_type in {ScanType.Contents, ScanType.ContentsAudio, ScanType.Folders}:
|
||||
sizeattr = 'audiosize' if self.scan_type == ScanType.ContentsAudio else 'size'
|
||||
return engine.getmatches_by_contents(files, sizeattr, partial=self.scan_type==ScanType.ContentsAudio, j=j)
|
||||
return engine.getmatches_by_contents(
|
||||
files, sizeattr, partial=self.scan_type == ScanType.ContentsAudio, j=j
|
||||
)
|
||||
else:
|
||||
j = j.start_subjob([2, 8])
|
||||
kw = {}
|
||||
@@ -94,7 +96,11 @@ class Scanner:
|
||||
func = {
|
||||
ScanType.Filename: lambda f: engine.getwords(rem_file_ext(f.name)),
|
||||
ScanType.Fields: lambda f: engine.getfields(rem_file_ext(f.name)),
|
||||
ScanType.Tag: lambda f: [engine.getwords(str(getattr(f, attrname))) for attrname in SCANNABLE_TAGS if attrname in self.scanned_tags],
|
||||
ScanType.Tag: lambda f: [
|
||||
engine.getwords(str(getattr(f, attrname)))
|
||||
for attrname in SCANNABLE_TAGS
|
||||
if attrname in self.scanned_tags
|
||||
],
|
||||
}[self.scan_type]
|
||||
for f in j.iter_with_progress(files, tr("Read metadata of %d/%d files")):
|
||||
logging.debug("Reading metadata of {}".format(str(f.path)))
|
||||
@@ -152,8 +158,10 @@ class Scanner:
|
||||
if self.ignore_list:
|
||||
j = j.start_subjob(2)
|
||||
iter_matches = j.iter_with_progress(matches, tr("Processed %d/%d matches against the ignore list"))
|
||||
matches = [m for m in iter_matches
|
||||
if not self.ignore_list.AreIgnored(str(m.first.path), str(m.second.path))]
|
||||
matches = [
|
||||
m for m in iter_matches
|
||||
if not self.ignore_list.AreIgnored(str(m.first.path), str(m.second.path))
|
||||
]
|
||||
logging.info('Grouping matches')
|
||||
groups = engine.get_groups(matches, j)
|
||||
matched_files = dedupe([m.first for m in matches] + [m.second for m in matches])
|
||||
@@ -178,10 +186,11 @@ class Scanner:
|
||||
g.prioritize(self._key_func, self._tie_breaker)
|
||||
return groups
|
||||
|
||||
match_similar_words = False
|
||||
match_similar_words = False
|
||||
min_match_percentage = 80
|
||||
mix_file_kind = True
|
||||
scan_type = ScanType.Filename
|
||||
scanned_tags = {'artist', 'title'}
|
||||
size_threshold = 0
|
||||
word_weighting = False
|
||||
mix_file_kind = True
|
||||
scan_type = ScanType.Filename
|
||||
scanned_tags = {'artist', 'title'}
|
||||
size_threshold = 0
|
||||
word_weighting = False
|
||||
|
||||
|
||||
Reference in New Issue
Block a user