This commit is contained in:
Kameron Kenny 2024-09-15 20:48:11 -04:00
parent dd35353068
commit fc32855c86
No known key found for this signature in database
GPG Key ID: E5006629839D2276
21 changed files with 2087 additions and 401 deletions

6
.gitignore vendored
View File

@ -1,3 +1,9 @@
*.swp *.swp
*.orig *.orig
config.yaml config.yaml
.DS_Store
.idea
__pycache__
log
thumbnail.jpg
files_dict.yaml

418
BitMover.ui Normal file
View File

@ -0,0 +1,418 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1473</width>
<height>928</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<widget class="QWidget" name="gridLayoutWidget">
<property name="geometry">
<rect>
<x>20</x>
<y>10</y>
<width>871</width>
<height>121</height>
</rect>
</property>
<layout class="QGridLayout" name="grid_dir_selector">
<item row="1" column="0">
<widget class="QLabel" name="label_1_src_dir">
<property name="text">
<string>Source Directory</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="pushButton_src_browse">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="lineEdit_dst_dir">
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2_dst_dir">
<property name="text">
<string>Destination Directory</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="lineEdit_src_dir">
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QPushButton" name="pushButton_dst_browse">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QPushButton" name="pushButton_3_scan_dir">
<property name="baseSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="font">
<font>
<pointsize>16</pointsize>
</font>
</property>
<property name="text">
<string>Scan Directory</string>
</property>
<property name="icon">
<iconset theme="edit-find">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="checkable">
<bool>false</bool>
</property>
<property name="flat">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QListWidget" name="file_list">
<property name="geometry">
<rect>
<x>20</x>
<y>150</y>
<width>871</width>
<height>701</height>
</rect>
</property>
</widget>
<widget class="QWidget" name="gridLayoutWidget_2">
<property name="geometry">
<rect>
<x>910</x>
<y>580</y>
<width>551</width>
<height>251</height>
</rect>
</property>
<layout class="QGridLayout" name="grid_metadata" columnstretch="0,1">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="horizontalSpacing">
<number>20</number>
</property>
<item row="6" column="0">
<widget class="QLabel" name="l_camera">
<property name="text">
<string>Camera</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="l_iso">
<property name="text">
<string>ISO</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="label_data_width_height">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="l_date_time_created">
<property name="text">
<string>Date / Time Created</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="l_lens">
<property name="text">
<string>Lens</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="l_dpi">
<property name="text">
<string>Resolution (DPI)</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="l_aperture">
<property name="text">
<string>Aperture</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLabel" name="label_data_iso">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLabel" name="label_data_aperture">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QLabel" name="label_data_lens">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="l_megapixels">
<property name="text">
<string>Megapixels</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLabel" name="label_data_megapixels">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="l_width_height">
<property name="text">
<string>Width / Height</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label_data_date_time_created">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QLabel" name="label_data_camera">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="l_zoom">
<property name="text">
<string>Zoom</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QLabel" name="label_data_zoom">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="label_data_dpi">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QLabel" name="l_exif_ffprobe_title">
<property name="geometry">
<rect>
<x>910</x>
<y>550</y>
<width>371</width>
<height>16</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>18</pointsize>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Exif / ffprobe Data</string>
</property>
</widget>
<widget class="QWidget" name="gridLayoutWidget_3">
<property name="geometry">
<rect>
<x>910</x>
<y>10</y>
<width>541</width>
<height>71</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<widget class="QLineEdit" name="eventName"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="labelEvent">
<property name="text">
<string>Event Label</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="gridLayoutWidget_4">
<property name="geometry">
<rect>
<x>910</x>
<y>90</y>
<width>221</width>
<height>41</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_2" rowstretch="0" columnstretch="0,1">
<item row="0" column="0">
<widget class="QLabel" name="label_3">
<property name="font">
<font>
<pointsize>18</pointsize>
</font>
</property>
<property name="text">
<string>Files Found</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLCDNumber" name="lcd_files_found"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="gridLayoutWidget_5">
<property name="geometry">
<rect>
<x>1140</x>
<y>90</y>
<width>311</width>
<height>46</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="1">
<widget class="QProgressBar" name="progressBar_overall">
<property name="value">
<number>24</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Current Progress</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Overall Progress</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QProgressBar" name="progressBar_current">
<property name="value">
<number>24</number>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QLabel" name="img_preview">
<property name="geometry">
<rect>
<x>910</x>
<y>150</y>
<width>541</width>
<height>371</height>
</rect>
</property>
<property name="autoFillBackground">
<bool>true</bool>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="text">
<string/>
</property>
</widget>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1473</width>
<height>24</height>
</rect>
</property>
<widget class="QMenu" name="menuBit_Mover">
<property name="title">
<string>Bit Mover</string>
</property>
</widget>
<addaction name="menuBit_Mover"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<action name="actionScan_Directory">
<property name="text">
<string>Scan Directory</string>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>

