Compare commits

...

10 Commits

Author SHA1 Message Date
Kameron Kenny fc32855c86
push 2024-09-15 20:48:11 -04:00
Kameron Kenny dd35353068
change hashing method, update progress by size where it makes sense. 2024-08-20 22:13:20 -04:00
Kameron Kenny dc9458a890
config example 2024-08-20 11:03:38 -04:00
Kameron Kenny 827f057989
add logic for EXIF timestamp not being consistent 2024-08-20 11:02:59 -04:00
Kameron Kenny 25fcfdbb27 fix event if arg is missing. 2023-08-16 10:48:31 -04:00
Kameron Kenny 300b1c9ef0 add argument parsing plus refactoring 2023-08-15 10:52:43 -04:00
Kameron Kenny ef2ca3d70d define dump_yaml 2023-08-10 16:32:35 -04:00
Kameron Kenny 912ef08783 directory stuff 2023-08-10 14:52:27 -04:00
Kameron Kenny c23164d428 refactoring and add tqdm 2023-08-10 11:21:13 -04:00
Kameron Kenny c884753f09 get date from metadata, implement sd card cleanup. 2023-08-08 11:33:16 -04:00
22 changed files with 2168 additions and 171 deletions

6
.gitignore vendored
View File

@ -1,3 +1,9 @@
*.swp
*.orig
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'])

26
config.yaml.EXAMPLE Normal file
View File

@ -0,0 +1,26 @@
---
folders:
destination:
base: '/path/to/DESTINATION'
source:
base: '/path/to/SD_CARD'
originals: 'Originals' # Concatenated onto destination folder to keep a local 'clean' copy
backup: '/Volumes/Multimedia' # A path to a separate disk or NAS to store a backup copy
store_originals: TRUE
store_backup: TRUE
cleanup_sd: FALSE # Delete files from the SD after checksum validation
file_types:
image:
- 'jpg'
- 'jpeg'
- 'raw'
- 'dng'
- 'rw2'
- 'arw'
- 'nef'
video:
- 'mov'
- 'mp4'
audio:
- 'wav'

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

254
import_media.py Normal file → Executable file
View File

