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 0000000..958e81d Binary files /dev/null and b/assets/forklift.ico differ diff --git a/configure.py b/configure.py index c83439f..6060819 100644 --- a/configure.py +++ b/configure.py @@ -4,11 +4,13 @@ Load the config file from yaml file """ import sys +import os import yaml from lumberjack import timber +basedir = os.path.dirname(__file__) files = {} -CONFIG_FILE = 'config.yaml' +CONFIG_FILE = os.path.join(basedir, 'config.yaml') log = timber(__name__) class Configure: diff --git a/img_preview.py b/img_preview.py index 891a80b..6e17cfb 100644 --- a/img_preview.py +++ b/img_preview.py @@ -3,7 +3,8 @@ from media import Media from get_image_tag import get_exif_tag from PIL import Image -from raw_photo import get_raw_image_dimensions +from raw_photo import get_raw_image_dimensions, extract_jpg_thumb +from _video import Video class ImgPreview: def __init__(self,*args,**kwargs): @@ -19,6 +20,13 @@ class ImgPreview: self.file_type = self.m.file_type self.is_jpg = False self.is_raw = False + self.path_hash = self.m.source_path_hash + self.dtc = f'{self.m.capture_date[0]}/{self.m.capture_date[1]}/{self.m.capture_date[2]}' + self.thumbnail = 'thumbnail.jpg' + self.ratio = None + self.thumbnail_width = None + self.thumbnail_height = None + self.thumbnail_ratio = None if self.file_type == 'image': self._img_preview() @@ -31,8 +39,12 @@ class ImgPreview: print(f'aperture: {self.aperture}') print(f'mpixels: {self.mpixels}') + if self.file_type == 'video': + self._video_preview() + + self.thumb_ratio() + def _img_preview(self): - self.dtc = f'{self.m.capture_date[0]}/{self.m.capture_date[1]}/{self.m.capture_date[2]}' self.dpi = get_exif_tag(self.file, "xresolution") self.iso = get_exif_tag(self.file, 'iso') self.aperture = get_exif_tag(self.file, 'fnumber') @@ -55,13 +67,41 @@ class ImgPreview: else: self.mpixels = 'Unknown :(' + + def _jpg_preview(self): self.is_jpg = True img = Image.open(self.file) self.width = img.width self.height = img.height + self.gen_thumb_from_jpg() + + def gen_thumb_from_jpg(self): + """Generates a thumbnail image from the given input image.""" + thumb_width = 500 + thumb_size = (thumb_width, int(thumb_width // 1.5)) + try: + with Image.open(self.file) as img: + img.thumbnail(thumb_size) + img.save(self.thumbnail, "JPEG") + except IOError: + print(f"Error: Cannot create thumbnail for '{self.file}'") def _raw_preview(self): self.is_raw = True self.width = get_raw_image_dimensions(self.file)[1] - self.height = get_raw_image_dimensions(self.file)[0] \ No newline at end of file + self.height = get_raw_image_dimensions(self.file)[0] + self.thumbnail = extract_jpg_thumb(self.file) + + def _video_preview(self): + vid = Video(file=self.file) + self.thumbnail = vid.gen_video_thumbnail() + + def thumb_ratio(self): + img = Image.open(self.thumbnail) + self.thumbnail_width = img.width + self.thumbnail_height = img.height + self.thumbnail_ratio = float(self.thumbnail_width / self.thumbnail_height) + print(self.thumbnail_width) + print(self.thumbnail_height) + print(self.thumbnail_ratio) \ No newline at end of file diff --git a/lumberjack.py b/lumberjack.py index 1b72ca5..c38d5ba 100644 --- a/lumberjack.py +++ b/lumberjack.py @@ -4,8 +4,11 @@ A class for logging... no, not timber """ -import logging +import logging +import os + +basedir = os.path.dirname(__file__) # class Logger(object): # level_relations = { # 'debug': logging.DEBUG, @@ -29,7 +32,7 @@ import logging def timber(name): file_formatter = logging.Formatter('%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s') - file_handler = logging.FileHandler("log/all.log") + file_handler = logging.FileHandler(os.path.join(basedir, "log", "all.log")) file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(file_formatter) diff --git a/media.py b/media.py index f70272c..7cbb245 100644 --- a/media.py +++ b/media.py @@ -146,25 +146,4 @@ def get_capture_date(path, f_type): year = stamp.strftime("%Y") month = stamp.strftime("%m") day = stamp.strftime("%d") - return year, month, day - -# def process_file(path_name, f_name, event, c): -# """ gather information and add to dictionary """ -# log.debug(f'process_file({path_name}, {f_name}, {event}, {c})') -# -# m = Media(os.path.join(path_name,f_name),event, c) -# i = m.source_path_hash -# log.debug(f'Source Path Hash: {i}') -# -# files[i] = { 'folders': {}, 'date': {} } -# -# files[i]['folders']['source_path'] = m.source_path_dir -# files[i]['type'] = m.file_type -# files[i]['name'] = m.file_name -# files[i]['extension'] = m.file_ext -# files[i]['date']['capture_date'] = {} -# files[i]['date']['capture_date']['y'] = m.capture_date[0] -# files[i]['date']['capture_date']['m'] = m.capture_date[1] -# files[i]['date']['capture_date']['d'] = m.capture_date[2] -# files[i]['folders']['destination'] = m.destination_path -# files[i]['folders']['destination_original'] = m.destination_originals_path + return year, month, day \ No newline at end of file diff --git a/raw_photo.py b/raw_photo.py index 5e29b9a..4e436c0 100644 --- a/raw_photo.py +++ b/raw_photo.py @@ -14,6 +14,7 @@ def extract_jpg_thumb(raw_file_path): if thumb.format == ThumbFormat.JPEG: with open('thumbnail.jpg', 'wb') as f: f.write(thumb.data) + f.close() elif thumb.format == ThumbFormat.BITMAP: imageio.imsave('thumbnail.jpg', thumb.data) else: diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..041c0bd --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +ffmpeg-python~=0.2.0 +setuptools~=56.0.0 +xxhash~=3.5.0 +tqdm~=4.66.5 +PyYAML~=6.0.2 +imageio~=2.35.1 +rawpy~=0.21.0 +PyQt6~=6.7.1 +PyQt6-sip~=13.8.0 +pillow~=10.4.0 +ExifRead~=3.0.0 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..42dccb9 --- /dev/null +++ b/setup.py @@ -0,0 +1,12 @@ +from setuptools import setup + +setup( + name='Media Utils', + version='0.0.1', + packages=[''], + url='thelinux.pro', + license='', + author='Kameron Kenny', + author_email='kkenny379@gmail.com', + description='A utility to manage multimedia files' +)