diff --git a/.gitignore b/.gitignore
index 46ecd9e..8bc4d10 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,9 @@
*.swp
*.orig
config.yaml
+.DS_Store
+.idea
+__pycache__
+log
+thumbnail.jpg
+files_dict.yaml
diff --git a/BitMover.ui b/BitMover.ui
new file mode 100644
index 0000000..2d449f5
--- /dev/null
+++ b/BitMover.ui
@@ -0,0 +1,418 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 1473
+ 928
+
+
+
+ MainWindow
+
+
+
+
+
+ 20
+ 10
+ 871
+ 121
+
+
+
+ -
+
+
+ Source Directory
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
+
+
+
+ -
+
+
+ Browse
+
+
+
+ -
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
+
+
+
+ -
+
+
+ Destination Directory
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
+
+
+
+ -
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
+
+
+
+ -
+
+
+ Browse
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 16
+
+
+
+ Scan Directory
+
+
+
+ ..
+
+
+ false
+
+
+ false
+
+
+
+
+
+
+
+
+ 20
+ 150
+ 871
+ 701
+
+
+
+
+
+
+ 910
+ 580
+ 551
+ 251
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 20
+
+ -
+
+
+ Camera
+
+
+
+ -
+
+
+ ISO
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+ Date / Time Created
+
+
+
+ -
+
+
+ Lens
+
+
+
+ -
+
+
+ Resolution (DPI)
+
+
+
+ -
+
+
+ Aperture
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+ Megapixels
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+ Width / Height
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+ Zoom
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+ 910
+ 550
+ 371
+ 16
+
+
+
+
+ 18
+ true
+
+
+
+ Exif / ffprobe Data
+
+
+
+
+
+ 910
+ 10
+ 541
+ 71
+
+
+
+ -
+
+
+ -
+
+
+ Event Label
+
+
+
+
+
+
+
+
+ 910
+ 90
+ 221
+ 41
+
+
+
+ -
+
+
+
+ 18
+
+
+
+ Files Found
+
+
+
+ -
+
+
+
+
+
+
+
+ 1140
+ 90
+ 311
+ 46
+
+
+
+ -
+
+
+ 24
+
+
+
+ -
+
+
+ Current Progress
+
+
+
+ -
+
+
+ Overall Progress
+
+
+
+ -
+
+
+ 24
+
+
+
+
+
+
+
+
+ 910
+ 150
+ 541
+ 371
+
+
+
+ true
+
+
+ QFrame::StyledPanel
+
+
+
+
+
+
+
+
+
+
+ Scan Directory
+
+
+
+
+
+
diff --git a/BitMover_MainWindow.py b/BitMover_MainWindow.py
new file mode 100644
index 0000000..2f5f861
--- /dev/null
+++ b/BitMover_MainWindow.py
@@ -0,0 +1,231 @@
+# Form implementation generated from reading ui file 'BitMover.ui'
+#
+# Created by: PyQt6 UI code generator 6.4.2
+#
+# WARNING: Any manual changes made to this file will be lost when pyuic6 is
+# run again. Do not edit this file unless you know what you are doing.
+
+
+from PyQt6 import QtCore, QtGui, QtWidgets
+
+class Ui_MainWindow(object):
+ def setupUi(self, MainWindow):
+ MainWindow.setObjectName("MainWindow")
+ MainWindow.resize(1473, 928)
+ 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.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.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")
+ self.grid_dir_selector.addWidget(self.lineEdit_src_dir, 1, 1, 1, 1)
+ 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.file_list = QtWidgets.QListWidget(parent=self.centralwidget)
+ self.file_list.setGeometry(QtCore.QRect(20, 150, 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.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)
+ 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)
+ 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)
+ self.label_data_width_height.setText("")
+ 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)
+ 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)
+ 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)
+ 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)
+ 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)
+ self.label_data_iso.setText("")
+ self.label_data_iso.setObjectName("label_data_iso")
+ self.grid_metadata.addWidget(self.label_data_iso, 4, 1, 1, 1)
+ self.label_data_aperture = QtWidgets.QLabel(parent=self.gridLayoutWidget_2)
+ self.label_data_aperture.setText("")
+ self.label_data_aperture.setObjectName("label_data_aperture")
+ self.grid_metadata.addWidget(self.label_data_aperture, 5, 1, 1, 1)
+ self.label_data_lens = QtWidgets.QLabel(parent=self.gridLayoutWidget_2)
+ self.label_data_lens.setText("")
+ 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)
+ 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)
+ self.label_data_megapixels.setText("")
+ 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)
+ 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)
+ self.label_data_date_time_created.setText("")
+ self.label_data_date_time_created.setObjectName("label_data_date_time_created")
+ self.grid_metadata.addWidget(self.label_data_date_time_created, 0, 1, 1, 1)
+ self.label_data_camera = QtWidgets.QLabel(parent=self.gridLayoutWidget_2)
+ self.label_data_camera.setText("")
+ 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)
+ 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)
+ self.label_data_zoom.setText("")
+ self.label_data_zoom.setObjectName("label_data_zoom")
+ self.grid_metadata.addWidget(self.label_data_zoom, 8, 1, 1, 1)
+ self.label_data_dpi = QtWidgets.QLabel(parent=self.gridLayoutWidget_2)
+ self.label_data_dpi.setText("")
+ self.label_data_dpi.setObjectName("label_data_dpi")
+ 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))
+ font = QtGui.QFont()
+ font.setPointSize(18)
+ font.setBold(True)
+ self.l_exif_ffprobe_title.setFont(font)
+ self.l_exif_ffprobe_title.setObjectName("l_exif_ffprobe_title")
+ self.gridLayoutWidget_3 = QtWidgets.QWidget(parent=self.centralwidget)
+ self.gridLayoutWidget_3.setGeometry(QtCore.QRect(910, 10, 541, 71))
+ self.gridLayoutWidget_3.setObjectName("gridLayoutWidget_3")
+ self.gridLayout = QtWidgets.QGridLayout(self.gridLayoutWidget_3)
+ self.gridLayout.setContentsMargins(0, 0, 0, 0)
+ self.gridLayout.setObjectName("gridLayout")
+ self.eventName = QtWidgets.QLineEdit(parent=self.gridLayoutWidget_3)
+ self.eventName.setObjectName("eventName")
+ self.gridLayout.addWidget(self.eventName, 1, 0, 1, 1)
+ self.labelEvent = QtWidgets.QLabel(parent=self.gridLayoutWidget_3)
+ self.labelEvent.setObjectName("labelEvent")
+ self.gridLayout.addWidget(self.labelEvent, 0, 0, 1, 1)
+ self.gridLayoutWidget_4 = QtWidgets.QWidget(parent=self.centralwidget)
+ self.gridLayoutWidget_4.setGeometry(QtCore.QRect(910, 90, 221, 41))
+ self.gridLayoutWidget_4.setObjectName("gridLayoutWidget_4")
+ self.gridLayout_2 = QtWidgets.QGridLayout(self.gridLayoutWidget_4)
+ self.gridLayout_2.setContentsMargins(0, 0, 0, 0)
+ self.gridLayout_2.setObjectName("gridLayout_2")
+ self.label_3 = QtWidgets.QLabel(parent=self.gridLayoutWidget_4)
+ font = QtGui.QFont()
+ font.setPointSize(18)
+ self.label_3.setFont(font)
+ self.label_3.setObjectName("label_3")
+ self.gridLayout_2.addWidget(self.label_3, 0, 0, 1, 1)
+ self.lcd_files_found = QtWidgets.QLCDNumber(parent=self.gridLayoutWidget_4)
+ self.lcd_files_found.setObjectName("lcd_files_found")
+ self.gridLayout_2.addWidget(self.lcd_files_found, 0, 1, 1, 1)
+ self.gridLayout_2.setColumnStretch(1, 1)
+ self.gridLayoutWidget_5 = QtWidgets.QWidget(parent=self.centralwidget)
+ self.gridLayoutWidget_5.setGeometry(QtCore.QRect(1140, 90, 311, 46))
+ self.gridLayoutWidget_5.setObjectName("gridLayoutWidget_5")
+ 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.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")
+ MainWindow.setCentralWidget(self.centralwidget)
+ self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
+ self.menubar.setGeometry(QtCore.QRect(0, 0, 1473, 24))
+ self.menubar.setObjectName("menubar")
+ self.menuBit_Mover = QtWidgets.QMenu(parent=self.menubar)
+ self.menuBit_Mover.setObjectName("menuBit_Mover")
+ MainWindow.setMenuBar(self.menubar)
+ self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
+ self.statusbar.setObjectName("statusbar")
+ MainWindow.setStatusBar(self.statusbar)
+ self.actionScan_Directory = QtGui.QAction(parent=MainWindow)
+ self.actionScan_Directory.setObjectName("actionScan_Directory")
+ self.menubar.addAction(self.menuBit_Mover.menuAction())
+
+ self.retranslateUi(MainWindow)
+ QtCore.QMetaObject.connectSlotsByName(MainWindow)
+
+ 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.l_camera.setText(_translate("MainWindow", "Camera"))
+ self.l_iso.setText(_translate("MainWindow", "ISO"))
+ self.l_date_time_created.setText(_translate("MainWindow", "Date / Time Created"))
+ self.l_lens.setText(_translate("MainWindow", "Lens"))
+ self.l_dpi.setText(_translate("MainWindow", "Resolution (DPI)"))
+ 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_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.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
new file mode 100755
index 0000000..f08bdeb
--- /dev/null
+++ b/BitMover_ui.py
@@ -0,0 +1,312 @@
+#!/usr/bin/env python
+import os
+import sys
+# import time
+
+from PyQt6.QtCore import *
+from PyQt6.QtGui import *
+from PyQt6.QtWidgets import *
+
+import traceback
+
+from configure import CONFIG_FILE, Configure
+from file_stuff import is_file, get_t_files
+from BitMover_MainWindow import Ui_MainWindow
+
+from get_image_tag import get_exif_tag
+
+from media import Media
+
+
+from lumberjack import timber
+from raw_photo import extract_jpg_thumb
+
+log = timber(__name__)
+
+class WorkerSignals(QObject):
+ """
+ Defines the signals available from a running worker thread.
+
+ Supported signals are:
+
+ finished
+ No data
+
+ error
+ tuple (exctype, value, traceback.format_exc() )
+
+ result
+ object data returned from processing, anything
+
+ progress
+ int indicating % progress
+
+ """
+ finished = pyqtSignal()
+ error = pyqtSignal(tuple)
+ result = pyqtSignal(object)
+ progress = pyqtSignal(int)
+
+
+class Worker(QRunnable):
+ """
+ Worker thread
+
+ Inherits from QRunnable to handler worker thread setup, signals and wrap-up.
+
+ :param callback: The function callback to run on this worker thread. Supplied args and
+ kwargs will be passed through to the runner.
+ :type callback: function
+ :param args: Arguments to pass to the callback function
+ :param kwargs: Keywords to pass to the callback function
+ """
+
+ def __init__(self, fn, *args, **kwargs):
+ super(Worker, self).__init__()
+
+ # Store constructor arguments (re-used for processing)
+ self.fn = fn
+ self.args = args
+ self.kwargs = kwargs
+ self.signals = WorkerSignals()
+
+ # Add the callback to our kwargs
+ self.kwargs['progress_callback'] = self.signals.progress
+
+ @pyqtSlot()
+ def run(self):
+ """
+ Initialise the runner function with passed args, kwargs.
+ """
+
+ # Retrieve args/kwargs here; and fire processing using them
+ try:
+ result = self.fn(*self.args, **self.kwargs)
+ except:
+ traceback.print_exc()
+ exctype, value = sys.exc_info()[:2]
+ self.signals.error.emit((exctype, value, traceback.format_exc()))
+ else:
+ self.signals.result.emit(result) # Return the result of the processing
+ finally:
+ self.signals.finished.emit() # Done
+
+# Subclass QMainWindow to customize your application's main window
+class MainWindow(QMainWindow, Ui_MainWindow):
+ def __init__(self, *args, **kwargs):
+ super(MainWindow,self).__init__(*args,**kwargs)
+ self.setupUi(self)
+ # uic.loadUi('BitMover.ui',self)
+ c = Configure(CONFIG_FILE)
+ self.config = c.load_config()
+ self.total_files = 0
+
+ # self.t_find_files = threading.Thread(target=self.find_files)
+
+ self.setWindowTitle("BitMover")
+ self.setWindowIcon(QIcon('assets/forklift.png'))
+
+ self.src_dir = self.config['folders']['source']['base']
+ self.dst_dir = self.config['folders']['destination']['base']
+
+ self.file_types = self.config['file_types']
+
+ self.lineEdit_src_dir.setText(self.src_dir)
+ self.lineEdit_dst_dir.setText(self.dst_dir)
+
+ self.pushButton_src_browse.clicked.connect(self.select_src_directory)
+ self.pushButton_dst_browse.clicked.connect(self.select_dst_directory)
+ self.pushButton_3_scan_dir.clicked.connect(self.find_files)
+ # self.pushButton_3_scan_dir.clicked.connect(self.t_find_files.start)
+ self.lcd_files_found.display(int(0))
+ self.set_progress(0,0)
+
+ self.img_preview.setPixmap(QPixmap('assets/preview_placeholder.jpg'))
+ self.img_preview.setScaledContents(True)
+ # self.img_preview.setFixedWidth(preview_width)
+ # self.img_preview.setFixedHeight(preview_height)
+
+ self.file_list.currentItemChanged.connect(self.index_changed)
+ self.threadpool = QThreadPool()
+
+ self.files = {}
+
+ def index_changed(self,i):
+ f = i.text()
+
+ event = self.get_event()
+ c = self.config
+
+ m = Media(f,event,c)
+
+ dtc = f'{m.capture_date[0]}/{m.capture_date[1]}/{m.capture_date[2]}'
+ self.label_data_date_time_created.setText(dtc)
+
+ if m.file_type == 'image':
+ # img = Image.open(f)
+ dpi = get_exif_tag(f,"xresolution")
+ width = get_exif_tag(f,"image width")
+ height = get_exif_tag(f,"image height")
+ if width is None:
+ get_exif_tag(f,"exifimagewidth")
+ if height is None:
+ get_exif_tag(f,"exifimagelength")
+ # width = img.width
+ # height = img.height
+ size = f'{width}x{height}'
+ if width is not None and height is not None:
+ mpixels = (width * height) / 1000000
+ else:
+ mpixels = ''
+ iso = get_exif_tag(f,"iso")
+ aperture = get_exif_tag(f,"fnumber")
+ camera = get_exif_tag(f,"cameramodelname")
+ if camera is None:
+ camera = get_exif_tag(f,"image model")
+ lens = get_exif_tag(f,"lensmodel")
+ zoom = get_exif_tag(f,"focallength")
+
+ print(f'size: {size}')
+ print(f'dpi: {dpi}')
+ print(f'iso: {iso}')
+ print(f'lens: {lens}')
+ print(f'zoom: {zoom}')
+ print(f'camera: {camera}')
+ print(f'aperture: {aperture}')
+ print(f'mpixels: {mpixels}')
+
+ self.label_data_width_height.setText(str(size))
+ self.label_data_dpi.setText(str(dpi))
+ self.label_data_iso.setText(str(iso))
+ self.label_data_lens.setText(str(lens))
+ self.label_data_zoom.setText(str(zoom))
+ self.label_data_camera.setText(str(camera))
+ self.label_data_aperture.setText(str(aperture))
+ self.label_data_megapixels.setText(str(mpixels))
+
+ if f.lower().endswith("jpg") or f.lower().endswith("jpeg"):
+ self.img_preview.setPixmap(QPixmap(f))
+ else:
+ # jpg = img.convert("RGB")
+ jpg = extract_jpg_thumb(f)
+ self.img_preview.setPixmap(QPixmap(jpg))
+
+
+ def select_src_directory(self):
+ directory = QFileDialog.getExistingDirectory(self,
+ "Select Directory",
+ self.src_dir)
+ if directory:
+ print("Selected Directory:", directory)
+ # path = Path(directory)
+ self.src_dir = directory
+ self.lineEdit_src_dir.setText(self.src_dir)
+
+ def select_dst_directory(self):
+ directory = QFileDialog.getExistingDirectory(self,
+ "Select Directory",
+ self.dst_dir)
+ if directory:
+ print("Selected Directory:", directory)
+ # path = Path(directory)
+ self.dst_dir = directory
+ self.lineEdit_dst_dir.setText(self.dst_dir)
+
+ def set_progress(self,p,t):
+ """
+ set progress for bar,
+ p = progress counter
+ t = target total
+ o = progress bar object
+ """
+ if int(t) == 0:
+ t += 1
+ #
+ # while QPainter.isActive(Ui_MainWindow.QPainter()):
+ # print('painter active')
+ # time.sleep(0.02)
+
+ percent_complete = (int(p) / int(t)) * 100
+ self.progressBar_overall.setValue(int(percent_complete))
+
+ def print_output(self, s):
+ print(s)
+
+ def thread_complete(self):
+ print("THREAD COMPLETE!")
+
+ def thread_find_files(self):
+ print('in thread_find_files')
+ print(self.src_dir)
+ worker = Worker(self.find_files())
+ worker.signals.result.connect(self.print_output)
+ worker.signals.finished.connect(self.thread_complete)
+ worker.signals.progress.connect(self.progress_fn)
+ self.threadpool.start(worker)
+
+
+
+ def find_files(self):
+ """ find files to build a dictionary out of """
+ log.info('In find_files')
+ print(self.src_dir)
+
+ file_count = int(0)
+ file_total = get_t_files(self.src_dir,self.file_types)
+
+ 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 tqdm(filename,
+ # desc='Finding ' + ext + ' Files in ' + folder):
+ 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(folder, file)
+ self.set_progress(file_count,file_total)
+ # time.sleep(.02)
+
+ else:
+ print(f"Skipping {current_file} as it does not look like a real file.")
+
+ progress_callback.emit((file_count / file_total) * 100)
+
+ def get_event(self):
+ event_name = self.eventName.text()
+ return event_name
+
+ def process_file(self,path_name, f_name):
+ """ gather information and add to dictionary """
+
+ event = self.get_event()
+ c = self.config
+
+ 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}')
+
+ self.files[i] = { 'folders': {}, 'date': {} }
+
+ self.files[i]['folders']['source_path'] = m.source_path_dir
+ self.files[i]['type'] = m.file_type
+ self.files[i]['name'] = m.file_name
+ self.files[i]['extension'] = m.file_ext
+ self.files[i]['date']['capture_date'] = {}
+ self.files[i]['date']['capture_date']['y'] = m.capture_date[0]
+ self.files[i]['date']['capture_date']['m'] = m.capture_date[1]
+ 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.file_list.addItem(f"{self.files[i]['folders']['source_path']}/{self.files[i]['name']}")
+
+
+app = QApplication(sys.argv)
+
+window = MainWindow()
+window.show()
+
+app.exec()
\ No newline at end of file
diff --git a/assets/forklift.png b/assets/forklift.png
new file mode 100644
index 0000000..0fa43a8
Binary files /dev/null and b/assets/forklift.png differ
diff --git a/assets/preview_placeholder.jpg b/assets/preview_placeholder.jpg
new file mode 100644
index 0000000..a1685b8
Binary files /dev/null and b/assets/preview_placeholder.jpg differ
diff --git a/bitmover.py b/bitmover.py
new file mode 100644
index 0000000..8e521cb
--- /dev/null
+++ b/bitmover.py
@@ -0,0 +1,85 @@
+#!/usr/bin/env python
+
+"""
+Functions to copy bits around ...
+"""
+
+from os import system,path,rename
+from tqdm import tqdm
+
+from file_stuff import create_folder, cmp_files, path_exists
+from lumberjack import timber
+from hashing import xx_hash
+# from configure import Configure, CONFIG_FILE
+
+# c = Configure(CONFIG_FILE)
+# config = c.load_config()
+log = timber(__name__)
+
+def copy_with_progress(s,d,f):
+ """ Copy a file with the progress bar """
+ log.debug(f'copy_with_progress({s},{d},{f})')
+
+ size = path.getsize(s)
+ with open(s, 'rb') as fs:
+ with open(d, 'wb') as fd:
+ with tqdm(total=size, unit='B', unit_scale=True, desc=f'Copying {f}') as pbar:
+ while True:
+ chunk = fs.read(4096)
+ if not chunk:
+ break
+ fd.write(chunk)
+ pbar.update(len(chunk))
+
+def copy_from_source(source_path,dest_path,file_name):
+ """ Copy file from source to destination """
+ log.debug(f'copy_from_source({source_path},{dest_path},{file_name}')
+
+ file_exists = path_exists(path.join(dest_path,file_name))
+
+ if file_exists is True:
+ log.debug(f'\nFound {file_name} at destination, checking if they match.')
+ check_match = cmp_files(
+ path.join(source_path,file_name),
+ path.join(dest_path, file_name))
+ if check_match is False:
+ log.warn(f'\nFound duplicate for {source_path}/{file_name}, \
+ renaming destination with hash appended.')
+ base, extension = path.splitext(file_name)
+ #md5 = md5_hash(os.path.join(dest_path, file_name))
+ f_xxhash = xx_hash(path.join(dest_path, file_name))
+ #file_name_hash = base + '_' + md5 + extension
+ file_name_hash = base + '_' + f_xxhash + extension
+ rename(path.join(dest_path, file_name),
+ path.join(dest_path, file_name_hash))
+ else:
+ log.info(f'\n{file_name} hashes match')
+ # remove(path.join(source_path,file_name))
+ # f.pop(file_name)
+ return
+
+ # create_folder(dest_path)
+ # shutil.copy(os.path.join(source_path,file_name), dest_path)
+ copy_with_progress(path.join(source_path, file_name),
+ path.join(dest_path, file_name),
+ file_name)
+ system('clear')
+
+def copy_files(f,config):
+ """ Copy Files. """
+ log.debug(f'copy_files({f})')
+ system('clear')
+ for file in tqdm(f, desc="Copying Files:"):
+ create_folder(f[file]['folders']['destination'])
+
+ copy_from_source(f[file]['folders']['source_path'],
+ f[file]['folders']['destination'],
+ f[file]['name'])
+
+ if config['store_originals'] is True:
+ if f[file]['type'] == 'image':
+ create_folder(f[file]['folders']['destination_original'])
+
+ copy_from_source(f[file]['folders']['destination'],
+ f[file]['folders']['destination_original'],
+ f[file]['name'])
\ No newline at end of file
diff --git a/configure.py b/configure.py
new file mode 100644
index 0000000..c83439f
--- /dev/null
+++ b/configure.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+
+"""
+Load the config file from yaml file
+"""
+import sys
+import yaml
+from lumberjack import timber
+
+files = {}
+CONFIG_FILE = 'config.yaml'
+log = timber(__name__)
+
+class Configure:
+ """ Configure Class """
+ def __init__(self,config_file):
+ """ init """
+ self.config_file = config_file
+ self.config = ''
+
+ def load_config(self):
+ """ load configuration from yaml file """
+ try:
+ with open(self.config_file, 'r', encoding="utf-8") as cf:
+ self.config = yaml.load(cf, Loader=yaml.FullLoader)
+ except FileNotFoundError as fnf_err:
+ print(f'{fnf_err}: {self.config_file}') # This cannot be log b/c we haven't validated the logger yet.
+ print(f'Copy config.yaml.EXAMPLE to {self.config_file}, and update accordingly.') # This cannot be log b/c we haven't validated the logger yet.
+ sys.exit()
+
+ return self.config
\ No newline at end of file
diff --git a/dedup.py b/dedup.py
new file mode 100755
index 0000000..1ee3935
--- /dev/null
+++ b/dedup.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python
+
+import argparse
+import sys
+import os
+
+from bitmover import copy_from_source
+from file_stuff import create_folder, cmp_files
+from lumberjack import timber
+
+log = timber(__name__)
+
+parser = argparse.ArgumentParser()
+parser.add_argument("-f", "--folder", help = "folder with files to rename")
+parser.add_argument("-d", "--dryrun", help = "dry run, no action")
+
+args = parser.parse_args()
+
+if args.folder:
+ FOLDER = args.folder
+else:
+ print("you need to specify a folder.")
+ sys.exit()
+if args.dryrun:
+ dry_run = True
+else:
+ dry_run = False
+
+def get_file_size(f_name):
+ return os.path.getsize(f_name)
+
+
+l = []
+files = os.listdir(FOLDER)
+dup_folder = os.path.join(FOLDER,"__dups")
+SIZE = 0
+MAX_SIZE = 0
+BIGGEST_FILE = ''
+
+create_folder(dup_folder)
+f_list = {}
+dictionary = {}
+for x in files:
+ if os.path.isfile(os.path.join(FOLDER,x)):
+ if x.lower().endswith("jpg") or x.lower().endswith("jpeg"):
+ group = dictionary.get(x[:23],[])
+ group.append(x)
+ dictionary[x[:23]] = group
+
+for g in dictionary:
+ f_list[g] = {'files': {}}
+
+ for f in dictionary[g]:
+ p = os.path.join(FOLDER,f)
+ size = os.path.getsize(p)
+ f_list[g]['files'][f] = {}
+ f_list[g]['files'][f]['path'] = p
+ f_list[g]['files'][f]['size'] = size
+
+# print(f_list)
+
+for g in f_list:
+ MAX_SIZE = 0
+ log.debug(g)
+ if len(f_list[g]['files']) > 1:
+ for f in f_list[g]['files']:
+ log.debug(f"{f_list[g]['files'][f]['path']}: {f_list[g]['files'][f]['size']}")
+ SIZE = f_list[g]['files'][f]['size']
+
+ if SIZE > MAX_SIZE:
+ MAX_SIZE = SIZE
+ BIGGEST_FILE = f_list[g]['files'][f]['path']
+ log.debug(f'New Biggest File: {BIGGEST_FILE}, {MAX_SIZE*1024}KB')
+ f_list[g]['biggest_file'] = BIGGEST_FILE
+ else:
+ log.debug(f'Only 1 file in {g}')
+
+for g in f_list:
+ # log.debug(g)
+ if len(f_list[g]['files']) > 1:
+ for f in f_list[g]['files']:
+ if f_list[g]['biggest_file'] != f_list[g]['files'][f]['path']:
+ copy_from_source(FOLDER, dup_folder, os.path.basename(f_list[g]['files'][f]['path']))
+
+ file_match = cmp_files(f_list[g]['files'][f]['path'],
+ os.path.join(dup_folder,
+ os.path.basename(f_list[g]['files'][f]['path'])))
+
+ if file_match is True:
+ os.remove(f_list[g]['files'][f]['path'])
+ else:
+ print(f"{f_list[g]['files'][f]['path']} does not match {os.path.join(dup_folder, os.path.basename(f_list[g]['files'][f]['path']))}")
\ No newline at end of file
diff --git a/dedup_wrapper.sh b/dedup_wrapper.sh
new file mode 100755
index 0000000..8c21149
--- /dev/null
+++ b/dedup_wrapper.sh
@@ -0,0 +1,78 @@
+#!/usr/bin/env bash
+
+
+root_dir="$1"
+
+if [[ $root_dir == '' ]]; then
+ echo "You must pass a directory as the first argument."
+ exit 1
+fi
+
+function dedup {
+ echo "renaming: $1"
+ ./rename.py -f "$1"
+
+ echo "deduping: $1"
+ ./dedup.py -f "$1"
+}
+
+for i in `ls -1 $root_dir`; do
+ echo $i
+ if [[ -d "${root_dir}/$i" ]]; then
+ echo "${root_dir}/$i is a dir"
+ if [[ $i == *JPG* || $i == *RAW* ]]; then
+ echo "${root_dir}/$i contains JPG/RAW"
+ dedup ${root_dir}/$i
+ else
+ echo "${root_dir}/$i does not contain JPG/RAW"
+ for s1 in `ls -1 ${root_dir}/$i`; do
+ echo "$s1"
+ if [[ -d "${root_dir}/${i}/$s1" ]]; then
+ echo "${root_dir}/${i}/$s1 is a dir"
+ if [[ $s1 == *JPG* || $s1 == *RAW* ]]; then
+ echo "${root_dir}/${i}/$s1 contains JPG/RAW"
+ dedup ${root_dir}/${i}/$s1
+ else
+ echo "${root_dir}/${i}/$s1 does not contain JPG/RAW"
+ for s2 in `ls -1 ${root_dir}/${i}/$s1`; do
+ echo ${root_dir}/${i}/${s1}/$s2
+ if [[ -d "${root_dir}/${i}/${s1}/$s2" ]]; then
+ echo "${root_dir}/${i}/${s1}/$s2 is a dir"
+ if [[ ${root_dir}/${i}/${s1}/$s2 == *JPG* || ${root_dir}/${i}/${s1}/$s2 == *RAW* ]]; then
+ echo "${root_dir}/${i}/${s1}/$s2 contains JPG/RAW"
+ dedup ${root_dir}/${i}/${s1}/$s2
+ else
+ echo "${root_dir}/${i}/${s1}/$s2 does not contain JPG/RAW"
+ for s3 in `ls -1 ${root_dir}/${i}/${s1}/$s2`; do
+ echo ${root_dir}/${i}/${s1}/${s2}/$s3
+ if [[ -d "${root_dir}/${i}/${s1}/${s2}/$s3" ]]; then
+ echo "${root_dir}/${i}/${s1}/${s2}/$s3 is a dir"
+ if [[ ${root_dir}/${i}/${s1}/${s2}/$s3 == *JPG* || ${root_dir}/${i}/${s1}/${s2}/$s3 == *RAW* ]]; then
+ echo "${root_dir}/${i}/${s1}/${s2}/$s3 contains JPG/RAW"
+ dedup ${root_dir}/${i}/${s1}/${s2}/$s3
+ else
+ echo "${root_dir}/${i}/${s1}/${s2}/${s3} does not contain JPG/RAW"
+ for s4 in `ls -1 ${root_dir}/${i}/${s1}/${s2}/${s3}`; do
+ echo ${root_dir}/${i}/${s1}/${s2}/${s3}/$s4
+ if [[ -d "${root_dir}/${i}/${s1}/${s2}/${s3}/$s4" ]]; then
+ echo "${root_dir}/${i}/${s1}/${s2}/${s3}/$s4 is a dir"
+ if [[ ${root_dir}/${i}/${s1}/${s2}/${s3}/$s4 == *JPG* || ${root_dir}/${i}/${s1}/${s2}/${s3}/$s4 == *RAW* ]]; then
+ echo "${root_dir}/${i}/${s1}/${s2}/${s3}/$s4 contains JPG/RAW"
+ dedup ${root_dir}/${i}/${s1}/${s2}/${s3}/$s4
+ fi
+ fi
+ done
+ fi
+ fi
+ done
+ fi
+ fi
+ done
+ fi
+ fi
+ done
+ fi
+ fi
+done
+
+
diff --git a/file_stuff.py b/file_stuff.py
new file mode 100644
index 0000000..c0a6f69
--- /dev/null
+++ b/file_stuff.py
@@ -0,0 +1,143 @@
+#!/usr/bin/env python
+
+"""
+dump the dictionary generated from findings into a yaml file for later inspection
+"""
+
+import os
+import yaml
+from tqdm import tqdm
+
+### Local Imports
+# from configure import Configure, CONFIG_FILE
+from hashing import xx_hash
+from lumberjack import timber
+
+def check_log_dir(d):
+ create_folder(d)
+
+
+# c = Configure(CONFIG_FILE)
+# config = c.load_config()
+log = timber(__name__)
+log.info("Starting")
+
+def dump_yaml(dictionary,file):
+ """ dump dictionary to yaml file """
+ with open(file, 'w', encoding="utf-8") as f:
+ yaml.dump(dictionary, f)
+
+ f.close()
+
+def path_exists(p):
+ """ determine if the path exists """
+ log.debug(f'path_exists({p})')
+ pe = os.path.exists(p)
+ # print(f'Path Exists: {pe}')
+
+ return pe
+
+def is_dir(d):
+ """ determine if object is a dir or not """
+ log.debug(f'is_dir({d})')
+
+ return os.path.isdir(d)
+
+def is_file(f):
+ """ determine if object is a file or not """
+ log.debug(f'is_file({f})')
+
+ return os.path.isfile(f)
+
+def cmp_files(f1,f2):
+ """ compare two files """
+ log.debug(f'cmp_files({f1},{f2})')
+ #TODO: Determine if path is actually a file
+ #TODO: Determine if the hash has already been stored and use it if so
+
+ hash1 = xx_hash(f1)
+ hash2 = xx_hash(f2)
+ return hash1 == hash2
+
+def path_access_read(path):
+ """ make sure we can read from the path """
+ log.debug(f'path_access_read({path})')
+
+ val = os.access(path, os.R_OK)
+
+ if val is False:
+ log.error(f'Can not read from {path}')
+
+ return val
+
+def path_access_write(path):
+ """ make sure we can write to the path """
+ log.debug(f'path_access_write({path})')
+
+ val = os.access(path, os.W_OK)
+
+ if val is False:
+ log.error(f'Can not write to {path}')
+
+ return val
+
+def create_folder(file):
+ """ Function to create folder """
+ log.debug(f'create_folder({file})')
+
+ if path_exists(file) is False:
+ os.makedirs(file)
+ elif is_dir(file) is False:
+ pass # this needs to turn into bailing out as there is a collision.
+
+def cleanup_sd(f,config):
+ """ If we should clean up the SD, nuke the copied files. """
+ log.debug(f'cleanup_sd({f})')
+
+ if config['cleanup_sd'] is True:
+ os.system('clear')
+ for file in tqdm(f, desc = "Cleaning Up SD:"):
+ if f[file]['source_cleanable'] is True:
+ log.debug(f"Cleanup SD: Removing File -\n{os.path.join(f[file]['folders']['source_path'],f[file]['name'])}")
+
+ os.remove(os.path.join(f[file]['folders']['source_path'],f[file]['name']))
+ else:
+ log.debug(f"Cleanup SD: Not Removing File -\n{os.path.join(f[file]['folders']['source_path'],f[file]['name'])}")
+
+def validate_config_dir_access(config):
+ """ Validate we can operate in the defined directories """
+ log.debug('validate_config_dir_access')
+
+ check = path_access_write(config['folders']['destination']['base'])
+ if check is False:
+ accessible = False
+ else:
+ check = path_access_read(config['folders']['source']['base'])
+ if check is False:
+ accessible = False
+ else:
+ if config['store_backup'] is True:
+ check = path_access_write(config['folders']['backup'])
+ if check is False:
+ accessible = False
+ else:
+ accessible = True
+ else:
+ accessible = True
+ return accessible
+
+def get_t_files(p,t):
+ total = int(0)
+ for folder, subfolders, filename in os.walk(p):
+ for f_type in t:
+ for ext in t[f_type]:
+ # for file in tqdm(filename,
+ # desc='Finding ' + ext + ' Files in ' + folder):
+ for file in filename:
+ if file.lower().endswith(ext):
+ current_file = os.path.join(folder, file)
+ if is_file(current_file):
+ total += int(1)
+ else:
+ print(f"Skipping {current_file} as it does not look like a real file.")
+ return total
\ No newline at end of file
diff --git a/get_image_tag.py b/get_image_tag.py
new file mode 100644
index 0000000..8035d13
--- /dev/null
+++ b/get_image_tag.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python
+
+"""
+Get EXIF information from image
+"""
+from uu import Error
+
+import exifread
+from datetime import datetime
+from lumberjack import timber
+
+log = timber(__name__)
+
+def get_exif_date(tags,tag,f):
+ t = ''
+ log.debug(f'function: get_exif_tag(tags:{tags},tag:{tag},format:{f}')
+ try:
+ t = datetime.strptime(str(tags[tag]),f)
+ except:
+ pass
+ # log.debug(f'Error: {e}. Format: {f}')
+
+ return t
+
+def get_os_ctime(path):
+ # Don't pull the file ctime anymore, more often than not, it's wrong.
+ # t = datetime.fromtimestamp(os.path.getctime(path))
+
+ #raise an error so it will except and move on.
+ raise ValueError(f"{path}: Using ctime like this is usually not good.")
+
+
+def set_generic_date_time(date = '1900:01:01',
+ time = '00:00:00',
+ f = '%Y:%m:%d %H:%M:%S'):
+
+ t = datetime.strptime(str(f'{date} {time}'),f)
+ return t
+
+
+def get_img_date(p):
+ with open(p, 'rb') as file:
+ tags = exifread.process_file(file)
+
+ if 'Composite DateTimeOriginal' in tags:
+ get_exif_date(tags,'Composite DateTimeOriginal','%Y:%m:%d %H:%M')
+
+def get_exif_tag(p,t):
+ with open(p, "rb") as f:
+ try:
+ tags = exifread.process_file(f)
+ print(f'{p}: {tags}')
+ except Error as e:
+ return f'Received Error: {e} when trying to open {p}'
+ finally:
+ f.close()
+
+ for tag in tags:
+ print(tag)
+ if t in tag.lower():
+ print(tags[tag])
+ return tags[tag]
+ else:
+ pass
+
+def get_image_date(path):
+ t = ''
+ exif_dt = ''
+
+ with open(path, "rb") as file:
+ try:
+ tags = exifread.process_file(file)
+ except Error as e:
+ log.error(e)
+ finally:
+ file.close()
+
+ for tag in tags:
+ if "DateTime" in tag:
+ t = tag
+ break
+
+ if '' == t:
+ exif_dt = set_generic_date_time()
+ else:
+ for f in ['%Y:%m:%d %H:%M:%S',
+ '%Y/%m/%d %H:%M:%S',
+ '%Y-%m-%d-%H-%M-%S']:
+ log.debug(f'Trying... {t}, {f}, {path} ')
+ exif_dt = get_exif_date(tags, t, f)
+
+ if '' != exif_dt:
+ break
+
+ if '' == exif_dt:
+ exif_dt = set_generic_date_time()
+ # s = get_os_ctime(path) # This could produce wildly incorrect results
+ return exif_dt
\ No newline at end of file
diff --git a/hashing.py b/hashing.py
new file mode 100644
index 0000000..ab8012a
--- /dev/null
+++ b/hashing.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python
+
+"""
+dump the dictionary generated from findings into a yaml file for later inspection
+"""
+
+import os
+import xxhash
+from tqdm import tqdm
+# from configure import Configure, CONFIG_FILE
+from lumberjack import timber
+
+# c = Configure(CONFIG_FILE)
+# config = c.load_config()
+log = timber(__name__)
+
+def xx_hash(file):
+ """ calculates and returns file hash based on xxHash """
+ size = os.path.getsize(file)
+ hasher = xxhash.xxh64()
+
+ with open(file, 'rb') as f:
+ with tqdm(total=size,
+ unit='B',
+ unit_scale=True,
+ desc=f'Getting hash for {os.path.basename(file)}') as pbar:
+ for chunk in iter(lambda: f.read(4096), b""):
+ hasher.update(chunk)
+ pbar.update(len(chunk))
+ file_hash = hasher.hexdigest()
+
+ return file_hash
+
+def hash_path(path):
+ """ hashes a string passed as a path """
+
+ hasher = xxhash.xxh64(path)
+ return hasher.hexdigest()
+
+def gen_xxhashes(f):
+ """ Generate xxHashes """
+ log.debug(f'gen_xxhashes({f})')
+ for file in tqdm(f, desc = "Generating xx Hashes:"):
+ os.system('clear')
+ log.debug(f[file])
+ f[file]['xx_checksums'] = {}
+ for folder in f[file]['folders']:
+ k = os.path.join(f[file]['folders'][folder], f[file]['name'])
+ if k != f[file]['name']:
+ # k = f[file]['folders'][folder]
+ log.debug(k)
+ f[file]['xx_checksums'][k] = xx_hash(k)
+ log.debug(f"{k}: {f[file]['xx_checksums'][k]}")
+ log.debug(f[file])
+
+def validate_xx_checksums(f):
+ """ Validate Checksums """
+ for file in tqdm(f, desc = "Verifying Checksums:"):
+ os.system('clear')
+ i = 0
+ c = {}
+ for checksum in f[file]['xx_checksums']:
+ c[i] = f[file]['xx_checksums'][checksum]
+ if i > 0:
+ p = i - 1
+ if c[i] == c[p]:
+ f[file]['source_cleanable'] = True
+ else:
+ f[file]['source_cleanable'] = False
+ log.critical(f'FATAL: Checksum validation failed for: \
+ {f[file]["name"]} \n{c[i]}\n is not equal to \n{c[p]}\n')
+ log.debug('\n File Meta:\n')
+ log.debug(f[file])
+ i = i + 1
\ No newline at end of file
diff --git a/import_media.py b/import_media.py
index b5e2759..11586dc 100755
--- a/import_media.py
+++ b/import_media.py
@@ -14,30 +14,22 @@ TODO:
12. Make a graphical interface
"""
-import os
-import sys
-from pprint import pprint
import argparse
-import shutil
-import hashlib
-import xxhash
-from datetime import datetime
+import os
from tqdm import tqdm
-import yaml
-import exifread
-import ffmpeg
-CONFIG_FILE = 'config.yaml'
-files = {}
+### Local Imports
+from configure import CONFIG_FILE, Configure, files
+from file_stuff import cleanup_sd, validate_config_dir_access, is_file
+from hashing import gen_xxhashes, validate_xx_checksums
+from bitmover import copy_files
+from lumberjack import timber
+from media import process_file
-# Read configuration from file
-try:
- with open(CONFIG_FILE, 'r') as cf:
- config = yaml.load(cf, Loader=yaml.FullLoader)
-except FileNotFoundError:
- print("Configuration file not found: ", CONFIG_FILE)
- print("Copy config.yaml.EXAMPLE to ", CONFIG_FILE, " and update accordingly.")
- sys.exit()
+c = Configure(CONFIG_FILE)
+config = c.load_config()
+log = timber(__name__)
+log.info("Starting import_media")
parser = argparse.ArgumentParser()
parser.add_argument("-e", "--event", help = "Event Name")
@@ -77,403 +69,42 @@ if args.destination:
#if args.generate-config:
# pass
-def dump_yaml(dictionary, file):
- """ dump a dictionary to a yaml file """
- one_million = 1000**2
- with open(file, 'w') as f:
- yaml.dump(
- dictionary, f,
- default_flow_style=False,
- width=one_million)
-
-def is_file(file):
- """ Determine if the object is a file. """
- return bool(os.path.isfile(file))
-
-'''
-def md5_hash(file):
- """ calculates and returns md5 hash """
- if config['verify_checksum']:
- #print("calculating md5 for ", f)
- md5 = hashlib.md5(open(file, 'rb').read()).hexdigest()
- #with open(file, 'r') as f:
- # md5 = hashlib.md5(f).hexdigest()
- else:
- md5 = 'no_verify'
- return md5
-'''
-
-def xx_hash(file):
- """ calculates and returns file hash based on xxHash """
- if config['verify_checksum']:
- size = os.path.getsize(file)
- hasher = xxhash.xxh64()
- with open(file, 'rb') as f:
- with tqdm(total=size,
- unit='B',
- unit_scale=True,
- desc=f'Getting hash for {os.path.basename(file)}') as pbar:
- for chunk in iter(lambda: f.read(4096), b""):
- hasher.update(chunk)
- pbar.update(len(chunk))
- file_hash = hasher.hexdigest()
- else:
- file_hash = 'no_verify'
- return file_hash
-
-def cmp_files(file_1,file_2):
- """ Use file hashes to compare files """
- hash1 = xx_hash(file_1)
- hash2 = xx_hash(file_2)
- print(f'\n{hash1}')
- print(f'\n{hash2}')
- return hash1 == hash2
-
-def get_capture_date(path, f_type):
- """ get capture date from meta """
- if f_type == 'image':
- with open(path, "rb") as file:
- tags = exifread.process_file(file)
-
- if 'EXIF DateTimeOriginal' in tags:
- try:
- stamp = datetime.strptime(
- str(tags['EXIF DateTimeOriginal']),
- '%Y:%m:%d %H:%M:%S')
- except ValueError as ve_dte:
- print(f"\nError: {ve_dte}")
- print("\nTrying digitized")
- try:
- stamp = datetime.strptime(
- str(tags['EXIF DateTimeDigitized']),
- '%Y:%m:%d %H:%M:%S')
- except ValueError as ve_dtd:
- print(f"\nError: {ve_dtd}")
- print("\nTrying Image DateTime")
- try:
- stamp = datetime.strptime(
- str(tags['Image DateTime']),
- '%Y:%m:%d %H:%M:%S')
- except ValueError as ve_idt:
- print(f"\nError: {ve_idt}")
- print(f"\nGiving up... Please inspect {path} and try again\n")
- sys.exit()
- elif 'Image DateTime' in tags:
- stamp = datetime.strptime(
- str(tags['Image DateTime']), '%Y:%m:%d %H:%M:%S')
- else:
- stamp = datetime.strptime(
- str('1900:01:01 00:00:00'), '%Y:%m:%d %H:%M:%S')
- elif f_type == 'video':
- try:
- stamp = datetime.strptime(
- ffmpeg.probe(path)['format']['tags']['creation_time'],
- '%Y-%m-%dT%H:%M:%S.%f%z')
- except:
- print(f"\n{path} had an error. Please inspect the file and try again.")
- sys.exit()
- elif f_type == 'audio':
- try:
- stamp = datetime.strptime(ffmpeg.probe(
- path)['format']['tags']['date'], '%Y-%m-%d')
- except KeyError as ke:
- print(f'\nError: {ke} for {path}. Trying getctime...')
- try:
- stamp = datetime.fromtimestamp(os.path.getctime(path))
- except:
- print(f'\nCould not get timestamp for {path}. Giving up.')
- sys.exit()
- else:
- try:
- stamp = datetime.fromtimestamp(os.path.getctime(path))
- except:
- print(f'\nCould not get timestamp for {path}. Giving up.')
- sys.exit()
-
- year = stamp.strftime("%Y")
- month = stamp.strftime("%m")
- day = stamp.strftime("%d")
- return year, month, day
-
-def path_exists(path):
- """ Does the path exist """
- return os.path.exists(path)
-
-def is_dir(path):
- """ determine if the argument passed is a directory """
- p_exists = path_exists(path)
-
- if p_exists is True:
- it_is_dir = os.path.isdir(path)
- else:
- it_is_dir = p_exists
- return it_is_dir
-
-def path_access_read(path):
- """ make sure we can read from the path """
- val = os.access(path, os.R_OK)
-
- if val is False:
- print(f'Can not read from {path}')
-
- return val
-
-def path_access_write(path):
- """ make sure we can write to the path """
- val = os.access(path, os.W_OK)
-
- if val is False:
- print(f'Can not write to {path}')
-
- return val
-
-def create_folder(file):
- """ Function to create folder """
- if path_exists(file) is False:
- os.makedirs(file)
- elif is_dir(file) is False:
- pass # this needs to turn into bailing out as there is a collision.
-
-def copy_with_progress(s,d,f):
- """ Copy a file with the progress bar """
- size = os.path.getsize(s)
- with open(s, 'rb') as fs:
- with open(d, 'wb') as fd:
- with tqdm(total=size, unit='B', unit_scale=True, desc=f'Copying {f}') as pbar:
- while True:
- chunk = fs.read(4096)
- if not chunk:
- break
- fd.write(chunk)
- pbar.update(len(chunk))
-
-def copy_from_source(source_path,dest_path,file_name):
- """ Copy file from source to destination """
-
- file_exists = path_exists(os.path.join(dest_path,file_name))
-
- if file_exists is True:
- print(f'\nFound {file_name} at destination, checking if they match.')
- check_match = cmp_files(os.path.join(source_path,file_name),
- os.path.join(dest_path, file_name))
- if check_match is False:
- print(f'\nFound duplicate for {source_path}/{file_name}, \
- renaming destination with hash appended.')
- base, extension = os.path.splitext(file_name)
- #md5 = md5_hash(os.path.join(dest_path, file_name))
- f_xxhash = xx_hash(os.path.join(dest_path, file_name))
- #file_name_hash = base + '_' + md5 + extension
- file_name_hash = base + '_' + f_xxhash + extension
- os.rename(os.path.join(dest_path, file_name),
- os.path.join(dest_path, file_name_hash))
- else:
- print(f'\n{file_name} hashes match')
- return
-
- create_folder(dest_path)
- #shutil.copy(os.path.join(source_path,file_name), dest_path)
- copy_with_progress(os.path.join(source_path,file_name),
- os.path.join(dest_path,file_name),
- file_name)
-
- os.system('clear')
-
-def process_file(path, f_type, f_name, ext):
- """ gather information and add to dictionary """
-
- i = os.path.join(path,f_name)
-
- files[i] = { 'folders': {}, 'date': {} }
-
- files[i]['folders']['source_path'] = path
- files[i]['type'] = f_type
- files[i]['name'] = f_name
- files[i]['extension'] = ext
-
- files[i]['date']['capture_date'] = get_capture_date(
- os.path.join(files[i]['folders']['source_path'],
- files[i]['name']),files[i]['type'])
- files[i]['date']['y'] = files[i]['date']['capture_date'][0]
- files[i]['date']['m'] = files[i]['date']['capture_date'][1]
- files[i]['date']['d'] = files[i]['date']['capture_date'][2]
-
- if EVENT is not False:
- files[i]['folders']['destination'] = config['folders']['destination']['base'] + \
- '/' + files[i]['date']['y'] + '/' + \
- files[i]['date']['y'] + '-' + \
- files[i]['date']['m'] + '/' + \
- files[i]['date']['y'] + '-' + \
- files[i]['date']['m'] + '-' + \
- files[i]['date']['d'] + '-' + \
- EVENT
- else:
- files[i]['folders']['destination'] = config['folders']['destination']['base'] + \
- '/' + files[i]['date']['y'] + '/' + \
- files[i]['date']['y'] + '-' + \
- files[i]['date']['m'] + '/' + \
- files[i]['date']['y'] + '-' + \
- files[i]['date']['m'] + '-' + \
- files[i]['date']['d']
-
- if files[i]['type'] == 'image':
- files[i]['folders']['destination'] = files[i]['folders']['destination'] + '/PHOTO'
-
- if files[i]['extension'] in ('jpg', 'jpeg'):
- if config['store_originals'] is True:
- files[i]['folders']['destination_original'] = files[i]['folders']['destination'] + \
- '/ORIGINALS/JPG'
- files[i]['folders']['destination'] = files[i]['folders']['destination'] + \
- '/JPG'
- else:
- if config['store_originals'] is True:
- files[i]['folders']['destination_original'] = files[i]['folders']['destination'] + \
- '/ORIGINALS/RAW'
- files[i]['folders']['destination'] = files[i]['folders']['destination'] + '/RAW'
-
- elif files[i]['type'] == 'video':
- files[i]['folders']['destination'] = files[i]['folders']['destination'] + '/VIDEO'
-
- elif files[i]['type'] == 'audio':
- files[i]['folders']['destination'] = files[i]['folders']['destination'] + '/AUDIO'
-
- else:
- print('WARN: ', files[i]['type'],
- ' is not a known type and you never should have landed here.')
def find_files(directory):
""" find files to build a dictionary out of """
+ log.debug(f'find_files({directory})')
os.system('clear')
for folder, subfolders, filename in os.walk(directory):
+ log.debug(f'{folder},{filename}')
for f_type in config['file_types']:
+ log.debug(f'Type: {f_type}')
for ext in config['file_types'][f_type]:
+ log.debug(f'Extension: {ext}')
+ os.system('clear')
for file in tqdm(filename,
desc = 'Finding ' + ext + ' Files in ' + folder):
+ log.debug(f'File: {file}')
if file.lower().endswith(ext):
current_file = os.path.join(folder,file)
+ log.debug(f'Current File: {current_file}')
if is_file(current_file):
- process_file(folder, f_type, file, ext)
+ log.debug(f'Is File: {current_file}')
+ log.debug(f'Call function: process_file({folder}, {file}, {EVENT}, {config})')
+ #process_file(folder, f_type, file, ext)
+ process_file(folder, file, EVENT, config)
else:
- print(f"Skipping {current_file} as it does not look like a real file.")
+ log.warn(f"Skipping {current_file} as it does not look like a real file.")
-def validate_config_dir_access():
- """ Validate we can operate in the defined directories """
- check = path_access_write(config['folders']['destination']['base'])
- if check is False:
- writable = False
- else:
- check = path_access_read(config['folders']['source']['base'])
- if check is False:
- writable = False
- else:
- if config['store_backup'] is True:
- check = path_access_write(config['folders']['backup'])
- if check is False:
- writable = False
- else:
- writable = True
- else:
- writable = True
- return writable
-
-def copy_files():
- """ Copy Files. """
- os.system('clear')
- for file in tqdm(files, desc = "Copying Files:"):
- create_folder(files[file]['folders']['destination'])
-
- copy_from_source(files[file]['folders']['source_path'],
- files[file]['folders']['destination'],
- files[file]['name'])
-
- if config['store_originals'] is True:
- if files[file]['type'] == 'image':
- create_folder(files[file]['folders']['destination_original'])
-
- copy_from_source(files[file]['folders']['destination'],
- files[file]['folders']['destination_original'],
- files[file]['name'])
-
-'''
-def gen_hashes():
- """ Generate Hashes """
- for file in tqdm(files, desc = "Generating MD5 Hashes:", ncols = 100):
- #print(files[file])
- files[file]['md5_checksums'] = {}
- for folder in files[file]['folders']:
- k = os.path.join(files[file]['folders'][folder], files[file]['name'])
- files[file]['md5_checksums'][k] = md5_hash(k)
-'''
-
-def gen_xxhashes():
- """ Generate xxHashes """
- os.system('clear')
- for file in tqdm(files, desc = "Generating xx Hashes:"):
- #print(files[file])
- files[file]['xx_checksums'] = {}
- for folder in files[file]['folders']:
- k = os.path.join(files[file]['folders'][folder], files[file]['name'])
- files[file]['xx_checksums'][k] = xx_hash(k)
- print(f"{k}: {files[file]['xx_checksums'][k]}")
-
-'''
-def validate_checksums():
- """ Validate Checksums """
- for file in tqdm(files, desc = "Verifying Checksums:", ncols = 100):
- i = 0
- c = {}
- for checksum in files[file]['md5_checksums']:
- c[i] = files[file]['md5_checksums'][checksum]
- if i > 0:
- p = i - 1
- if c[i] == c[p]:
- files[file]['source_cleanable'] = True
- else:
- files[file]['source_cleanable'] = False
- print(f'FATAL: Checksum validation failed for: \
- {files[file]["name"]} \n{c[i]}\n is not equal to \n{c[p]}\n')
- print('\n File Meta:\n')
- pprint(files[file])
- i = i + 1
-'''
-
-def validate_xx_checksums():
- """ Validate Checksums """
- os.system('clear')
- for file in tqdm(files, desc = "Verifying Checksums:"):
- i = 0
- c = {}
- for checksum in files[file]['xx_checksums']:
- c[i] = files[file]['xx_checksums'][checksum]
- if i > 0:
- p = i - 1
- if c[i] == c[p]:
- files[file]['source_cleanable'] = True
- else:
- files[file]['source_cleanable'] = False
- print(f'FATAL: Checksum validation failed for: \
- {files[file]["name"]} \n{c[i]}\n is not equal to \n{c[p]}\n')
- print('\n File Meta:\n')
- pprint(files[file])
- i = i + 1
-
-def cleanup_sd():
- """ If we should clean up the SD, nuke the copied files. """
- if config['cleanup_sd'] is True:
- os.system('clear')
- for file in tqdm(files, desc = "Cleaning Up SD:"):
- if files[file]['source_cleanable'] is True:
- os.remove(os.path.join(files[file]['folders']['source_path'],files[file]['name']))
-
-GO = validate_config_dir_access()
+GO = validate_config_dir_access(config)
if GO is True:
find_files(config['folders']['source']['base'])
- copy_files()
- gen_xxhashes()
- validate_xx_checksums()
- cleanup_sd()
+ copy_files(files,config)
+ gen_xxhashes(files)
+ validate_xx_checksums(files)
+ cleanup_sd(files,config)
else:
- print("There was a problem accessing one or more directories defined in the configuration.")
+ log.critical('There was a problem accessing one or more directories defined in the configuration.')
-dump_yaml(files, 'files_dict.yaml')
-print('done.')
+# dump_yaml(files, 'files_dict.yaml')
+log.info('done.')
diff --git a/lumberjack.py b/lumberjack.py
new file mode 100644
index 0000000..1b72ca5
--- /dev/null
+++ b/lumberjack.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python
+
+"""
+A class for logging... no, not timber
+"""
+
+import logging
+
+# class Logger(object):
+# level_relations = {
+# 'debug': logging.DEBUG,
+# 'info': logging.INFO,
+# 'warning': logging.WARNING,
+# 'error': logging.ERROR,
+# 'crit': logging.CRITICAL
+# } # relationship mapping
+#
+# def __init__(self, filename, level='info', when='D', backCount=3,
+# fmt='%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s'):
+# self.logger = logging.getLogger(filename)
+# format_str = logging.Formatter(fmt) # Setting the log format
+# self.logger.setLevel(self.level_relations.get(level)) # Setting the log level
+# console_handler = logging.StreamHandler() # on-screen output
+# console_handler .setFormatter(format_str) # Setting the format
+# th = logging.handlers.TimedRotatingFileHandler(filename=filename, when=when, backupCount=backCount,encoding='utf-8') # automatically generates the file at specified intervals
+# th.setFormatter(format_str) # Setting the format
+# self.logger.addHandler(console_handler) # Add the object to the logger
+# self.logger.addHandler(th)
+
+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.setLevel(logging.DEBUG)
+ file_handler.setFormatter(file_formatter)
+
+ console_handler = logging.StreamHandler()
+ console_handler.setLevel(logging.INFO)
+ console_handler.setFormatter(file_formatter)
+
+ logger = logging.getLogger(name)
+ logger.addHandler(file_handler)
+ logger.addHandler(console_handler)
+ logger.setLevel(logging.DEBUG)
+
+ return logger
diff --git a/media.py b/media.py
new file mode 100644
index 0000000..f70272c
--- /dev/null
+++ b/media.py
@@ -0,0 +1,170 @@
+#!/usr/bin/env python
+
+"""
+Create the media object
+"""
+import os
+from datetime import datetime
+import ffmpeg
+import sys
+
+#Local Imports
+from hashing import xx_hash, hash_path
+from get_image_tag import get_image_date
+# from configure import files
+from lumberjack import timber
+
+log = timber(__name__)
+
+class Media:
+ """ media class """
+
+ def __init__(self, source_path,event,config):
+ """ init """
+
+ self.configuration = config
+ self.source_path_dir = os.path.dirname(source_path)
+ self.source_path_hash = hash_path(source_path)
+ self.event_name = event
+ self.file_name = os.path.basename(source_path)
+ self.dotted_file_ext = os.path.splitext(self.file_name)[1].lower()
+ self.file_ext = self.dotted_file_ext.split('.')[1]
+ self.file_type = self.get_file_type()
+ self.capture_date = get_capture_date(source_path,self.file_type)
+ self.store_originals = config['store_originals']
+ self.destination_path = self.set_destination_path()
+ self.destination_originals_path = ''
+
+
+ self.filehash = '' # Only populate this one if it's called upon.
+
+ def set_destination_path(self):
+ """ set the destination path for the file """
+
+ p = self.configuration['folders']['destination']['base'] + '/' \
+ + self.capture_date[0] + '/' \
+ + self.capture_date[0] + '-' \
+ + self.capture_date[1] + '/' \
+ + self.capture_date[0] + '-' \
+ + self.capture_date[1] + '-' \
+ + self.capture_date[2]
+
+ if self.event_name:
+ p = p + '-' + self.event_name
+
+ if self.file_type == 'image':
+ p = p + '/PHOTO'
+ if self.file_ext.lower() in ('jpg', 'jpeg'):
+ if self.store_originals is True:
+ self.destination_originals_path = p + '/ORIGINALS/JPG'
+ p = p + '/JPG'
+ else:
+ if self.store_originals is True:
+ self.destination_originals_path = p + '/ORIGINALS/RAW'
+ p = p + '/RAW'
+ elif self.file_type == 'video':
+ p = p + '/VIDEO'
+ elif self.file_type == 'audio':
+ p = p + '/AUDIO'
+ else:
+ log.critical(f'{self.file_type} is not known. You should have never landed here.')
+
+ return p
+
+ def generate_hash(self):
+ """ generate the hash and store it to self.filehash """
+ return xx_hash(self.source_path_dir)
+
+ def set_hash(self):
+ """set the hash for the file """
+ self.filehash = self.generate_hash()
+
+ def get_hash(self):
+ """ a function to get return the hash """
+
+ if not self.filehash:
+ self.set_hash()
+
+ return self.filehash
+
+ def get_file_type(self):
+ """ determine if the extension is in the list of file types """
+ log.debug(f'get_file_type():')
+ for t in self.configuration['file_types']:
+ log.debug(f'Checking if file is part of {t}')
+
+ for e in self.configuration['file_types'][t]:
+ log.debug(f'Checking if {self.file_ext.lower()} ends with {e}')
+
+ if self.file_ext.lower().endswith(e):
+ log.debug(self.file_ext + ' ends with ' + e)
+ return t
+ else:
+ log.debug(self.file_ext + f' does not end with {e}')
+
+def get_capture_date(path, f_type):
+ """ get capture date from meta """
+ log.debug(f'get_capture_date({path}, {f_type}')
+
+ if f_type == 'image':
+ stamp = get_image_date(path)
+
+ elif f_type == 'video':
+ try:
+ stamp = datetime.strptime(
+ ffmpeg.probe(path)['format']['tags']['creation_time'],
+ '%Y-%m-%dT%H:%M:%S.%f%z')
+ except:
+ try:
+ stamp = datetime.fromtimestamp(os.path.getctime(path))
+ except:
+ try:
+ stamp = datetime.strptime(
+ str('1900:01:01 00:00:00'), '%Y:%m:%d %H:%M:%S')
+ except:
+ log.critical(f'\nCould not get timestamp for {path}. Giving up.')
+ sys.exit()
+
+ elif f_type == 'audio':
+ try:
+ stamp = datetime.strptime(ffmpeg.probe(
+ path)['format']['tags']['date'], '%Y-%m-%d')
+ except KeyError as ke:
+ log.warning(f'\nError: {ke} for {path}. Trying getctime...')
+ try:
+ stamp = datetime.fromtimestamp(os.path.getctime(path))
+ except:
+ log.critical(f'\nCould not get timestamp for {path}. Giving up.')
+ sys.exit()
+ else:
+ try:
+ stamp = datetime.fromtimestamp(os.path.getctime(path))
+ except:
+ log.critical(f'\nCould not get timestamp for {path}. Giving up.')
+ sys.exit()
+
+ 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
diff --git a/qodana.yaml b/qodana.yaml
new file mode 100644
index 0000000..76fb7ad
--- /dev/null
+++ b/qodana.yaml
@@ -0,0 +1,29 @@
+#-------------------------------------------------------------------------------#
+# Qodana analysis is configured by qodana.yaml file #
+# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
+#-------------------------------------------------------------------------------#
+version: "1.0"
+
+#Specify inspection profile for code analysis
+profile:
+ name: qodana.starter
+
+#Enable inspections
+#include:
+# - name:
+
+#Disable inspections
+#exclude:
+# - name:
+# paths:
+# -
+
+#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
+#bootstrap: sh ./prepare-qodana.sh
+
+#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
+#plugins:
+# - id: #(plugin id can be found at https://plugins.jetbrains.com)
+
+#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
+linter: jetbrains/qodana-:latest
diff --git a/raw_photo.py b/raw_photo.py
new file mode 100644
index 0000000..eed76e4
--- /dev/null
+++ b/raw_photo.py
@@ -0,0 +1,28 @@
+from rawpy import imread
+import imageio
+from rawpy._rawpy import LibRawNoThumbnailError, LibRawUnsupportedThumbnailError, ThumbFormat
+
+
+def extract_jpg_thumb(raw_file_path):
+
+ with imread(raw_file_path) as raw:
+ try:
+ thumb = raw.extract_thumb()
+ except (LibRawNoThumbnailError, LibRawUnsupportedThumbnailError):
+ return None
+
+ if thumb.format == ThumbFormat.JPEG:
+ with open('thumbnail.jpg', 'wb') as f:
+ f.write(thumb.data)
+ elif thumb.format == ThumbFormat.BITMAP:
+ imageio.imsave('thumbnail.jpg', thumb.data)
+ else:
+ return None
+
+ return 'thumbnail.jpg'
+
+# thumbnail_path = extract_jpg_thumb('your_raw_image.nef')
+# if thumbnail_path:
+# print("Thumbnail extracted to:", thumbnail_path)
+# else:
+# print("No thumbnail found or unsupported format.")
\ No newline at end of file
diff --git a/rename.py b/rename.py
new file mode 100755
index 0000000..47333c8
--- /dev/null
+++ b/rename.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python
+
+import argparse
+import sys
+import os
+
+from bitmover import copy_from_source
+from file_stuff import path_exists, cmp_files
+from get_image_tag import get_image_date, get_exif_tag
+from hashing import xx_hash
+
+parser = argparse.ArgumentParser()
+parser.add_argument("-f", "--folder", help = "folder with files to rename")
+parser.add_argument("-o", "--keeporiginalname", help = "keeps the original name attached to the file.")
+parser.add_argument("-d", "--dryrun", help = "dry run, no action")
+
+args = parser.parse_args()
+
+if args.folder:
+ FOLDER = args.folder
+else:
+ print("you need to specify a folder.")
+ sys.exit()
+if args.dryrun:
+ dry_run = True
+else:
+ dry_run = False
+
+if args.keeporiginalname:
+ keep_orig_name = True
+else:
+ keep_orig_name = False
+
+def get_file_size(f):
+ return os.path.getsize(f)
+
+for file in os.listdir(FOLDER):
+ if file.lower().endswith("gif"):
+ copy_from_source(FOLDER,'/Volumes/VIDEO_ARRAY_01/Multimedia/gif',file)
+ file_match = cmp_files(os.path.join(FOLDER,file),
+ os.path.join('/Volumes/VIDEO_ARRAY_01/Multimedia/gif',file))
+
+ if file_match is True:
+ os.remove(os.path.join(FOLDER,file))
+
+ if file.lower().endswith("png"):
+ copy_from_source(FOLDER,'/Volumes/VIDEO_ARRAY_01/Multimedia/png',file)
+
+ file_match = cmp_files(os.path.join(FOLDER, file),
+ os.path.join('/Volumes/VIDEO_ARRAY_01/Multimedia/gif', file))
+
+ if file_match is True:
+ os.remove(os.path.join(FOLDER, file))
+
+ if file.lower().endswith("heic"):
+ copy_from_source(FOLDER,'/Volumes/VIDEO_ARRAY_01/Multimedia/heic',file)
+
+ file_match = cmp_files(os.path.join(FOLDER, file),
+ os.path.join('/Volumes/VIDEO_ARRAY_01/Multimedia/heic', file))
+
+ if file_match is True:
+ os.remove(os.path.join(FOLDER, file))
+
+ if file.lower().endswith("jpg") or \
+ file.lower().endswith("jpeg") or \
+ file.lower().endswith("nef") or \
+ file.lower().endswith("rw2") or \
+ file.lower().endswith("arw") or \
+ file.lower().endswith("dng"):
+
+ old_path = os.path.join(FOLDER,file)
+
+ lowered_name = file.lower()
+ file_size = get_file_size(old_path)
+ file_extension = os.path.splitext(lowered_name)[1]
+ image_date = get_image_date(old_path)
+ image_date_year = image_date.strftime("%Y")
+ image_date_month = image_date.strftime("%m")
+ image_date_day = image_date.strftime("%d")
+ image_date_hour = image_date.strftime("%H")
+ image_date_minute = image_date.strftime("%M")
+ image_date_second = image_date.strftime("%S")
+ image_date_microsecond = image_date.strftime("%f")
+ image_date_subsecond = str(get_exif_tag(old_path,'EXIF SubSec'))
+ image_hash = xx_hash(old_path)
+
+ if image_date_subsecond:
+ subsecond_desired_length = 6
+
+ if image_date_subsecond == 'None':
+ image_date_subsecond = subsecond_desired_length*str('0')
+
+ l_image_date_subsecond = len(image_date_subsecond)
+
+ if subsecond_desired_length > l_image_date_subsecond:
+ pad = subsecond_desired_length - l_image_date_subsecond
+ image_date_subsecond = image_date_subsecond + pad*str("0")
+
+ new_name = f'{image_date_year}-{image_date_month}-{image_date_day}-{image_date_hour}{image_date_minute}{image_date_second}{image_date_subsecond}_{file_size}_{image_hash}'
+ else:
+ new_name = f'{image_date_year}-{image_date_month}-{image_date_day}-{image_date_hour}{image_date_minute}{image_date_second}000_{file_size}_{image_hash}'
+
+ if keep_orig_name is True:
+ new_file_name = f'{new_name}-{lowered_name}'
+ else:
+ new_file_name = f'{new_name}{file_extension}'
+
+ new_path = os.path.join(FOLDER,new_file_name)
+
+ if path_exists(new_path):
+ print(f"{new_path} exists.. skipping.")
+ elif dry_run is True:
+ print(f'Dry run: {old_path} becomes {new_path}')
+ else:
+ print(f'Renaming {old_path} to: {new_path}')
+ os.rename(old_path,new_path)
\ No newline at end of file
diff --git a/import_media_ui.py b/thread_my_stuff.py
similarity index 100%
rename from import_media_ui.py
rename to thread_my_stuff.py
diff --git a/ui_main_widgets.py b/ui_main_widgets.py
new file mode 100644
index 0000000..e58c795
--- /dev/null
+++ b/ui_main_widgets.py
@@ -0,0 +1,99 @@
+from PyQt6.QtCore import QSize, Qt
+from PyQt6.QtGui import QPixmap
+from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton, QVBoxLayout, QLabel, QLineEdit, QWidget, \
+ QHBoxLayout, QListWidget
+from ui_dialogues import select_directory, update_textbox_from_path
+
+
+def create_layout1():
+ layout1 = QVBoxLayout()
+ layout1_1 = QHBoxLayout()
+ layout1_2 = QHBoxLayout()
+ layout1_3 = QHBoxLayout()
+
+ ### Spacing and Alignment
+ layout1.setContentsMargins(10, 10, 10, 10)
+ layout1.setSpacing(10)
+ layout1.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
+ layout1_1.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
+ layout1_2.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
+ layout1_3.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
+
+ return layout1,layout1_1,layout1_2,layout1_3
+
+def layout1_widgets(config):
+ l1,l1_1,l1_2,l1_3 = create_layout1()
+
+ ### Create Top Row
+ lbl_src_directory = QLabel('Source Directory')
+ txt_box_src_directory = QLineEdit()
+ txt_box_src_directory.setPlaceholderText(config['folders']['source']['base'])
+
+ btn_src_browse = QPushButton('Browse')
+ btn_src_browse.setFixedSize(100, 40)
+ btn_src_browse.clicked.connect(update_textbox_from_path(
+ txt_box_src_directory,
+ config['folders']['source']['base']))
+
+ ### Create Second Row
+ lbl_dst_directory = QLabel('Destination Directory')
+ txt_box_dst_directory = QLineEdit()
+ txt_box_dst_directory.setPlaceholderText(config['folders']['destination']['base'])
+
+ btn_dst_browse = QPushButton('Browse')
+ btn_dst_browse.setFixedSize(100, 40)
+ btn_dst_browse.clicked.connect(update_textbox_from_path(
+ txt_box_dst_directory,
+ config['folders']['destination']['base']))
+
+ ### Create Third Row
+ btn_scan_dir = QPushButton("Scan Directory")
+ btn_scan_dir.setFixedSize(100, 40)
+ btn_scan_dir.setCheckable(False)
+
+ ### Add Widgets to Layouts
+ l1_1.addWidget(lbl_src_directory)
+ l1_1.addWidget(txt_box_src_directory)
+ l1_1.addWidget(btn_src_browse)
+ l1_2.addWidget(lbl_dst_directory)
+ l1_2.addWidget(txt_box_dst_directory)
+ l1_2.addWidget(btn_dst_browse)
+ l1_3.addWidget(btn_scan_dir)
+
+ ### Add Layouts to primary Layout 1
+ l1.addLayout(l1_1)
+ l1.addLayout(l1_2)
+ l1.addLayout(l1_3)
+
+ return l1
+
+def create_layout2():
+ layout2 = QHBoxLayout()
+ layout2_1 = QHBoxLayout()
+ layout2_2 = QVBoxLayout()
+ layout2_2_1 = QHBoxLayout()
+ layout2_2_2 = QVBoxLayout()
+
+ return layout2,layout2_1,layout2_2,layout2_2_1,layout2_2_2
+
+def layout2_widgets():
+ l2,l2_1,l2_2,l2_2_1,l2_2_2 = create_layout2()
+ preview_width = int(220)
+ preview_height = int(preview_width * 0.75)
+
+ src_file_list = QListWidget()
+
+ src_file_list.setFixedWidth(550)
+
+ preview_placeholder = QLabel("Placeholder")
+ preview_placeholder.setPixmap(QPixmap('assets/preview_placeholder.jpg'))
+ preview_placeholder.setScaledContents(True)
+ preview_placeholder.setFixedWidth(preview_width)
+ preview_placeholder.setFixedHeight(preview_height)
+
+ l2_1.addWidget(src_file_list)
+ l2_2.addWidget(preview_placeholder)
+ l2_1.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
+ l2_2.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
+ l2.addLayout(l2_1)
+ l2.addLayout(l2_2)
\ No newline at end of file