From 75d6105f89915ef13643f255b28d747fbc31d98c Mon Sep 17 00:00:00 2001 From: Kameron Kenny <1267885+kkenny@users.noreply.github.com> Date: Wed, 18 Sep 2024 18:12:27 -0400 Subject: [PATCH] new stuff --- .gitignore | 3 + BitMover.ui | 397 +++++++++++++++++++++++++++++++++++------ BitMover_MainWindow.py | 237 +++++++++++++++++++----- BitMover_ui.py | 131 +++++++++----- _video.py | 44 +++++ assets/forklift.ico | Bin 0 -> 19862 bytes configure.py | 4 +- img_preview.py | 46 ++++- lumberjack.py | 7 +- media.py | 23 +-- raw_photo.py | 1 + requirements.txt | 11 ++ setup.py | 12 ++ 13 files changed, 739 insertions(+), 177 deletions(-) create mode 100644 _video.py create mode 100644 assets/forklift.ico create mode 100644 requirements.txt create mode 100644 setup.py diff --git a/.gitignore b/.gitignore index 8bc4d10..734759b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ *.swp *.orig +*.spec +build/ +dist/ config.yaml .DS_Store .idea diff --git a/BitMover.ui b/BitMover.ui index 2d449f5..8f0c3a4 100644 --- a/BitMover.ui +++ b/BitMover.ui @@ -20,15 +20,12 @@ 20 10 871 - 121 + 71 - - - - Source Directory - + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop @@ -41,23 +38,6 @@ - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - - - - - Destination Directory - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - @@ -72,31 +52,23 @@ - - - - - 0 - 0 - - - - - 16 - - + + - Scan Directory + Source Directory - - - .. + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - false + + + + + + Destination Directory - - false + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop @@ -106,7 +78,7 @@ 20 - 150 + 160 871 701 @@ -116,9 +88,9 @@ 910 - 580 - 551 - 251 + 650 + 311 + 211 @@ -136,6 +108,12 @@ + + + true + true + + Camera @@ -143,6 +121,12 @@ + + + true + true + + ISO @@ -157,6 +141,12 @@ + + + true + true + + Date / Time Created @@ -164,6 +154,12 @@ + + + true + true + + Lens @@ -171,6 +167,12 @@ + + + true + true + + Resolution (DPI) @@ -178,6 +180,12 @@ + + + true + true + + Aperture @@ -206,6 +214,12 @@ + + + true + true + + Megapixels @@ -220,6 +234,12 @@ + + + true + true + + Width / Height @@ -241,8 +261,14 @@ + + + true + true + + - Zoom + Focal Length @@ -266,7 +292,7 @@ 910 - 550 + 630 371 16 @@ -341,28 +367,28 @@ - + 24 - + - Current Progress + Import Progress - + - Overall Progress + Processing Progress - + 24 @@ -389,6 +415,265 @@ + + + + 910 + 530 + 541 + 91 + + + + + 0 + + + 0 + + + 0 + + + 20 + + + + + + 11 + + + + + + + + + + + + true + true + + + + Source Path + + + false + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + + true + true + + + + Destination Path + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + + 11 + + + + + + + + + + + + + 20 + 119 + 871 + 35 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + After copying, verify that the hash of the original file equals the hash of the copied file. + + + Validate Checksum + + + true + + + + + + + After copy, delete the copied files in the source directory. This automatically enables checksum validation and will not delete the source file if the file hashes do not match. + + + Cleanup Files + + + false + + + + + + + For images only, create a folder called "Originals" at the destination and place an additional copy of the image in it. This is useful for those who use editors that still do destructive editing. + + + Store Originals + + + false + + + + + + + false + + + Import Media + + + + + + + + + + + + 20 + 80 + 871 + 35 + + + + + + + Search For + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Images + + + + + + true + + + + + + + Video + + + + + + true + + + + + + + Audio + + + + + + true + + + + + + + + 0 + 0 + + + + + 16 + + + + Scan Directory + + + + .. + + + false + + + false + + + + + diff --git a/BitMover_MainWindow.py b/BitMover_MainWindow.py index 2f5f861..fef642e 100644 --- a/BitMover_MainWindow.py +++ b/BitMover_MainWindow.py @@ -8,6 +8,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets + class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName("MainWindow") @@ -15,26 +16,18 @@ class Ui_MainWindow(object): self.centralwidget = QtWidgets.QWidget(parent=MainWindow) self.centralwidget.setObjectName("centralwidget") self.gridLayoutWidget = QtWidgets.QWidget(parent=self.centralwidget) - self.gridLayoutWidget.setGeometry(QtCore.QRect(20, 10, 871, 121)) + self.gridLayoutWidget.setGeometry(QtCore.QRect(20, 10, 871, 71)) self.gridLayoutWidget.setObjectName("gridLayoutWidget") self.grid_dir_selector = QtWidgets.QGridLayout(self.gridLayoutWidget) self.grid_dir_selector.setContentsMargins(0, 0, 0, 0) self.grid_dir_selector.setObjectName("grid_dir_selector") - self.label_1_src_dir = QtWidgets.QLabel(parent=self.gridLayoutWidget) - self.label_1_src_dir.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeading|QtCore.Qt.AlignmentFlag.AlignLeft|QtCore.Qt.AlignmentFlag.AlignTop) - self.label_1_src_dir.setObjectName("label_1_src_dir") - self.grid_dir_selector.addWidget(self.label_1_src_dir, 1, 0, 1, 1) - self.pushButton_src_browse = QtWidgets.QPushButton(parent=self.gridLayoutWidget) - self.pushButton_src_browse.setObjectName("pushButton_src_browse") - self.grid_dir_selector.addWidget(self.pushButton_src_browse, 1, 2, 1, 1) self.lineEdit_dst_dir = QtWidgets.QLineEdit(parent=self.gridLayoutWidget) self.lineEdit_dst_dir.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeading|QtCore.Qt.AlignmentFlag.AlignLeft|QtCore.Qt.AlignmentFlag.AlignTop) self.lineEdit_dst_dir.setObjectName("lineEdit_dst_dir") self.grid_dir_selector.addWidget(self.lineEdit_dst_dir, 2, 1, 1, 1) - self.label_2_dst_dir = QtWidgets.QLabel(parent=self.gridLayoutWidget) - self.label_2_dst_dir.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeading|QtCore.Qt.AlignmentFlag.AlignLeft|QtCore.Qt.AlignmentFlag.AlignTop) - self.label_2_dst_dir.setObjectName("label_2_dst_dir") - self.grid_dir_selector.addWidget(self.label_2_dst_dir, 2, 0, 1, 1) + self.pushButton_src_browse = QtWidgets.QPushButton(parent=self.gridLayoutWidget) + self.pushButton_src_browse.setObjectName("pushButton_src_browse") + self.grid_dir_selector.addWidget(self.pushButton_src_browse, 1, 2, 1, 1) self.lineEdit_src_dir = QtWidgets.QLineEdit(parent=self.gridLayoutWidget) self.lineEdit_src_dir.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeading|QtCore.Qt.AlignmentFlag.AlignLeft|QtCore.Qt.AlignmentFlag.AlignTop) self.lineEdit_src_dir.setObjectName("lineEdit_src_dir") @@ -42,31 +35,36 @@ class Ui_MainWindow(object): self.pushButton_dst_browse = QtWidgets.QPushButton(parent=self.gridLayoutWidget) self.pushButton_dst_browse.setObjectName("pushButton_dst_browse") self.grid_dir_selector.addWidget(self.pushButton_dst_browse, 2, 2, 1, 1) - self.pushButton_3_scan_dir = QtWidgets.QPushButton(parent=self.gridLayoutWidget) - self.pushButton_3_scan_dir.setBaseSize(QtCore.QSize(0, 0)) - font = QtGui.QFont() - font.setPointSize(16) - self.pushButton_3_scan_dir.setFont(font) - icon = QtGui.QIcon.fromTheme("edit-find") - self.pushButton_3_scan_dir.setIcon(icon) - self.pushButton_3_scan_dir.setCheckable(False) - self.pushButton_3_scan_dir.setFlat(False) - self.pushButton_3_scan_dir.setObjectName("pushButton_3_scan_dir") - self.grid_dir_selector.addWidget(self.pushButton_3_scan_dir, 3, 0, 1, 1) + self.label_1_src_dir = QtWidgets.QLabel(parent=self.gridLayoutWidget) + self.label_1_src_dir.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeading|QtCore.Qt.AlignmentFlag.AlignLeft|QtCore.Qt.AlignmentFlag.AlignTop) + self.label_1_src_dir.setObjectName("label_1_src_dir") + self.grid_dir_selector.addWidget(self.label_1_src_dir, 1, 0, 1, 1) + self.label_2_dst_dir = QtWidgets.QLabel(parent=self.gridLayoutWidget) + self.label_2_dst_dir.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeading|QtCore.Qt.AlignmentFlag.AlignLeft|QtCore.Qt.AlignmentFlag.AlignTop) + self.label_2_dst_dir.setObjectName("label_2_dst_dir") + self.grid_dir_selector.addWidget(self.label_2_dst_dir, 2, 0, 1, 1) self.file_list = QtWidgets.QListWidget(parent=self.centralwidget) - self.file_list.setGeometry(QtCore.QRect(20, 150, 871, 701)) + self.file_list.setGeometry(QtCore.QRect(20, 160, 871, 701)) self.file_list.setObjectName("file_list") self.gridLayoutWidget_2 = QtWidgets.QWidget(parent=self.centralwidget) - self.gridLayoutWidget_2.setGeometry(QtCore.QRect(910, 580, 551, 251)) + self.gridLayoutWidget_2.setGeometry(QtCore.QRect(910, 650, 311, 211)) self.gridLayoutWidget_2.setObjectName("gridLayoutWidget_2") self.grid_metadata = QtWidgets.QGridLayout(self.gridLayoutWidget_2) self.grid_metadata.setContentsMargins(0, 0, 0, 0) self.grid_metadata.setHorizontalSpacing(20) self.grid_metadata.setObjectName("grid_metadata") self.l_camera = QtWidgets.QLabel(parent=self.gridLayoutWidget_2) + font = QtGui.QFont() + font.setBold(True) + font.setItalic(True) + self.l_camera.setFont(font) self.l_camera.setObjectName("l_camera") self.grid_metadata.addWidget(self.l_camera, 6, 0, 1, 1) self.l_iso = QtWidgets.QLabel(parent=self.gridLayoutWidget_2) + font = QtGui.QFont() + font.setBold(True) + font.setItalic(True) + self.l_iso.setFont(font) self.l_iso.setObjectName("l_iso") self.grid_metadata.addWidget(self.l_iso, 4, 0, 1, 1) self.label_data_width_height = QtWidgets.QLabel(parent=self.gridLayoutWidget_2) @@ -74,15 +72,31 @@ class Ui_MainWindow(object): self.label_data_width_height.setObjectName("label_data_width_height") self.grid_metadata.addWidget(self.label_data_width_height, 1, 1, 1, 1) self.l_date_time_created = QtWidgets.QLabel(parent=self.gridLayoutWidget_2) + font = QtGui.QFont() + font.setBold(True) + font.setItalic(True) + self.l_date_time_created.setFont(font) self.l_date_time_created.setObjectName("l_date_time_created") self.grid_metadata.addWidget(self.l_date_time_created, 0, 0, 1, 1) self.l_lens = QtWidgets.QLabel(parent=self.gridLayoutWidget_2) + font = QtGui.QFont() + font.setBold(True) + font.setItalic(True) + self.l_lens.setFont(font) self.l_lens.setObjectName("l_lens") self.grid_metadata.addWidget(self.l_lens, 7, 0, 1, 1) self.l_dpi = QtWidgets.QLabel(parent=self.gridLayoutWidget_2) + font = QtGui.QFont() + font.setBold(True) + font.setItalic(True) + self.l_dpi.setFont(font) self.l_dpi.setObjectName("l_dpi") self.grid_metadata.addWidget(self.l_dpi, 2, 0, 1, 1) self.l_aperture = QtWidgets.QLabel(parent=self.gridLayoutWidget_2) + font = QtGui.QFont() + font.setBold(True) + font.setItalic(True) + self.l_aperture.setFont(font) self.l_aperture.setObjectName("l_aperture") self.grid_metadata.addWidget(self.l_aperture, 5, 0, 1, 1) self.label_data_iso = QtWidgets.QLabel(parent=self.gridLayoutWidget_2) @@ -98,6 +112,10 @@ class Ui_MainWindow(object): self.label_data_lens.setObjectName("label_data_lens") self.grid_metadata.addWidget(self.label_data_lens, 7, 1, 1, 1) self.l_megapixels = QtWidgets.QLabel(parent=self.gridLayoutWidget_2) + font = QtGui.QFont() + font.setBold(True) + font.setItalic(True) + self.l_megapixels.setFont(font) self.l_megapixels.setObjectName("l_megapixels") self.grid_metadata.addWidget(self.l_megapixels, 3, 0, 1, 1) self.label_data_megapixels = QtWidgets.QLabel(parent=self.gridLayoutWidget_2) @@ -105,6 +123,10 @@ class Ui_MainWindow(object): self.label_data_megapixels.setObjectName("label_data_megapixels") self.grid_metadata.addWidget(self.label_data_megapixels, 3, 1, 1, 1) self.l_width_height = QtWidgets.QLabel(parent=self.gridLayoutWidget_2) + font = QtGui.QFont() + font.setBold(True) + font.setItalic(True) + self.l_width_height.setFont(font) self.l_width_height.setObjectName("l_width_height") self.grid_metadata.addWidget(self.l_width_height, 1, 0, 1, 1) self.label_data_date_time_created = QtWidgets.QLabel(parent=self.gridLayoutWidget_2) @@ -116,6 +138,10 @@ class Ui_MainWindow(object): self.label_data_camera.setObjectName("label_data_camera") self.grid_metadata.addWidget(self.label_data_camera, 6, 1, 1, 1) self.l_zoom = QtWidgets.QLabel(parent=self.gridLayoutWidget_2) + font = QtGui.QFont() + font.setBold(True) + font.setItalic(True) + self.l_zoom.setFont(font) self.l_zoom.setObjectName("l_zoom") self.grid_metadata.addWidget(self.l_zoom, 8, 0, 1, 1) self.label_data_zoom = QtWidgets.QLabel(parent=self.gridLayoutWidget_2) @@ -128,7 +154,7 @@ class Ui_MainWindow(object): self.grid_metadata.addWidget(self.label_data_dpi, 2, 1, 1, 1) self.grid_metadata.setColumnStretch(1, 1) self.l_exif_ffprobe_title = QtWidgets.QLabel(parent=self.centralwidget) - self.l_exif_ffprobe_title.setGeometry(QtCore.QRect(910, 550, 371, 16)) + self.l_exif_ffprobe_title.setGeometry(QtCore.QRect(910, 630, 371, 16)) font = QtGui.QFont() font.setPointSize(18) font.setBold(True) @@ -168,26 +194,132 @@ class Ui_MainWindow(object): self.gridLayout_3 = QtWidgets.QGridLayout(self.gridLayoutWidget_5) self.gridLayout_3.setContentsMargins(0, 0, 0, 0) self.gridLayout_3.setObjectName("gridLayout_3") - self.progressBar_overall = QtWidgets.QProgressBar(parent=self.gridLayoutWidget_5) - self.progressBar_overall.setProperty("value", 0) - self.progressBar_overall.setObjectName("progressBar_overall") - self.gridLayout_3.addWidget(self.progressBar_overall, 0, 1, 1, 1) - self.label_2 = QtWidgets.QLabel(parent=self.gridLayoutWidget_5) - self.label_2.setObjectName("label_2") - self.gridLayout_3.addWidget(self.label_2, 1, 0, 1, 1) - self.label = QtWidgets.QLabel(parent=self.gridLayoutWidget_5) - self.label.setObjectName("label") - self.gridLayout_3.addWidget(self.label, 0, 0, 1, 1) - self.progressBar_current = QtWidgets.QProgressBar(parent=self.gridLayoutWidget_5) - self.progressBar_current.setProperty("value", 0) - self.progressBar_current.setObjectName("progressBar_current") - self.gridLayout_3.addWidget(self.progressBar_current, 1, 1, 1, 1) + self.progressBar_processing = QtWidgets.QProgressBar(parent=self.gridLayoutWidget_5) + self.progressBar_processing.setProperty("value", 24) + self.progressBar_processing.setObjectName("progressBar_processing") + self.gridLayout_3.addWidget(self.progressBar_processing, 0, 1, 1, 1) + self.l_import_progress = QtWidgets.QLabel(parent=self.gridLayoutWidget_5) + self.l_import_progress.setObjectName("l_import_progress") + self.gridLayout_3.addWidget(self.l_import_progress, 1, 0, 1, 1) + self.l_proecessing_progress = QtWidgets.QLabel(parent=self.gridLayoutWidget_5) + self.l_proecessing_progress.setObjectName("l_proecessing_progress") + self.gridLayout_3.addWidget(self.l_proecessing_progress, 0, 0, 1, 1) + self.progressBar_importing = QtWidgets.QProgressBar(parent=self.gridLayoutWidget_5) + self.progressBar_importing.setProperty("value", 24) + self.progressBar_importing.setObjectName("progressBar_importing") + self.gridLayout_3.addWidget(self.progressBar_importing, 1, 1, 1, 1) self.img_preview = QtWidgets.QLabel(parent=self.centralwidget) self.img_preview.setGeometry(QtCore.QRect(910, 150, 541, 371)) self.img_preview.setAutoFillBackground(True) self.img_preview.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) self.img_preview.setText("") self.img_preview.setObjectName("img_preview") + self.gridLayoutWidget_6 = QtWidgets.QWidget(parent=self.centralwidget) + self.gridLayoutWidget_6.setGeometry(QtCore.QRect(910, 530, 541, 91)) + self.gridLayoutWidget_6.setObjectName("gridLayoutWidget_6") + self.grid_metadata_2 = QtWidgets.QGridLayout(self.gridLayoutWidget_6) + self.grid_metadata_2.setContentsMargins(0, 0, 0, 0) + self.grid_metadata_2.setHorizontalSpacing(20) + self.grid_metadata_2.setObjectName("grid_metadata_2") + self.l_data_file_source_path = QtWidgets.QLabel(parent=self.gridLayoutWidget_6) + font = QtGui.QFont() + font.setPointSize(11) + self.l_data_file_source_path.setFont(font) + self.l_data_file_source_path.setText("") + self.l_data_file_source_path.setObjectName("l_data_file_source_path") + self.grid_metadata_2.addWidget(self.l_data_file_source_path, 1, 0, 1, 1) + self.l_file_source_path = QtWidgets.QLabel(parent=self.gridLayoutWidget_6) + font = QtGui.QFont() + font.setBold(True) + font.setItalic(True) + self.l_file_source_path.setFont(font) + self.l_file_source_path.setScaledContents(False) + self.l_file_source_path.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeading|QtCore.Qt.AlignmentFlag.AlignLeft|QtCore.Qt.AlignmentFlag.AlignTop) + self.l_file_source_path.setObjectName("l_file_source_path") + self.grid_metadata_2.addWidget(self.l_file_source_path, 0, 0, 1, 1) + self.l_file_dest_path = QtWidgets.QLabel(parent=self.gridLayoutWidget_6) + font = QtGui.QFont() + font.setBold(True) + font.setItalic(True) + self.l_file_dest_path.setFont(font) + self.l_file_dest_path.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeading|QtCore.Qt.AlignmentFlag.AlignLeft|QtCore.Qt.AlignmentFlag.AlignTop) + self.l_file_dest_path.setObjectName("l_file_dest_path") + self.grid_metadata_2.addWidget(self.l_file_dest_path, 2, 0, 1, 1) + self.l_data_file_dest_path = QtWidgets.QLabel(parent=self.gridLayoutWidget_6) + font = QtGui.QFont() + font.setPointSize(11) + self.l_data_file_dest_path.setFont(font) + self.l_data_file_dest_path.setText("") + self.l_data_file_dest_path.setObjectName("l_data_file_dest_path") + self.grid_metadata_2.addWidget(self.l_data_file_dest_path, 3, 0, 1, 1) + self.grid_metadata_2.setRowStretch(1, 1) + self.grid_metadata_2.setRowStretch(3, 1) + self.horizontalLayoutWidget_2 = QtWidgets.QWidget(parent=self.centralwidget) + self.horizontalLayoutWidget_2.setGeometry(QtCore.QRect(20, 119, 871, 35)) + self.horizontalLayoutWidget_2.setObjectName("horizontalLayoutWidget_2") + self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget_2) + self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.horizontalLayout_2.addItem(spacerItem) + self.checkBox_verify_checksum = QtWidgets.QCheckBox(parent=self.horizontalLayoutWidget_2) + self.checkBox_verify_checksum.setChecked(True) + self.checkBox_verify_checksum.setObjectName("checkBox_verify_checksum") + self.horizontalLayout_2.addWidget(self.checkBox_verify_checksum) + self.checkBox_cleanup_files = QtWidgets.QCheckBox(parent=self.horizontalLayoutWidget_2) + self.checkBox_cleanup_files.setChecked(False) + self.checkBox_cleanup_files.setObjectName("checkBox_cleanup_files") + self.horizontalLayout_2.addWidget(self.checkBox_cleanup_files) + self.checkBox_store_originals = QtWidgets.QCheckBox(parent=self.horizontalLayoutWidget_2) + self.checkBox_store_originals.setChecked(False) + self.checkBox_store_originals.setObjectName("checkBox_store_originals") + self.horizontalLayout_2.addWidget(self.checkBox_store_originals) + self.pushButton_import = QtWidgets.QPushButton(parent=self.horizontalLayoutWidget_2) + self.pushButton_import.setEnabled(False) + icon = QtGui.QIcon.fromTheme("drive-harddisk") + self.pushButton_import.setIcon(icon) + self.pushButton_import.setObjectName("pushButton_import") + self.horizontalLayout_2.addWidget(self.pushButton_import) + self.horizontalLayoutWidget_3 = QtWidgets.QWidget(parent=self.centralwidget) + self.horizontalLayoutWidget_3.setGeometry(QtCore.QRect(20, 80, 871, 35)) + self.horizontalLayoutWidget_3.setObjectName("horizontalLayoutWidget_3") + self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget_3) + self.horizontalLayout_3.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.label = QtWidgets.QLabel(parent=self.horizontalLayoutWidget_3) + self.label.setObjectName("label") + self.horizontalLayout_3.addWidget(self.label) + spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.horizontalLayout_3.addItem(spacerItem1) + self.checkBox_search_for_images = QtWidgets.QCheckBox(parent=self.horizontalLayoutWidget_3) + icon = QtGui.QIcon.fromTheme("camera-photo") + self.checkBox_search_for_images.setIcon(icon) + self.checkBox_search_for_images.setChecked(True) + self.checkBox_search_for_images.setObjectName("checkBox_search_for_images") + self.horizontalLayout_3.addWidget(self.checkBox_search_for_images) + self.checkBox_search_for_video = QtWidgets.QCheckBox(parent=self.horizontalLayoutWidget_3) + icon = QtGui.QIcon.fromTheme("camera-video") + self.checkBox_search_for_video.setIcon(icon) + self.checkBox_search_for_video.setChecked(True) + self.checkBox_search_for_video.setObjectName("checkBox_search_for_video") + self.horizontalLayout_3.addWidget(self.checkBox_search_for_video) + self.checkBox_search_for_audio = QtWidgets.QCheckBox(parent=self.horizontalLayoutWidget_3) + icon = QtGui.QIcon.fromTheme("multimedia-player") + self.checkBox_search_for_audio.setIcon(icon) + self.checkBox_search_for_audio.setChecked(True) + self.checkBox_search_for_audio.setObjectName("checkBox_search_for_audio") + self.horizontalLayout_3.addWidget(self.checkBox_search_for_audio) + self.pushButton_3_scan_dir = QtWidgets.QPushButton(parent=self.horizontalLayoutWidget_3) + self.pushButton_3_scan_dir.setBaseSize(QtCore.QSize(0, 0)) + font = QtGui.QFont() + font.setPointSize(16) + self.pushButton_3_scan_dir.setFont(font) + icon = QtGui.QIcon.fromTheme("edit-find") + self.pushButton_3_scan_dir.setIcon(icon) + self.pushButton_3_scan_dir.setCheckable(False) + self.pushButton_3_scan_dir.setFlat(False) + self.pushButton_3_scan_dir.setObjectName("pushButton_3_scan_dir") + self.horizontalLayout_3.addWidget(self.pushButton_3_scan_dir) MainWindow.setCentralWidget(self.centralwidget) self.menubar = QtWidgets.QMenuBar(parent=MainWindow) self.menubar.setGeometry(QtCore.QRect(0, 0, 1473, 24)) @@ -208,11 +340,10 @@ class Ui_MainWindow(object): def retranslateUi(self, MainWindow): _translate = QtCore.QCoreApplication.translate MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) - self.label_1_src_dir.setText(_translate("MainWindow", "Source Directory")) self.pushButton_src_browse.setText(_translate("MainWindow", "Browse")) - self.label_2_dst_dir.setText(_translate("MainWindow", "Destination Directory")) self.pushButton_dst_browse.setText(_translate("MainWindow", "Browse")) - self.pushButton_3_scan_dir.setText(_translate("MainWindow", "Scan Directory")) + self.label_1_src_dir.setText(_translate("MainWindow", "Source Directory")) + self.label_2_dst_dir.setText(_translate("MainWindow", "Destination Directory")) self.l_camera.setText(_translate("MainWindow", "Camera")) self.l_iso.setText(_translate("MainWindow", "ISO")) self.l_date_time_created.setText(_translate("MainWindow", "Date / Time Created")) @@ -221,11 +352,25 @@ class Ui_MainWindow(object): self.l_aperture.setText(_translate("MainWindow", "Aperture")) self.l_megapixels.setText(_translate("MainWindow", "Megapixels")) self.l_width_height.setText(_translate("MainWindow", "Width / Height")) - self.l_zoom.setText(_translate("MainWindow", "Zoom")) + self.l_zoom.setText(_translate("MainWindow", "Focal Length")) self.l_exif_ffprobe_title.setText(_translate("MainWindow", "Exif / ffprobe Data")) self.labelEvent.setText(_translate("MainWindow", "Event Label")) self.label_3.setText(_translate("MainWindow", "Files Found")) - self.label_2.setText(_translate("MainWindow", "Current Progress")) - self.label.setText(_translate("MainWindow", "Overall Progress")) + self.l_import_progress.setText(_translate("MainWindow", "Import Progress")) + self.l_proecessing_progress.setText(_translate("MainWindow", "Processing Progress")) + self.l_file_source_path.setText(_translate("MainWindow", "Source Path")) + self.l_file_dest_path.setText(_translate("MainWindow", "Destination Path")) + self.checkBox_verify_checksum.setToolTip(_translate("MainWindow", "After copying, verify that the hash of the original file equals the hash of the copied file.")) + self.checkBox_verify_checksum.setText(_translate("MainWindow", "Validate Checksum")) + self.checkBox_cleanup_files.setToolTip(_translate("MainWindow", "After copy, delete the copied files in the source directory. This automatically enables checksum validation and will not delete the source file if the file hashes do not match.")) + self.checkBox_cleanup_files.setText(_translate("MainWindow", "Cleanup Files")) + self.checkBox_store_originals.setToolTip(_translate("MainWindow", "For images only, create a folder called \"Originals\" at the destination and place an additional copy of the image in it. This is useful for those who use editors that still do destructive editing.")) + self.checkBox_store_originals.setText(_translate("MainWindow", "Store Originals")) + self.pushButton_import.setText(_translate("MainWindow", "Import Media")) + self.label.setText(_translate("MainWindow", "Search For")) + self.checkBox_search_for_images.setText(_translate("MainWindow", "Images")) + self.checkBox_search_for_video.setText(_translate("MainWindow", "Video")) + self.checkBox_search_for_audio.setText(_translate("MainWindow", "Audio")) + self.pushButton_3_scan_dir.setText(_translate("MainWindow", "Scan Directory")) self.menuBit_Mover.setTitle(_translate("MainWindow", "Bit Mover")) self.actionScan_Directory.setText(_translate("MainWindow", "Scan Directory")) diff --git a/BitMover_ui.py b/BitMover_ui.py index 51b16f6..45ebdba 100755 --- a/BitMover_ui.py +++ b/BitMover_ui.py @@ -11,11 +11,11 @@ from file_stuff import is_file from BitMover_MainWindow import Ui_MainWindow from media import Media from lumberjack import timber -from raw_photo import extract_jpg_thumb from thread_my_stuff import Worker from img_preview import ImgPreview log = timber(__name__) +basedir = os.path.dirname(__file__) # Subclass QMainWindow to customize your application's main window class MainWindow(QMainWindow, Ui_MainWindow): @@ -24,7 +24,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.setupUi(self) self.setWindowTitle("BitMover") - self.setWindowIcon(QIcon('assets/forklift.png')) + self.setWindowIcon(QIcon(os.path.join(basedir,'assets', 'forklift.ico'))) c = Configure(CONFIG_FILE) self.config = c.load_config() @@ -42,8 +42,11 @@ class MainWindow(QMainWindow, Ui_MainWindow): # Initialize widgets self.lcd_files_found.display(int(0)) - self.set_progress(0, 0) - self.img_preview.setPixmap(QPixmap('assets/preview_placeholder.jpg')) + self.set_progress_processing(0) + self.set_progress_importing(0) + self.img_preview.setPixmap(QPixmap(os.path.join(basedir, + 'assets', + 'preview_placeholder.jpg'))) self.img_preview.setScaledContents(True) self.file_list.currentItemChanged.connect(self.index_changed) @@ -59,10 +62,19 @@ class MainWindow(QMainWindow, Ui_MainWindow): def toggle_scan_button(self,enable=True): self.pushButton_3_scan_dir.setEnabled(enable) - def index_changed(self,i): - preview = ImgPreview(file=i.text(),event=self.get_event(),config=self.config) + def update_preview(self,i): + preview = ImgPreview(file=i.text(), event=self.get_event(), config=self.config) self.label_data_date_time_created.setText(preview.dtc) + path_hash = preview.path_hash + + self.l_data_file_source_path.setText( + self.files[path_hash]['folders']['source_path']) + self.l_data_file_dest_path.setText( + self.files[path_hash]['folders']['destination']) + + self.img_preview.setPixmap(QPixmap(preview.thumbnail)) + self.img_preview.setFixedHeight(self.img_preview.width() / preview.thumbnail_ratio) if preview.file_type == 'image': self.label_data_width_height.setText(str(preview.size)) @@ -73,15 +85,14 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.label_data_camera.setText(str(preview.camera)) self.label_data_aperture.setText(str(preview.aperture)) self.label_data_megapixels.setText(str(preview.mpixels)) - # - # h = self.img_preview.width * (preview.height / preview.width) - # self.img_preview.setFixedHeight(h) - if preview.is_jpg: - self.img_preview.setPixmap(QPixmap(preview.file)) - else: - jpg = extract_jpg_thumb(preview.file) - self.img_preview.setPixmap(QPixmap(jpg)) + def index_changed(self,i): + if i is None: + self.img_preview.setPixmap(QPixmap(os.path.join(basedir, + 'assets', + 'preview_placeholder.jpg'))) + else: + self.update_preview(i) def select_src_directory(self): directory = QFileDialog.getExistingDirectory(self, @@ -107,25 +118,17 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.total_files = t self.lcd_files_found.display(self.total_files) - def progress_fn(self,n): + def set_progress_processing(self, n): # print("%d%% done" % n) - self.progressBar_overall.setValue(int(n)) + self.progressBar_processing.setValue(int(n)) - def set_progress(self,p,t): - """ - set progress for bar, - p = progress counter - t = target total - """ - if int(t) == 0: - t += 1 + def set_progress_importing(self, n): + # print("%d%% done" % n) + self.progressBar_importing.setValue(int(n)) - percent_complete = (int(p) / int(t)) * 100 - self.progressBar_overall.setValue(int(percent_complete)) - - def get_t_files(self): + def get_t_files(self,search_types): for folder, subfolders, filename in os.walk(self.src_dir): - for f_type in self.file_types: + for f_type in search_types: for ext in self.file_types[f_type]: for file in filename: if file.lower().endswith(ext): @@ -137,25 +140,39 @@ class MainWindow(QMainWindow, Ui_MainWindow): def t_find_files(self,progress_callback): file_count = int(0) - self.get_t_files() + + + search_types = [] + + if self.checkBox_search_for_images.isChecked(): + search_types.append('image') + if self.checkBox_search_for_video.isChecked(): + search_types.append('video') + if self.checkBox_search_for_audio.isChecked(): + search_types.append('audio') + + self.get_t_files(search_types) self.set_total_files(self.file_total) - for folder, subfolders, filename in os.walk(self.src_dir): - for f_type in self.file_types: - for ext in self.file_types[f_type]: - for file in filename: - if file.lower().endswith(ext): - current_file = os.path.join(folder, file) - if is_file(current_file): - file_count += int(1) - self.process_file(current_file) - # self.set_progress(file_count,file_total) - # time.sleep(.02) + if len(search_types) > 0: + for folder, subfolders, filename in os.walk(self.src_dir): + for f_type in search_types: + for ext in self.file_types[f_type]: + for file in filename: + if file.lower().endswith(ext): + current_file = os.path.join(folder, file) + if is_file(current_file): + file_count += int(1) + self.process_file(current_file) - else: - print(f"Skipping {current_file} as it does not look like a real file.") - progress_callback.emit((file_count / self.file_total) * 100) + else: + print(f"Skipping {current_file} as it does not look like a real file.") + + progress_callback.emit(round((file_count / self.file_total) * 100, 0)) + else: + print("Nothing to search for.") + return "Done." @staticmethod @@ -176,12 +193,27 @@ class MainWindow(QMainWindow, Ui_MainWindow): def find_files(self): """ find files to build a dictionary out of """ + + # Initialize widgets + self.lcd_files_found.display(int(0)) + self.set_progress_processing(0) + self.set_progress_importing(0) + self.img_preview.setPixmap(QPixmap(os.path.join(basedir, + 'assets', + 'preview_placeholder.jpg'))) + self.file_list.clear() + + # File Stuff + self.total_files = 0 + self.file_total = 0 + self.files = {} + worker = Worker(self.t_find_files) worker.signals.started.connect(self.scan_thread_started) worker.signals.result.connect(self.print_output) worker.signals.finished.connect(self.thread_complete) worker.signals.finished.connect(self.scan_thread_done) - worker.signals.progress.connect(self.progress_fn) + worker.signals.progress.connect(self.set_progress_processing) # Execute. self.threadpool.start(worker) @@ -203,9 +235,13 @@ class MainWindow(QMainWindow, Ui_MainWindow): m = Media(os.path.join(path_name,f_name),event, c) i = m.source_path_hash - log.debug(f'Source Path Hash: {i}') + # log.debug(f'Source Path Hash: {i}') - self.files[i] = { 'folders': {}, 'date': {} } + self.files[i] = { + 'folders': {}, + 'date': {}, + 'event': {} + } self.files[i]['folders']['source_path'] = m.source_path_dir self.files[i]['type'] = m.file_type @@ -217,6 +253,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.files[i]['date']['capture_date']['d'] = m.capture_date[2] self.files[i]['folders']['destination'] = m.destination_path self.files[i]['folders']['destination_original'] = m.destination_originals_path + self.files[i]['event']['name'] = m.event_name self.file_list.addItem(f"{self.files[i]['folders']['source_path']}/{self.files[i]['name']}") app = QApplication(sys.argv) diff --git a/_video.py b/_video.py new file mode 100644 index 0000000..9c57587 --- /dev/null +++ b/_video.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python + +import sys +import ffmpeg + +class Video: + def __init__(self,max_width=1024,*args,**kwargs): + super(Video,self).__init__() + self.args = args + self.kwargs = kwargs + self.file = kwargs['file'] + self.out = 'thumbnail.jpg' + self.max_width = max_width + + def gen_video_thumbnail(self): + """ + Generate a thumbnail from a video + """ + + probe = ffmpeg.probe(self.file) + time = float(probe['streams'][0]['duration']) // 5 + v_width = probe['streams'][0]['width'] + width = self.set_width(v_width) + + try: + ( + ffmpeg.input(self.file, ss=time) + .filter('scale', width, -1) + .output(self.out, vframes=1) + .overwrite_output() + .run(capture_stdout=True, capture_stderr=True) + ) + except ffmpeg.Error as e: + print(e.stderr.decode(), file=sys.stderr) + + return self.out + + def set_width(self,v_width): + if v_width > self.max_width: + width = self.max_width + else: + width = v_width + + return width \ No newline at end of file diff --git a/assets/forklift.ico b/assets/forklift.ico new file mode 100644 index 0000000000000000000000000000000000000000..958e81d7db423c706c694f622bd8aa8a2b0d170b GIT binary patch literal 19862 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 ifp-Rf<|lz2#s3fJH&A<517HvU0000