qt: merge ME edition into SE

(breaks PE temporarily)

Adds a Standard/Music Application Mode button to SE and thus adds the
ability to run ME scan types in SE. When in Music mode, the
Music-specific results window, details panel and preferences panel will
show up.

All preferences except scan_type become shared between app modes
(changing the pref in a mode changes it in the other mode).

Results Window and Details Panel are now re-created at each scan
operation because they could change their type between two runs.

Preferences panel is instantiated on the fly and discarded after close.

This is a very big merge operation and I'm trying to touch as little
code as possible, sometimes at the cost of elegance. I try to minimize
the breakage that this change brings.
This commit is contained in:
Virgil Dupras 2016-05-29 22:37:38 -04:00
parent 8b878b7b13
commit 7d749779f2
18 changed files with 261 additions and 393 deletions

View File

@ -55,6 +55,11 @@ class JobType:
Copy = 'job_copy'
Delete = 'job_delete'
class AppMode:
Standard = 0
Music = 1
Picture = 2
JOBID2TITLE = {
JobType.Scan: tr("Scanning for duplicates"),
JobType.Load: tr("Loading"),
@ -149,6 +154,7 @@ class DupeGuru(Broadcaster):
# open_path(path)
# reveal_path(path)
# ask_yes_no(prompt) --> bool
# create_results_window()
# show_results_window()
# show_problem_dialog()
# select_dest_folder(prompt: str) --> str
@ -166,9 +172,8 @@ class DupeGuru(Broadcaster):
self.appdata = desktop.special_folder_path(desktop.SpecialFolder.AppData, appname=self.NAME)
if not op.exists(self.appdata):
os.makedirs(self.appdata)
self.app_mode = AppMode.Standard
self.discarded_file_count = 0
self.fileclasses = [fs.File]
self.folderclass = fs.Folder
self.directories = directories.Directories()
self.results = results.Results(self)
self.ignore_list = IgnoreList()
@ -187,10 +192,10 @@ class DupeGuru(Broadcaster):
self.problem_dialog = ProblemDialog(self)
self.ignore_list_dialog = IgnoreListDialog(self)
self.stats_label = StatsLabel(self)
self.result_table = self._create_result_table()
self.result_table = None
self.deletion_options = DeletionOptions()
self.progress_window = ProgressWindow(self._job_completed)
children = [self.result_table, self.directory_tree, self.stats_label, self.details_panel]
children = [self.directory_tree, self.stats_label, self.details_panel]
for child in children:
child.connect()
@ -746,6 +751,11 @@ class DupeGuru(Broadcaster):
if hasattr(scanner, k):
setattr(scanner, k, v)
self.results.groups = []
if self.result_table is not None:
self.result_table.disconnect()
self.result_table = self._create_result_table()
self.result_table.connect()
self.view.create_results_window()
self._results_changed()
def do(j):

View File

@ -1,9 +1,9 @@
# Created By: Virgil Dupras
# Created On: 2010-02-05
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "GPLv3" 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 "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from hscommon.gui.base import GUIObject
@ -11,14 +11,14 @@ from .base import DupeGuruGUIObject
class DetailsPanel(GUIObject, DupeGuruGUIObject):
def __init__(self, app):
GUIObject.__init__(self)
GUIObject.__init__(self, multibind=True)
DupeGuruGUIObject.__init__(self, app)
self._table = []
def _view_updated(self):
self._refresh()
self.view.refresh()
#--- Private
def _refresh(self):
if self.app.selected_dupes:
@ -33,16 +33,16 @@ class DetailsPanel(GUIObject, DupeGuruGUIObject):
data2 = self.app.get_display_info(ref, group, False)
columns = self.app.result_table.COLUMNS[1:] # first column is the 'marked' column
self._table = [(c.display, data1[c.name], data2[c.name]) for c in columns]
#--- Public
def row_count(self):
return len(self._table)
def row(self, row_index):
return self._table[row_index]
#--- Event Handlers
def dupes_selected(self):
self._refresh()
self.view.refresh()

View File

@ -384,7 +384,7 @@ class TestCaseDupeGuruWithResults:
app.JOB = Job(1, lambda *args, **kw: False) # Cancels the task
add_fake_files_to_directories(app.directories, self.objects) # We want the scan to at least start
app.start_scanning() # will be cancelled immediately
eq_(len(self.rtable), 0)
eq_(len(app.result_table), 0)
def test_selected_dupes_after_removal(self, do_setup):
# Purge the app's `selected_dupes` attribute when removing dupes, or else it might cause a

View File

@ -4,7 +4,7 @@
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from hscommon.testutil import TestApp as TestAppBase, eq_, with_app # noqa
from hscommon.testutil import TestApp as TestAppBase, CallLogger, eq_, with_app # noqa
from hscommon.path import Path
from hscommon.util import get_file_ext, format_size
from hscommon.gui.column import Column
@ -41,6 +41,8 @@ class DupeGuruView:
def ask_yes_no(self, prompt):
return True # always answer yes
def create_results_window(self):
pass
class ResultTable(ResultTableBase):
COLUMNS = [
@ -59,12 +61,16 @@ class DupeGuru(DupeGuruBase):
def __init__(self):
DupeGuruBase.__init__(self, DupeGuruView())
self.appdata = '/tmp'
self.result_table = self._create_result_table()
self.result_table.connect()
def _prioritization_categories(self):
return prioritize.all_categories()
def _create_result_table(self):
return ResultTable(self)
result = ResultTable(self)
result.view = CallLogger()
return result
class NamedObject:
@ -141,7 +147,6 @@ class TestApp(TestAppBase):
TestAppBase.__init__(self)
self.app = DupeGuru()
self.default_parent = self.app
self.rtable = link_gui(self.app.result_table)
self.dtree = link_gui(self.app.directory_tree)
self.dpanel = link_gui(self.app.details_panel)
self.slabel = link_gui(self.app.stats_label)
@ -155,6 +160,11 @@ class TestApp(TestAppBase):
link_gui(self.app.progress_window.jobdesc_textfield)
link_gui(self.app.progress_window.progressdesc_textfield)
@property
def rtable(self):
# rtable is a property because its instance can be replaced during execution
return self.app.result_table
#--- Helpers
def select_pri_criterion(self, name):
# Select a main prioritize criterion by name instead of by index. Makes tests more

View File

@ -1,41 +0,0 @@
# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from core.app import DupeGuru as DupeGuruBase
from . import prioritize
from . import __appname__
from . import scanner, fs
from .result_table import ResultTable
class DupeGuru(DupeGuruBase):
NAME = __appname__
METADATA_TO_READ = [
'size', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
'album', 'genre', 'year', 'track', 'comment'
]
SCANNER_CLASS = scanner.ScannerME
def __init__(self, view):
DupeGuruBase.__init__(self, view)
self.fileclasses = [fs.MusicFile]
def _get_dupe_sort_key(self, dupe, get_group, key, delta):
if key == 'folder_path':
dupe_folder_path = getattr(dupe, 'display_folder_path', dupe.folder_path)
return str(dupe_folder_path).lower()
return DupeGuruBase._get_dupe_sort_key(self, dupe, get_group, key, delta)
def _get_group_sort_key(self, group, key):
if key == 'folder_path':
dupe_folder_path = getattr(group.ref, 'display_folder_path', group.ref.folder_path)
return str(dupe_folder_path).lower()
return DupeGuruBase._get_group_sort_key(self, group, key)
def _prioritization_categories(self):
return prioritize.all_categories()
def _create_result_table(self):
return ResultTable(self)

View File

@ -4,24 +4,69 @@
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from core.app import DupeGuru as DupeGuruBase
from core.app import DupeGuru as DupeGuruBase, AppMode
from core import prioritize
import core_me.fs
import core_me.prioritize
import core_me.result_table
import core_me.scanner
from . import __appname__, fs, scanner
from .result_table import ResultTable
class DupeGuru(DupeGuruBase):
NAME = __appname__
METADATA_TO_READ = ['size', 'mtime']
SCANNER_CLASS = scanner.ScannerSE
def __init__(self, view):
DupeGuruBase.__init__(self, view)
self.fileclasses = [fs.File]
self.folderclass = fs.Folder
def _get_dupe_sort_key(self, dupe, get_group, key, delta):
if self.app_mode == AppMode.Music:
if key == 'folder_path':
dupe_folder_path = getattr(dupe, 'display_folder_path', dupe.folder_path)
return str(dupe_folder_path).lower()
return DupeGuruBase._get_dupe_sort_key(self, dupe, get_group, key, delta)
def _get_group_sort_key(self, group, key):
if self.app_mode == AppMode.Music:
if key == 'folder_path':
dupe_folder_path = getattr(group.ref, 'display_folder_path', group.ref.folder_path)
return str(dupe_folder_path).lower()
return DupeGuruBase._get_group_sort_key(self, group, key)
def _prioritization_categories(self):
return prioritize.all_categories()
if self.app_mode == AppMode.Music:
return prioritize.all_categories()
else:
return prioritize.all_categories()
def _create_result_table(self):
return ResultTable(self)
if self.app_mode == AppMode.Music:
return core_me.result_table.ResultTable(self)
else:
return ResultTable(self)
@property
def fileclasses(self):
if self.app_mode == AppMode.Music:
return [core_me.fs.MusicFile]
else:
return [fs.File]
@property
def SCANNER_CLASS(self):
if self.app_mode == AppMode.Music:
return core_me.scanner.ScannerME
else:
return scanner.ScannerSE
@property
def METADATA_TO_READ(self):
if self.app_mode == AppMode.Music:
return [
'size', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
'album', 'genre', 'year', 'track', 'comment'
]
else:
return ['size', 'mtime']

@ -1 +1 @@
Subproject commit ea634cefdf78ae9e4c7470e571fce859760f6f38
Subproject commit 316af1bca53915f99b9bb874064cba6bee881cc1

View File

@ -47,7 +47,6 @@ class DupeGuru(QObject):
self.prefs.load()
self.model = self.MODELCLASS(view=self)
self._setup()
self.prefsChanged.emit(self.prefs)
#--- Private
def _setup(self):
@ -55,15 +54,14 @@ class DupeGuru(QObject):
self._update_options()
self.recentResults = Recent(self, 'recentResults')
self.recentResults.mustOpenItem.connect(self.model.load_from)
self.resultWindow = None
self.details_dialog = None
self.directories_dialog = DirectoriesDialog(self)
self.resultWindow = self.RESULT_WINDOW_CLASS(self.directories_dialog, self)
self.progress_window = ProgressWindow(self.resultWindow, self.model.progress_window)
self.details_dialog = self.DETAILS_DIALOG_CLASS(self.resultWindow, self)
self.problemDialog = ProblemDialog(parent=self.resultWindow, model=self.model.problem_dialog)
self.ignoreListDialog = IgnoreListDialog(parent=self.resultWindow, model=self.model.ignore_list_dialog)
self.deletionOptions = DeletionOptions(parent=self.resultWindow, model=self.model.deletion_options)
self.preferences_dialog = self.PREFERENCES_DIALOG_CLASS(self.resultWindow, self)
self.about_box = AboutBox(self.resultWindow, self)
self.progress_window = ProgressWindow(self.directories_dialog, self.model.progress_window)
self.problemDialog = ProblemDialog(parent=self.directories_dialog, model=self.model.problem_dialog)
self.ignoreListDialog = IgnoreListDialog(parent=self.directories_dialog, model=self.model.ignore_list_dialog)
self.deletionOptions = DeletionOptions(parent=self.directories_dialog, model=self.model.deletion_options)
self.about_box = AboutBox(self.directories_dialog, self)
self.directories_dialog.show()
self.model.load()
@ -94,6 +92,7 @@ class DupeGuru(QObject):
self.model.options['clean_empty_dirs'] = self.prefs.remove_empty_folders
self.model.options['ignore_hardlink_matches'] = self.prefs.ignore_hardlink_matches
self.model.options['copymove_dest_type'] = self.prefs.destination_type
self.model.options['scan_type'] = self.prefs.get_scan_type(self.model.app_mode)
#--- Public
def add_selected_to_ignore_list(self):
@ -112,14 +111,15 @@ class DupeGuru(QObject):
self.model.invoke_custom_command()
def show_details(self):
self.details_dialog.show()
if self.details_dialog is not None:
self.details_dialog.show()
def showResultsWindow(self):
self.resultWindow.show()
if self.resultWindow is not None:
self.resultWindow.show()
#--- Signals
willSavePrefs = pyqtSignal()
prefsChanged = pyqtSignal(object)
#--- Events
def finishedLaunching(self):
@ -143,13 +143,14 @@ class DupeGuru(QObject):
desktop.open_path(debugLogPath)
def preferencesTriggered(self):
self.preferences_dialog.load()
result = self.preferences_dialog.exec()
preferences_dialog = self.PREFERENCES_DIALOG_CLASS(self.directories_dialog, self)
preferences_dialog.load()
result = preferences_dialog.exec()
if result == QDialog.Accepted:
self.preferences_dialog.save()
preferences_dialog.save()
self.prefs.save()
self._update_options()
self.prefsChanged.emit(self.prefs)
preferences_dialog.setParent(None)
def quitTriggered(self):
self.directories_dialog.close()
@ -176,6 +177,18 @@ class DupeGuru(QObject):
def ask_yes_no(self, prompt):
return self.confirm('', prompt)
def create_results_window(self):
"""Creates resultWindow and details_dialog depending on the selected ``app_mode``.
"""
if self.details_dialog is not None:
self.details_dialog.close()
self.details_dialog.setParent(None)
if self.resultWindow is not None:
self.resultWindow.close()
self.resultWindow.setParent(None)
self.resultWindow = self.RESULT_WINDOW_CLASS(self.directories_dialog, self)
self.details_dialog = self.DETAILS_DIALOG_CLASS(self.resultWindow, self)
def show_results_window(self):
self.showResultsWindow()

View File

@ -13,6 +13,8 @@ from PyQt5.QtWidgets import (
from PyQt5.QtGui import QPixmap, QIcon
from hscommon.trans import trget
from core.app import AppMode
from qtlib.radio_box import RadioBox
from qtlib.recent import Recent
from qtlib.util import moveToScreenCenter, createActions
@ -28,9 +30,7 @@ class DirectoriesDialog(QMainWindow):
self.lastAddedFolder = platform.INITIAL_FOLDER_IN_DIALOGS
self.recentFolders = Recent(self.app, 'recentFolders')
self._setupUi()
SCAN_TYPE_ORDER = [so.scan_type for so in self.app.model.SCANNER_CLASS.get_scan_options()]
scan_type_index = SCAN_TYPE_ORDER.index(self.app.prefs.scan_type)
self.scanTypeComboBox.setCurrentIndex(scan_type_index)
self._updateScanTypeList()
self.directoriesModel = DirectoriesModel(self.app.model.directory_tree, view=self.treeView)
self.directoriesDelegate = DirectoriesDelegate()
self.treeView.setItemDelegate(self.directoriesDelegate)
@ -41,11 +41,12 @@ class DirectoriesDialog(QMainWindow):
self._updateAddButton()
self._updateRemoveButton()
self._updateLoadResultsButton()
self._updateActionsState()
self._setupBindings()
def _setupBindings(self):
self.appModeRadioBox.itemSelected.connect(self.appModeButtonSelected)
self.showPreferencesButton.clicked.connect(self.app.actionPreferences.trigger)
self.scanTypeComboBox.currentIndexChanged[int].connect(self.scanTypeChanged)
self.scanButton.clicked.connect(self.scanButtonClicked)
self.loadResultsButton.clicked.connect(self.actionLoadResults.trigger)
self.addFolderButton.clicked.connect(self.actionAddFolder.trigger)
@ -123,6 +124,13 @@ class DirectoriesDialog(QMainWindow):
self.treeView.setUniformRowHeights(True)
self.verticalLayout.addWidget(self.treeView)
hl = QHBoxLayout()
label = QLabel(tr("Application Mode:"), self)
label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
hl.addWidget(label)
self.appModeRadioBox = RadioBox(self, items=[tr("Standard"), tr("Music")], spread=False)
hl.addWidget(self.appModeRadioBox)
self.verticalLayout.addLayout(hl)
hl = QHBoxLayout()
hl.setAlignment(Qt.AlignLeft)
label = QLabel(tr("Scan Type:"), self)
label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
@ -130,10 +138,8 @@ class DirectoriesDialog(QMainWindow):
self.scanTypeComboBox = QComboBox(self)
self.scanTypeComboBox.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed))
self.scanTypeComboBox.setMaximumWidth(400)
for scan_option in self.app.model.SCANNER_CLASS.get_scan_options():
self.scanTypeComboBox.addItem(scan_option.label)
hl.addWidget(self.scanTypeComboBox)
self.showPreferencesButton = QPushButton(tr("Options"), self.centralwidget)
self.showPreferencesButton = QPushButton(tr("More Options"), self.centralwidget)
self.showPreferencesButton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
hl.addWidget(self.showPreferencesButton)
self.verticalLayout.addLayout(hl)
@ -172,6 +178,9 @@ class DirectoriesDialog(QMainWindow):
header.setSectionResizeMode(1, QHeaderView.Fixed)
header.resizeSection(1, 100)
def _updateActionsState(self):
self.actionShowResultsWindow.setEnabled(self.app.resultWindow is not None)
def _updateAddButton(self):
if self.recentFolders.isEmpty():
self.addFolderButton.setMenu(None)
@ -191,6 +200,23 @@ class DirectoriesDialog(QMainWindow):
else:
self.loadResultsButton.setMenu(self.menuRecentResults)
def _updateScanTypeList(self):
try:
self.scanTypeComboBox.currentIndexChanged[int].disconnect(self.scanTypeChanged)
except TypeError:
# Not connected, ignore
pass
self.scanTypeComboBox.clear()
scan_options = self.app.model.SCANNER_CLASS.get_scan_options()
for scan_option in scan_options:
self.scanTypeComboBox.addItem(scan_option.label)
SCAN_TYPE_ORDER = [so.scan_type for so in scan_options]
selected_scan_type = self.app.prefs.get_scan_type(self.app.model.app_mode)
scan_type_index = SCAN_TYPE_ORDER.index(selected_scan_type)
self.scanTypeComboBox.setCurrentIndex(scan_type_index)
self.scanTypeComboBox.currentIndexChanged[int].connect(self.scanTypeChanged)
self.app._update_options()
#--- QWidget overrides
def closeEvent(self, event):
event.accept()
@ -213,6 +239,14 @@ class DirectoriesDialog(QMainWindow):
self.app.model.add_directory(dirpath)
self.recentFolders.insertItem(dirpath)
def appModeButtonSelected(self, index):
if index == 1:
mode = AppMode.Music
else:
mode = AppMode.Standard
self.app.model.app_mode = mode
self._updateScanTypeList()
def appWillSavePrefs(self):
self.app.prefs.directoriesWindowRect = self.geometry()
@ -241,7 +275,7 @@ class DirectoriesDialog(QMainWindow):
def scanTypeChanged(self, index):
scan_options = self.app.model.SCANNER_CLASS.get_scan_options()
self.app.prefs.scan_type = scan_options[index].scan_type
self.app.prefs.set_scan_type(self.app.model.app_mode, scan_options[index].scan_type)
self.app._update_options()
def selectionChanged(self, selected, deselected):

View File

@ -7,6 +7,8 @@
from PyQt5.QtWidgets import QApplication
from hscommon import trans
from core.app import AppMode
from core.scanner import ScanType
from qtlib.preferences import Preferences as PreferencesBase
class Preferences(PreferencesBase):
@ -89,10 +91,14 @@ class Preferences(PreferencesBase):
self._save_specific(settings)
# scan_type is special because we save it immediately when we set it.
@property
def scan_type(self):
return self.get_value('ScanType', self.DEFAULT_SCAN_TYPE)
def get_scan_type(self, app_mode):
if app_mode == AppMode.Music:
return self.get_value('ScanTypeMusic', ScanType.Tag)
else:
return self.get_value('ScanTypeStandard', ScanType.Contents)
@scan_type.setter
def scan_type(self, value):
self.set_value('ScanType', value)
def set_scan_type(self, app_mode, value):
if app_mode == AppMode.Music:
self.set_value('ScanTypeMusic', value)
else:
self.set_value('ScanTypeStandard', value)

View File

@ -1,9 +1,9 @@
# Created By: Virgil Dupras
# Created On: 2009-04-23
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "GPLv3" 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 "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from PyQt5.QtCore import Qt, pyqtSignal, QModelIndex
@ -17,13 +17,17 @@ class ResultsModel(Table):
model = app.model.result_table
super().__init__(model, view, **kwargs)
view.horizontalHeader().setSortIndicator(1, Qt.AscendingOrder)
app.prefsChanged.connect(self.appPrefsChanged)
font = view.font()
font.setPointSize(app.prefs.tableFontSize)
self.view.setFont(font)
fm = QFontMetrics(font)
view.verticalHeader().setDefaultSectionSize(fm.height()+2)
app.willSavePrefs.connect(self.appWillSavePrefs)
def _getData(self, row, column, role):
if column.name == 'marked':
if role == Qt.CheckStateRole and row.markable:
if role == Qt.CheckStateRole and row.markable:
return Qt.Checked if row.marked else Qt.Unchecked
return None
if role == Qt.DisplayRole:
@ -43,7 +47,7 @@ class ResultsModel(Table):
if column.name == 'name':
return row.data[column.name]
return None
def _getFlags(self, row, column):
flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
if column.name == 'marked':
@ -52,7 +56,7 @@ class ResultsModel(Table):
elif column.name == 'name':
flags |= Qt.ItemIsEditable
return flags
def _setData(self, row, column, value, role):
if role == Qt.CheckStateRole:
if column.name == 'marked':
@ -62,46 +66,39 @@ class ResultsModel(Table):
if column.name == 'name':
return self.model.rename_selected(value)
return False
def sort(self, column, order):
column = self.model.COLUMNS[column]
self.model.sort(column.name, order == Qt.AscendingOrder)
#--- Properties
@property
def power_marker(self):
return self.model.power_marker
@power_marker.setter
def power_marker(self, value):
self.model.power_marker = value
@property
def delta_values(self):
return self.model.delta_values
@delta_values.setter
def delta_values(self, value):
self.model.delta_values = value
#--- Events
def appPrefsChanged(self, prefs):
font = self.view.font()
font.setPointSize(prefs.tableFontSize)
self.view.setFont(font)
fm = QFontMetrics(font)
self.view.verticalHeader().setDefaultSectionSize(fm.height()+2)
def appWillSavePrefs(self):
self.model.columns.save_columns()
#--- model --> view
def invalidate_markings(self):
# redraw view
# HACK. this is the only way I found to update the widget without reseting everything
self.view.scroll(0, 1)
self.view.scroll(0, -1)
class ResultsView(QTableView):
#--- Override
@ -110,10 +107,10 @@ class ResultsView(QTableView):
self.spacePressed.emit()
return
super().keyPressEvent(event)
def mouseDoubleClickEvent(self, event):
self.doubleClicked.emit(QModelIndex())
# We don't call the superclass' method because the default behavior is to rename the cell.
#--- Signals
spacePressed = pyqtSignal()

View File

@ -1,47 +0,0 @@
# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from core_me import __appname__
from core_me.app import DupeGuru as DupeGuruModel
from ..base.app import DupeGuru as DupeGuruBase
from .details_dialog import DetailsDialog
from .results_model import ResultsModel
from .preferences import Preferences
from .preferences_dialog import PreferencesDialog
class DupeGuru(DupeGuruBase):
MODELCLASS = DupeGuruModel
EDITION = 'me'
LOGO_NAME = 'logo_me'
NAME = __appname__
DETAILS_DIALOG_CLASS = DetailsDialog
RESULT_MODEL_CLASS = ResultsModel
PREFERENCES_CLASS = Preferences
PREFERENCES_DIALOG_CLASS = PreferencesDialog
def _update_options(self):
DupeGuruBase._update_options(self)
self.model.options['min_match_percentage'] = self.prefs.filter_hardness
self.model.options['scan_type'] = self.prefs.scan_type
self.model.options['word_weighting'] = self.prefs.word_weighting
self.model.options['match_similar_words'] = self.prefs.match_similar
scanned_tags = set()
if self.prefs.scan_tag_track:
scanned_tags.add('track')
if self.prefs.scan_tag_artist:
scanned_tags.add('artist')
if self.prefs.scan_tag_album:
scanned_tags.add('album')
if self.prefs.scan_tag_title:
scanned_tags.add('title')
if self.prefs.scan_tag_genre:
scanned_tags.add('genre')
if self.prefs.scan_tag_year:
scanned_tags.add('year')
self.model.options['scanned_tags'] = scanned_tags

View File

@ -1,164 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<DOCUMENT Type="Advanced Installer" CreateVersion="4.7.2" version="8.0.2" Modules="professional" RootPath="." Language="en">
<COMPONENT cid="caphyon.advinst.msicomp.MsiPropsComponent">
<ROW Property="AI_SHORTCUTSREG" Value="0|0|0|"/>
<ROW Property="ALLUSERS" Value="2"/>
<ROW Property="ARPCOMMENTS" Value="This installer database contains the logic and data required to install [|ProductName]." ValueLocId="*"/>
<ROW Property="ARPCONTACT" Value="support@hardcoded.net"/>
<ROW Property="ARPHELPLINK" Value="http://www.hardcoded.net/support/"/>
<ROW Property="ARPURLINFOABOUT" Value="http://www.hardcoded.net/dupeguru_me/"/>
<ROW Property="ARPURLUPDATEINFO" Value="http://www.hardcoded.net/dupeguru_me/"/>
<ROW Property="BannerBitmap" MultiBuildValue="DefaultBuild:banner_image.jpg" Type="1"/>
<ROW Property="CTRLS" Value="2"/>
<ROW Property="DialogBitmap" MultiBuildValue="DefaultBuild:dialog_image.jpg" Type="1"/>
<ROW Property="Manufacturer" Value="Hardcoded Software" ValueLocId="*"/>
<ROW Property="ProductCode" Value="1033:{A7037226-389C-4704-81C3-E2CA17D11058} " Type="16"/>
<ROW Property="ProductLanguage" Value="1033"/>
<ROW Property="ProductName" Value="dupeGuru Music Edition" ValueLocId="*"/>
<ROW Property="ProductVersion" Value="5.6.0"/>
<ROW Property="RUNAPPLICATION" Value="1" Type="4"/>
<ROW Property="SecureCustomProperties" Value="OLDPRODUCTS;AI_NEWERPRODUCTFOUND"/>
<ROW Property="UpgradeCode" Value="{E11BFC48-7639-44BD-BB5B-A6AC934BC12D}"/>
<ROW Property="WindowsType9X" MultiBuildValue="DefaultBuild:Windows 9x/ME" ValueLocId="-"/>
<ROW Property="WindowsType9XDisplay" MultiBuildValue="DefaultBuild:Windows 9x/ME" ValueLocId="-"/>
<ROW Property="WindowsTypeNT" MultiBuildValue="DefaultBuild:Windows NT/2k/XP/Vista/Windows7 x86" ValueLocId="-"/>
<ROW Property="WindowsTypeNT64" MultiBuildValue="DefaultBuild:Windows XP x64, Windows XP x64 ServicePack 1, Windows XP x64 ServicePack 2, Windows Server 2003 x64, Windows Server 2003 x64 Service Pack 1, Windows Server 2003 x64 Service pack 2" ValueLocId="-"/>
<ROW Property="WindowsTypeNT64Display" MultiBuildValue="DefaultBuild:Windows XP x64, Windows Server 2003 x64" ValueLocId="-"/>
<ROW Property="WindowsTypeNTDisplay" MultiBuildValue="DefaultBuild:Windows NT/2k/XP/Vista/Windows7 x86" ValueLocId="-"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiDirsComponent">
<ROW Directory="APPDIR" Directory_Parent="TARGETDIR" DefaultDir="APPDIR:." IsPseudoRoot="1"/>
<ROW Directory="DesktopFolder" Directory_Parent="TARGETDIR" DefaultDir="Deskto~1|DesktopFolder" IsPseudoRoot="1"/>
<ROW Directory="SHORTCUTDIR" Directory_Parent="TARGETDIR" DefaultDir="SHORTC~1|SHORTCUTDIR" IsPseudoRoot="1"/>
<ROW Directory="TARGETDIR" DefaultDir="SourceDir"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiCompsComponent">
<ROW Component="AIShRegAnswer" ComponentId="{7DB5C163-2978-436F-A47D-54F1C76F1D64}" Directory_="APPDIR" Attributes="4" KeyPath="AIShRegAnswer"/>
<ROW Component="CurrentVersion" ComponentId="{1EF88868-DCB1-4F7D-83F6-CC17C9F65740}" Directory_="APPDIR" Attributes="4" KeyPath="CurrentVersion"/>
<ROW Component="SHORTCUTDIR" ComponentId="{B21C7D52-4D02-4470-AD46-9EF2780EEB12}" Directory_="SHORTCUTDIR" Attributes="0"/>
<ROW Component="dupeGuru_ME.exe" ComponentId="{14F7B73A-FA85-4C10-AA92-10BE05FE103B}" Directory_="APPDIR" Attributes="0" KeyPath="dupeGuru_ME.exe"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiFeatsComponent">
<ROW Feature="MainFeature" Title="MainFeature" Description="Description" Display="1" Level="1" Directory_="APPDIR" Attributes="0" Components="dupeGuru_ME.exe AIShRegAnswer CurrentVersion SHORTCUTDIR"/>
<ATTRIBUTE name="CurrentFeature" value="MainFeature"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiFilesComponent">
<ROW File="dupeGuru_ME.exe" Component_="dupeGuru_ME.exe" FileName="dupeGu~1.exe|dupeGuru ME.exe" Version="65535.65535.65535.65535" Attributes="0" SourcePath="dist\dupeGuru ME.exe" SelfReg="false" Sequence="1"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.BuildComponent">
<ROW BuildKey="DefaultBuild" BuildName="DefaultBuild" BuildOrder="1" BuildType="0" PackageFolder="install" PackageFileName="dupeguru_me_win64_[|ProductVersion]" Languages="en" InstallationType="4" CreateMd5="true" ExtUI="true" MsiPackageType="x64"/>
<ATTRIBUTE name="CurrentBuild" value="DefaultBuild"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.CacheComponent">
<ATTRIBUTE name="Enable" value="false"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.DictionaryComponent">
<ROW Path="&lt;AI_DICTS&gt;ui.ail"/>
<ROW Path="&lt;AI_DICTS&gt;ui_en.ail"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.FragmentComponent">
<ROW Fragment="CommonUI.aip" Path="&lt;AI_FRAGS&gt;CommonUI.aip"/>
<ROW Fragment="FolderDlg.aip" Path="&lt;AI_THEMES&gt;classic\fragments\FolderDlg.aip"/>
<ROW Fragment="SequenceDialogs.aip" Path="&lt;AI_THEMES&gt;classic\fragments\SequenceDialogs.aip"/>
<ROW Fragment="Sequences.aip" Path="&lt;AI_FRAGS&gt;Sequences.aip"/>
<ROW Fragment="ShortcutsDlg.aip" Path="&lt;AI_THEMES&gt;classic\fragments\ShortcutsDlg.aip"/>
<ROW Fragment="StaticUIStrings.aip" Path="&lt;AI_FRAGS&gt;StaticUIStrings.aip"/>
<ROW Fragment="UI.aip" Path="&lt;AI_THEMES&gt;classic\fragments\UI.aip"/>
<ROW Fragment="Validation.aip" Path="&lt;AI_FRAGS&gt;Validation.aip"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiAppSearchComponent">
<ROW Property="AI_SHORTCUTSREG" Signature_="AI_ShRegOptionMachine"/>
<ROW Property="AI_SHORTCUTSREG" Signature_="AI_ShRegOptionUser"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiBinaryComponent">
<ROW Name="aicustact.dll" SourcePath="&lt;AI_CUSTACTS&gt;aicustact.dll"/>
<ROW Name="banner_image.jpg" SourcePath="&lt;AI_THEMES&gt;classic\resources\banner-image.jpg"/>
<ROW Name="dialog_image.jpg" SourcePath="&lt;AI_THEMES&gt;classic\resources\dialog-image.jpg"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiControlComponent">
<ATTRIBUTE name="FixedSizeBitmaps" value="0"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiControlConditionComponent">
<ROW Dialog_="ShortcutsDlg" Control_="QuickLaunchShorcutsCheckBox" Action="Hide" Condition="(Not Installed) AND (VersionNT&lt;&quot;601&quot;)"/>
<ROW Dialog_="ShortcutsDlg" Control_="StartupShorcutsCheckBox" Action="Hide" Condition="(Not Installed)"/>
<ATTRIBUTE name="DeletedRows" value="ShortcutsDlg#QuickLaunchShorcutsCheckBox#Show#(Not Installed) AND (VersionNT&lt;&quot;601&quot;)@ShortcutsDlg#StartupShorcutsCheckBox#Show#(Not Installed)@ShortcutsDlg#QuickLaunchShorcutsCheckBox#Show#(Not Installed)"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiControlEventComponent">
<ROW Dialog_="FolderDlg" Control_="Back" Event="NewDialog" Argument="ShortcutsDlg" Condition="AI_INSTALL" Ordering="1"/>
<ROW Dialog_="WelcomeDlg" Control_="Next" Event="NewDialog" Argument="ShortcutsDlg" Condition="AI_INSTALL" Ordering="1"/>
<ROW Dialog_="VerifyReadyDlg" Control_="Back" Event="NewDialog" Argument="FolderDlg" Condition="AI_INSTALL" Ordering="1"/>
<ROW Dialog_="FolderDlg" Control_="Next" Event="NewDialog" Argument="VerifyReadyDlg" Condition="AI_INSTALL" Ordering="3"/>
<ROW Dialog_="MaintenanceTypeDlg" Control_="Back" Event="NewDialog" Argument="MaintenanceWelcomeDlg" Condition="AI_MAINT" Ordering="1"/>
<ROW Dialog_="MaintenanceWelcomeDlg" Control_="Next" Event="NewDialog" Argument="MaintenanceTypeDlg" Condition="AI_MAINT" Ordering="2"/>
<ROW Dialog_="VerifyReadyDlg" Control_="Back" Event="NewDialog" Argument="PatchWelcomeDlg" Condition="AI_PATCH" Ordering="1"/>
<ROW Dialog_="PatchWelcomeDlg" Control_="Next" Event="NewDialog" Argument="VerifyReadyDlg" Condition="AI_PATCH" Ordering="3"/>
<ROW Dialog_="ShortcutsDlg" Control_="Back" Event="NewDialog" Argument="WelcomeDlg" Condition="AI_INSTALL" Ordering="1"/>
<ROW Dialog_="ShortcutsDlg" Control_="Next" Event="NewDialog" Argument="FolderDlg" Condition="AI_INSTALL" Ordering="1"/>
<ROW Dialog_="CustomizeDlg" Control_="Back" Event="NewDialog" Argument="MaintenanceTypeDlg" Condition="AI_MAINT" Ordering="1"/>
<ROW Dialog_="CustomizeDlg" Control_="Next" Event="NewDialog" Argument="VerifyReadyDlg" Condition="AI_MAINT" Ordering="1"/>
<ROW Dialog_="MaintenanceTypeDlg" Control_="ChangeButton" Event="NewDialog" Argument="CustomizeDlg" Condition="AI_MAINT" Ordering="301"/>
<ROW Dialog_="ResumeDlg" Control_="Install" Event="EndDialog" Argument="Return" Condition="AI_RESUME" Ordering="299"/>
<ROW Dialog_="VerifyReadyDlg" Control_="Install" Event="EndDialog" Argument="Return" Condition="AI_MAINT" Ordering="197"/>
<ROW Dialog_="VerifyReadyDlg" Control_="Install" Event="EndDialog" Argument="Return" Condition="AI_PATCH" Ordering="198"/>
<ROW Dialog_="VerifyReadyDlg" Control_="Install" Event="EndDialog" Argument="Return" Condition="AI_INSTALL" Ordering="199"/>
<ROW Dialog_="VerifyReadyDlg" Control_="Back" Event="NewDialog" Argument="CustomizeDlg" Condition="AI_MAINT" Ordering="201"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiCreateFolderComponent">
<ROW Directory_="SHORTCUTDIR" Component_="SHORTCUTDIR"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiCustActComponent">
<ROW Action="AI_DELETE_SHORTCUTS" Type="1" Source="aicustact.dll" Target="DeleteShortcuts"/>
<ROW Action="AI_DOWNGRADE" Type="19" Target="4010"/>
<ROW Action="AI_LaunchApp" Type="1" Source="aicustact.dll" Target="[#dupeGuru_ME.exe]"/>
<ROW Action="AI_PREPARE_UPGRADE" Type="65" Source="aicustact.dll" Target="PrepareUpgrade"/>
<ROW Action="AI_RESTORE_LOCATION" Type="65" Source="aicustact.dll" Target="RestoreLocation"/>
<ROW Action="AI_ResolveKnownFolders" Type="1" Source="aicustact.dll" Target="AI_ResolveKnownFolders"/>
<ROW Action="AI_STORE_LOCATION" Type="51" Source="ARPINSTALLLOCATION" Target="[APPDIR]"/>
<ROW Action="SET_APPDIR" Type="307" Source="APPDIR" Target="[ProgramFilesFolder][Manufacturer]\[ProductName]" MultiBuildTarget="DefaultBuild:[ProgramFiles64Folder][Manufacturer]\[ProductName]"/>
<ROW Action="SET_SHORTCUTDIR" Type="307" Source="SHORTCUTDIR" Target="[ProgramMenuFolder][ProductName]"/>
<ROW Action="SET_TARGETDIR_TO_APPDIR" Type="51" Source="TARGETDIR" Target="[APPDIR]"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiIconsComponent">
<ROW Name="SystemFolder_msiexec.exe" SourcePath="&lt;AI_RES&gt;uninstall.ico" Index="0"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiInstExSeqComponent">
<ROW Action="AI_DOWNGRADE" Condition="AI_NEWERPRODUCTFOUND AND (UILevel &lt;&gt; 5)" Sequence="210"/>
<ROW Action="AI_RESTORE_LOCATION" Condition="APPDIR=&quot;&quot;" Sequence="740"/>
<ROW Action="AI_STORE_LOCATION" Condition="Not Installed" Sequence="1545"/>
<ROW Action="AI_PREPARE_UPGRADE" Condition="AI_UPGRADE=&quot;No&quot; AND (Not Installed)" Sequence="1300"/>
<ROW Action="AI_DELETE_SHORTCUTS" Condition="NOT (REMOVE=&quot;ALL&quot;)" Sequence="1449"/>
<ROW Action="AI_ResolveKnownFolders" Sequence="51"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiInstallUISequenceComponent">
<ROW Action="AI_RESTORE_LOCATION" Condition="APPDIR=&quot;&quot;" Sequence="740"/>
<ROW Action="AI_ResolveKnownFolders" Sequence="51"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiLaunchConditionsComponent">
<ROW Condition="( Version9X OR ( NOT VersionNT64 ) OR ( VersionNT64 AND ((VersionNT64 &lt;&gt; 502) OR (((VersionNT64 = 502) AND (ServicePackLevel &gt;= 1)) OR (MsiNTProductType &lt;&gt; 1))) AND ((VersionNT64 &lt;&gt; 502) OR (((VersionNT64 = 502) AND (ServicePackLevel &lt;&gt; 1)) OR (MsiNTProductType &lt;&gt; 1))) AND ((VersionNT64 &lt;&gt; 502) OR (((VersionNT64 = 502) AND (ServicePackLevel &lt;&gt; 2)) OR (MsiNTProductType &lt;&gt; 1))) AND ((VersionNT64 &lt;&gt; 502) OR ((VersionNT64 = 502) AND ((MsiNTProductType = 1) OR (ServicePackLevel &gt;= 1)))) AND ((VersionNT64 &lt;&gt; 502) OR ((VersionNT64 = 502) AND ((MsiNTProductType = 1) OR (ServicePackLevel &lt;&gt; 1)))) AND ((VersionNT64 &lt;&gt; 502) OR ((VersionNT64 = 502) AND ((MsiNTProductType = 1) OR (ServicePackLevel &lt;&gt; 2)))) ) )" Description="[ProductName] cannot be installed on the following Windows versions: [WindowsTypeNT64Display]" DescriptionLocId="AI.LaunchCondition.NoSpecificNT64" IsPredefined="true" Builds="DefaultBuild"/>
<ROW Condition="( Version9X OR VersionNT64 )" Description="[ProductName] cannot be installed on [WindowsTypeNTDisplay]" DescriptionLocId="AI.LaunchCondition.NoNT" IsPredefined="true" Builds="DefaultBuild"/>
<ROW Condition="VersionNT" Description="[ProductName] cannot be installed on [WindowsType9XDisplay]" DescriptionLocId="AI.LaunchCondition.No9X" IsPredefined="true" Builds="DefaultBuild"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiRegLocatorComponent">
<ROW Signature_="AI_ShRegOptionMachine" Root="2" Key="Software\Caphyon\Advanced Installer\Installs\[ProductCode]" Name="AIShRegAnswer" Type="2"/>
<ROW Signature_="AI_ShRegOptionUser" Root="1" Key="Software\Caphyon\Advanced Installer\Installs\[ProductCode]" Name="AIShRegAnswer" Type="2"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiRegsComponent">
<ROW Registry="AIShRegAnswer" Root="-1" Key="Software\Caphyon\Advanced Installer\Installs\[ProductCode]" Name="AIShRegAnswer" Value="[AI_SHORTCUTSREG]" Component_="AIShRegAnswer"/>
<ROW Registry="CurrentVersion" Root="-1" Key="Software\[Manufacturer]\[ProductName]" Name="CurrentVersion" Value="[ProductVersion]" Component_="CurrentVersion"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiShortsComponent">
<ROW Shortcut="Uninstall_dupeGuru_ME" Directory_="SHORTCUTDIR" Name="Uninst~1|Uninstall dupeGuru ME" Component_="AIShRegAnswer" Target="[SystemFolder]msiexec.exe" Arguments="/x [ProductCode]" Hotkey="0" Icon_="SystemFolder_msiexec.exe" IconIndex="0" ShowCmd="1"/>
<ROW Shortcut="dupeGuru_ME" Directory_="SHORTCUTDIR" Name="dupeGu~1|dupeGuru ME" Component_="dupeGuru_ME.exe" Target="[#dupeGuru_ME.exe]" Hotkey="0" IconIndex="0" ShowCmd="1" WkDir="APPDIR"/>
<ROW Shortcut="dupeGuru_ME_1" Directory_="DesktopFolder" Name="dupeGu~1|dupeGuru ME" Component_="dupeGuru_ME.exe" Target="[#dupeGuru_ME.exe]" Hotkey="0" IconIndex="0" ShowCmd="1" WkDir="APPDIR"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiThemeComponent">
<ATTRIBUTE name="UsedTheme" value="classic"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiUpgradeComponent">
<ROW UpgradeCode="[|UpgradeCode]" VersionMax="[|ProductVersion]" Attributes="1025" ActionProperty="OLDPRODUCTS"/>
<ROW UpgradeCode="[|UpgradeCode]" VersionMin="[|ProductVersion]" Attributes="2" ActionProperty="AI_NEWERPRODUCTFOUND"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.SynchronizedFolderComponent">
<ROW Directory_="APPDIR" SourcePath="dist" Feature="MainFeature" ExcludePattern="*~|#*#|%*%|._|CVS|.cvsignore|SCCS|vssver.scc|mssccprj.scc|vssver2.scc|.svn|.DS_Store|*.pdb|*.vshost.*" ExcludeFlags="6"/>
</COMPONENT>
</DOCUMENT>

View File

@ -1,46 +0,0 @@
# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from core.scanner import ScanType
from ..base.preferences import Preferences as PreferencesBase
class Preferences(PreferencesBase):
DEFAULT_SCAN_TYPE = ScanType.Tag
def _load_specific(self, settings):
get = self.get_value
self.word_weighting = get('WordWeighting', self.word_weighting)
self.match_similar = get('MatchSimilar', self.match_similar)
self.scan_tag_track = get('ScanTagTrack', self.scan_tag_track)
self.scan_tag_artist = get('ScanTagArtist', self.scan_tag_artist)
self.scan_tag_album = get('ScanTagAlbum', self.scan_tag_album)
self.scan_tag_title = get('ScanTagTitle', self.scan_tag_title)
self.scan_tag_genre = get('ScanTagGenre', self.scan_tag_genre)
self.scan_tag_year = get('ScanTagYear', self.scan_tag_year)
def _reset_specific(self):
self.filter_hardness = 80
self.word_weighting = True
self.match_similar = False
self.scan_tag_track = False
self.scan_tag_artist = True
self.scan_tag_album = True
self.scan_tag_title = True
self.scan_tag_genre = False
self.scan_tag_year = False
def _save_specific(self, settings):
set_ = self.set_value
set_('WordWeighting', self.word_weighting)
set_('MatchSimilar', self.match_similar)
set_('ScanTagTrack', self.scan_tag_track)
set_('ScanTagArtist', self.scan_tag_artist)
set_('ScanTagAlbum', self.scan_tag_album)
set_('ScanTagTitle', self.scan_tag_title)
set_('ScanTagGenre', self.scan_tag_genre)
set_('ScanTagYear', self.scan_tag_year)

View File

@ -10,10 +10,11 @@ from PyQt5.QtWidgets import (
)
from hscommon.trans import trget
from core.app import AppMode
from core.scanner import ScanType
from ..base.preferences_dialog import PreferencesDialogBase
from . import preferences
from ..se import preferences
tr = trget('ui')
@ -74,7 +75,7 @@ class PreferencesDialog(PreferencesDialogBase):
setchecked(self.wordWeightingBox, prefs.word_weighting)
# Update UI state based on selected scan type
scan_type = prefs.scan_type
scan_type = prefs.get_scan_type(AppMode.Music)
word_based = scan_type in (
ScanType.Filename, ScanType.Fields, ScanType.FieldsNoOrder,
ScanType.Tag

View File

@ -7,12 +7,16 @@
from core_se import __appname__
from core_se.app import DupeGuru as DupeGuruModel
from core.directories import Directories as DirectoriesBase, DirectoryState
from core.app import AppMode
from ..base.app import DupeGuru as DupeGuruBase
from .details_dialog import DetailsDialog
from .results_model import ResultsModel
from .details_dialog import DetailsDialog as DetailsDialogStandard
from ..me.details_dialog import DetailsDialog as DetailsDialogMusic
from .results_model import ResultsModel as ResultsModelStandard
from ..me.results_model import ResultsModel as ResultsModelMusic
from .preferences import Preferences
from .preferences_dialog import PreferencesDialog
from .preferences_dialog import PreferencesDialog as PreferencesDialogStandard
from ..me.preferences_dialog import PreferencesDialog as PreferencesDialogMusic
class Directories(DirectoriesBase):
ROOT_PATH_TO_EXCLUDE = frozenset(['windows', 'program files'])
@ -30,10 +34,7 @@ class DupeGuru(DupeGuruBase):
LOGO_NAME = 'logo_se'
NAME = __appname__
DETAILS_DIALOG_CLASS = DetailsDialog
RESULT_MODEL_CLASS = ResultsModel
PREFERENCES_CLASS = Preferences
PREFERENCES_DIALOG_CLASS = PreferencesDialog
def _setup(self):
self.directories = Directories()
@ -42,9 +43,43 @@ class DupeGuru(DupeGuruBase):
def _update_options(self):
DupeGuruBase._update_options(self)
self.model.options['min_match_percentage'] = self.prefs.filter_hardness
self.model.options['scan_type'] = self.prefs.scan_type
self.model.options['word_weighting'] = self.prefs.word_weighting
self.model.options['match_similar_words'] = self.prefs.match_similar
threshold = self.prefs.small_file_threshold if self.prefs.ignore_small_files else 0
self.model.options['size_threshold'] = threshold * 1024 # threshold is in KB. the scanner wants bytes
scanned_tags = set()
if self.prefs.scan_tag_track:
scanned_tags.add('track')
if self.prefs.scan_tag_artist:
scanned_tags.add('artist')
if self.prefs.scan_tag_album:
scanned_tags.add('album')
if self.prefs.scan_tag_title:
scanned_tags.add('title')
if self.prefs.scan_tag_genre:
scanned_tags.add('genre')
if self.prefs.scan_tag_year:
scanned_tags.add('year')
self.model.options['scanned_tags'] = scanned_tags
@property
def DETAILS_DIALOG_CLASS(self):
if self.model.app_mode == AppMode.Music:
return DetailsDialogMusic
else:
return DetailsDialogStandard
@property
def RESULT_MODEL_CLASS(self):
if self.model.app_mode == AppMode.Music:
return ResultsModelMusic
else:
return ResultsModelStandard
@property
def PREFERENCES_DIALOG_CLASS(self):
if self.model.app_mode == AppMode.Music:
return PreferencesDialogMusic
else:
return PreferencesDialogStandard

View File

@ -4,19 +4,21 @@
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from core.scanner import ScanType
from ..base.preferences import Preferences as PreferencesBase
class Preferences(PreferencesBase):
DEFAULT_SCAN_TYPE = ScanType.Contents
def _load_specific(self, settings):
get = self.get_value
self.word_weighting = get('WordWeighting', self.word_weighting)
self.match_similar = get('MatchSimilar', self.match_similar)
self.ignore_small_files = get('IgnoreSmallFiles', self.ignore_small_files)
self.small_file_threshold = get('SmallFileThreshold', self.small_file_threshold)
self.scan_tag_track = get('ScanTagTrack', self.scan_tag_track)
self.scan_tag_artist = get('ScanTagArtist', self.scan_tag_artist)
self.scan_tag_album = get('ScanTagAlbum', self.scan_tag_album)
self.scan_tag_title = get('ScanTagTitle', self.scan_tag_title)
self.scan_tag_genre = get('ScanTagGenre', self.scan_tag_genre)
self.scan_tag_year = get('ScanTagYear', self.scan_tag_year)
def _reset_specific(self):
self.filter_hardness = 80
@ -24,6 +26,12 @@ class Preferences(PreferencesBase):
self.match_similar = False
self.ignore_small_files = True
self.small_file_threshold = 10 # KB
self.scan_tag_track = False
self.scan_tag_artist = True
self.scan_tag_album = True
self.scan_tag_title = True
self.scan_tag_genre = False
self.scan_tag_year = False
def _save_specific(self, settings):
set_ = self.set_value
@ -31,4 +39,10 @@ class Preferences(PreferencesBase):
set_('MatchSimilar', self.match_similar)
set_('IgnoreSmallFiles', self.ignore_small_files)
set_('SmallFileThreshold', self.small_file_threshold)
set_('ScanTagTrack', self.scan_tag_track)
set_('ScanTagArtist', self.scan_tag_artist)
set_('ScanTagAlbum', self.scan_tag_album)
set_('ScanTagTitle', self.scan_tag_title)
set_('ScanTagGenre', self.scan_tag_genre)
set_('ScanTagYear', self.scan_tag_year)

View File

@ -13,6 +13,7 @@ from hscommon.plat import ISWINDOWS, ISLINUX
from hscommon.trans import trget
from hscommon.util import tryint
from core.app import AppMode
from core.scanner import ScanType
from ..base.preferences_dialog import PreferencesDialogBase
@ -81,7 +82,7 @@ class PreferencesDialog(PreferencesDialogBase):
self.sizeThresholdEdit.setText(str(prefs.small_file_threshold))
# Update UI state based on selected scan type
scan_type = prefs.scan_type
scan_type = prefs.get_scan_type(AppMode.Standard)
word_based = scan_type == ScanType.Filename
self.filterHardnessSlider.setEnabled(word_based)
self.matchSimilarBox.setEnabled(word_based)