@ -1,194 +1,110 @@
#!/usr/bin/env python3
#!/usr/bin/env python
'''
Import photos from SD card into folder with todays date + nickname
Use: importphotos (--jpg|--raw|--both) <nickname of folder (optional)>
"""
Import photos from SD card into folder with today's date + nickname
Use: import_media.py (--jpg|--raw|--both) <nickname of folder (optional)>
Add script to path
'''
'''
TODO:
1. Import configuration from config file
2. Set raw file extension based on camera specified in configuration
3. Create destination folders based on concatination of configuration,
metadata, and event name passed from ARG
4. Create destination sub-folder based on filetype
5. Copy files to appropriate folder
6. Compare files from source
7. Create 'originals' with copy of files from destination after
checksum for photos only
8. Optinally allow specification of a backup location on another disk
8. Optionally allow specification of a backup location on another disk
or NAS to ship a 3rd copy to
9. Optionally cleanup SD only after checksum matching
10. Every config option has an arg override
11. Optionally rename file if event name was passed in
11. Optionally rename file if EVENT name was passed in
-- STRETCH --
12. Make a graphical interface
'''
"""
import os
import sys
import yaml
import argparse
import shutil
import hashlib
from datetime import datetime
import exifread
import os
from tqdm import tqdm
config_file = 'config.yaml'
### 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 f:
config = yaml.load(f, Loader=yaml.FullLoader)
except FileNotFoundError:
print("Configuration file not found: ", config_file)
print("Copy config.yaml.EXAMPLE to ", config_file, " and update accordingly.")
c = Configure(CONFIG_FILE)
config = c.load_config()
log = timber(__name__)
log.info("Starting import_media")
''' Parse Arguments '''
parser = argparse.ArgumentParser()
parser.add_argument("-e", "--event", help = "Event Name")
parser.add_argument("-s", "--source", help = "Source Directory to search for files")
parser.add_argument("-d", "--destination", help = "Destination Directory to put files")
parser.add_argument("-o", "--create-originals", help = "For images only, create an originals \
folder for safe keeping")
parser.add_argument("-b", "--backup-destination", help = "Create a backup of everything at the \
specified location")
parser.add_argument("-D", "--delete-source-files", help = "Delete files from SD after validating \
checksum of copied files")
parser.add_argument("-v", "--verify", help = "[True|False] Verify the checksum of \
the copied file")
parser.add_argument("-c", "--config", help = "Load the specified config file instead \
of the default " + CONFIG_FILE)
parser.add_argument("-g", "--generate-config", help = "Generate config file based on options \
passed from command arguments")
args = parser.parse_args()
if args.event:
event = args.event
def md5_hash(f):
print("calculating md5 for ", f)
md5 = hashlib.md5(open(f, 'rb').read()).hexdigest()
return md5
def cmp_files(f1,f2):
print('comparing md5 hashes...')
return md5_hash(f1) == md5_hash(f2)
def file_classification(f):
print('Classifying media for: ', f)
for classification in config['file_types']:
for ext in config['file_types'][classification]:
if f.lower().endswith(ext):
c = classification
return classification
def get_capture_date(p, t):
if t == 'image':
with open(p, 'rb') as f:
tags = exifread.process_file(f)
captured = tags['EXIF DateTimeOriginal']
year = str(captured).split(' ')[0].split(':')[0]
month = str(captured).split(' ')[0].split(':')[1]
day = str(captured).split(' ')[0].split(':')[2]
EVENT = args.event
else:
stamp = datetime.fromtimestamp(os.path.getctime(p))
year = stamp.strftime("%Y")
month = stamp.strftime("%m")
day = stamp.strftime("%d")
return year, month, day
def create_folder(f):
try:
os.makedirs(f)
except FileExistsError as exists:
print()
def copy_from_source(p, dest_folder, dest_orig_folder, file):
if os.path.exists(os.path.join(dest_folder, file)):
check_match = cmp_files(p, os.path.join(dest_folder, file))
if check_match == False:
base, extension = os.path.splitext(file)
file_name_hash = base + '_' + md5_hash(os.path.join(dest_folder, file)) + extension
os.rename(os.path.join(dest_folder, file), os.path.join(dest_folder, file_name_hash))
shutil.copy(p, dest_folder)
check_match = cmp_files(p, dest_folder + '/' + file)
if check_match == False:
print(f'CRITICAL: md5 hash does not match for {file}')
print(p, ': ', md5_hash(p))
print(dest_folder + '/' + file, ': ', md5_hash(dest_folder + '/' + file))
exit
if dest_orig_folder != False:
shutil.copy(dest_folder + '/' + file, dest_orig_folder)
check_match = cmp_files(dest_folder + '/' + file, dest_orig_folder + '/' + file)
if check_match == False:
print(f'CRITICAL: md5 hash does not match for {file}')
print(dest_folder + '/' + file, ': ', md5_hash(dest_folder + '/' + file))
print(dest_orig_folder + '/' + file, ': ', md5_hash(dest_orig_folder + '/' + file))
exit
else:
shutil.copy(p, dest_folder)
check_match = cmp_files(p, dest_folder + '/' + file)
if check_match == False:
print(f'CRITICAL: md5 hash does not match for {file}')
print(p, ': ', md5_hash(p))
print(dest_folder + '/' + file, ': ', md5_hash(dest_folder + '/' + file))
exit
if dest_orig_folder != False:
shutil.copy(dest_folder + '/' + file, dest_orig_folder)
check_match = cmp_files(dest_folder + '/' + file, dest_orig_folder + '/' + file)
if check_match == False:
print(f'CRITICAL: md5 hash does not match for {file}')
print(dest_folder + '/' + file, ': ', md5_hash(dest_folder + '/' + file))
print(dest_orig_folder + '/' + file, ': ', md5_hash(dest_orig_folder + '/' + file))
exit
def process_file(p, t, file, ext):
capture_date = get_capture_date(p, t)
y = capture_date[0]
m = capture_date[1]
d = capture_date[2]
if event:
dest_folder = config['folders']['destination']['base'] + '/' + y + '/' + y + '-' + m + '/' + y + '-' + m + '-' + d + '-' + event
else:
dest_folder = config['folders']['destination']['base'] + '/' + y + '/' + y + '-' + m + '/' + y + '-' + m + '-' + d
if t == 'image':
dest_folder = dest_folder + '/photos'
if config['store_originals'] == True:
dest_orig_folder = dest_folder + '/ORIGINALS'
if ext in ('jpg', 'jpeg'):
dest_folder = dest_folder + '/JPG'
if dest_orig_folder:
dest_orig_folder = dest_orig_folder + '/JPG'
else:
dest_folder = dest_folder + '/RAW'
if dest_orig_folder:
dest_orig_folder = dest_orig_folder + '/RAW'
elif t == 'video':
dest_folder = dest_folder + '/VIDEO'
elif t == 'audio':
dest_folder = dest_folder + '/AUDIO'
else:
print(f'WARN: {t} is not a known type and you never should have landed here.')
create_folder(dest_folder)
try:
dest_orig_folder
except NameError:
dest_orig_folder = False
else:
create_folder(dest_orig_folder)
copy_from_source(p, dest_folder, dest_orig_folder, file)
EVENT = False
if args.source:
config['folders']['source']['base'] = args.source
if args.destination:
config['folders']['destination']['base'] = args.source
#if args.create-originals:
# pass
#if args.backup-destination:
# pass
#if args.delete-source-files:
# pass
#if args.config:
# pass
#if args.generate-config:
# pass
def file_list(directory):
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):
for t in config['file_types']:
for ext in config['file_types'][t]:
for file in filename:
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):
p = folder + '/' + file
process_file(p, t, file, ext)
current_file = os.path.join(folder,file)
log.debug(f'Current File: {current_file}')
if is_file(current_file):
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:
log.warn(f"Skipping {current_file} as it does not look like a real file.")
file_list(config['folders']['source']['base'])
print('done.')
GO = validate_config_dir_access(config)
if GO is True:
find_files(config['folders']['source']['base'])
copy_files(files,config)
gen_xxhashes(files)
validate_xx_checksums(files)
cleanup_sd(files,config)
else:
log.critical('There was a problem accessing one or more directories defined in the configuration.')
# dump_yaml(files, 'files_dict.yaml')
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)

0
thread_my_stuff.py Normal file
View File

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)