231
BitMover_MainWindow.py Normal file
View File

@ -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"))

312
BitMover_ui.py Executable file
View File

@ -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()

BIN
assets/forklift.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

85
bitmover.py Normal file
View File

@ -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'])

31
configure.py Normal file
View File

@ -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

92
dedup.py Executable file
View File

@ -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']))}")

78
dedup_wrapper.sh Executable file
View File

@ -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

143
file_stuff.py Normal file
View File

@ -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

98
get_image_tag.py Normal file
View File

@ -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

74
hashing.py Normal file
View File

@ -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

View File

@ -14,30 +14,22 @@ TODO:
12. Make a graphical interface 12. Make a graphical interface
""" """
import os
import sys
from pprint import pprint
import argparse import argparse
import shutil import os
import hashlib
import xxhash
from datetime import datetime
from tqdm import tqdm from tqdm import tqdm
import yaml
import exifread
import ffmpeg
CONFIG_FILE = 'config.yaml' ### Local Imports
files = {} 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 c = Configure(CONFIG_FILE)
try: config = c.load_config()
with open(CONFIG_FILE, 'r') as cf: log = timber(__name__)
config = yaml.load(cf, Loader=yaml.FullLoader) log.info("Starting import_media")
except FileNotFoundError:
print("Configuration file not found: ", CONFIG_FILE)
print("Copy config.yaml.EXAMPLE to ", CONFIG_FILE, " and update accordingly.")
sys.exit()
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("-e", "--event", help = "Event Name") parser.add_argument("-e", "--event", help = "Event Name")
@ -77,403 +69,42 @@ if args.destination:
#if args.generate-config: #if args.generate-config:
# pass # 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): def find_files(directory):
""" find files to build a dictionary out of """ """ find files to build a dictionary out of """
log.debug(f'find_files({directory})')
os.system('clear') os.system('clear')
for folder, subfolders, filename in os.walk(directory): for folder, subfolders, filename in os.walk(directory):
log.debug(f'{folder},{filename}')
for f_type in config['file_types']: for f_type in config['file_types']:
log.debug(f'Type: {f_type}')
for ext in config['file_types'][f_type]: for ext in config['file_types'][f_type]:
log.debug(f'Extension: {ext}')
os.system('clear')
for file in tqdm(filename, for file in tqdm(filename,
desc = 'Finding ' + ext + ' Files in ' + folder): desc = 'Finding ' + ext + ' Files in ' + folder):
log.debug(f'File: {file}')
if file.lower().endswith(ext): if file.lower().endswith(ext):
current_file = os.path.join(folder,file) current_file = os.path.join(folder,file)
log.debug(f'Current File: {current_file}')
if is_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: 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(): GO = validate_config_dir_access(config)
""" 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()
if GO is True: if GO is True:
find_files(config['folders']['source']['base']) find_files(config['folders']['source']['base'])
copy_files() copy_files(files,config)
gen_xxhashes() gen_xxhashes(files)
validate_xx_checksums() validate_xx_checksums(files)
cleanup_sd() cleanup_sd(files,config)
else: 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') # dump_yaml(files, 'files_dict.yaml')
print('done.') log.info('done.')

45
lumberjack.py Normal file
View File

@ -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

170
media.py Normal file
View File

@ -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

29
qodana.yaml Normal file
View File

@ -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: <SomeEnabledInspectionId>
#Disable inspections
#exclude:
# - name: <SomeDisabledInspectionId>
# paths:
# - <path/where/not/run/inspection>
#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> #(plugin id can be found at https://plugins.jetbrains.com)
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
linter: jetbrains/qodana-<linter>:latest

28
raw_photo.py Normal file
View File

@ -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.")

116
rename.py Executable file
View File

@ -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)

99
ui_main_widgets.py Normal file
View File

@ -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)