Merge pull request 'dev' (#1) from dev into main

Reviewed-on: https://git.thelinuxpro.net/Utilities/BitMover/pulls/1
This commit is contained in:
kkenny 2024-10-07 14:51:00 -04:00
commit aa5a1ec148
42 changed files with 2227 additions and 1206 deletions

BIN
1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 KiB

BIN
2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 657 KiB

BIN
3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 KiB

BIN
4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 KiB

BIN
5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

View File

@ -11,7 +11,14 @@
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string>MainWindow</string> <string>BitMover</string>
</property>
<property name="windowIcon">
<iconset theme="applications-science">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="unifiedTitleAndToolBarOnMac">
<bool>true</bool>
</property> </property>
<widget class="QWidget" name="centralwidget"> <widget class="QWidget" name="centralwidget">
<widget class="QWidget" name="gridLayoutWidget"> <widget class="QWidget" name="gridLayoutWidget">
@ -88,8 +95,8 @@
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>910</x> <x>910</x>
<y>650</y> <y>610</y>
<width>311</width> <width>541</width>
<height>211</height> <height>211</height>
</rect> </rect>
</property> </property>
@ -292,7 +299,7 @@
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>910</x> <x>910</x>
<y>630</y> <y>590</y>
<width>371</width> <width>371</width>
<height>16</height> <height>16</height>
</rect> </rect>
@ -334,11 +341,55 @@
<rect> <rect>
<x>910</x> <x>910</x>
<y>50</y> <y>50</y>
<width>182</width> <width>541</width>
<height>91</height> <height>41</height>
</rect> </rect>
</property> </property>
<layout class="QGridLayout" name="gridLayout_2" rowstretch="0,0" columnstretch="0,1"> <layout class="QGridLayout" name="gridLayout_2" rowstretch="0" columnstretch="0,0,0,0,0">
<item row="0" column="2">
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Maximum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>180</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="3">
<widget class="QLabel" name="label_4">
<property name="font">
<font>
<pointsize>18</pointsize>
</font>
</property>
<property name="text">
<string>Files Imported</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLCDNumber" name="lcd_files_found">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
</widget>
</item>
<item row="0" column="0"> <item row="0" column="0">
<widget class="QLabel" name="label_3"> <widget class="QLabel" name="label_3">
<property name="font"> <property name="font">
@ -351,120 +402,25 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="1"> <item row="0" column="4">
<widget class="QLCDNumber" name="lcd_files_found"/> <widget class="QLCDNumber" name="lcd_files_imported">
</item> <property name="sizePolicy">
<item row="1" column="1"> <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<widget class="QLCDNumber" name="lcd_files_imported"/> <horstretch>0</horstretch>
</item> <verstretch>0</verstretch>
<item row="1" column="0"> </sizepolicy>
<widget class="QLabel" name="label_4">
<property name="font">
<font>
<pointsize>18</pointsize>
</font>
</property> </property>
<property name="text"> <property name="minimumSize">
<string>Files Imported</string> <size>
<width>80</width>
<height>0</height>
</size>
</property> </property>
</widget> <property name="maximumSize">
</item> <size>
</layout> <width>80</width>
</widget> <height>16777215</height>
<widget class="QWidget" name="gridLayoutWidget_5"> </size>
<property name="geometry">
<rect>
<x>1100</x>
<y>50</y>
<width>351</width>
<height>93</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="2" column="1">
<widget class="QProgressBar" name="progressBar_importing_2">
<property name="value">
<number>24</number>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLCDNumber" name="lcd_import_progress">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QLCDNumber" name="lcd_current_file_progress">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="l_proecessing_progress">
<property name="text">
<string>Processing Progress</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="l_current_file_progress">
<property name="text">
<string>Current File Progress</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QProgressBar" name="progressBar_processing">
<property name="value">
<number>24</number>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QProgressBar" name="progressBar_importing">
<property name="value">
<number>24</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="l_import_progress">
<property name="text">
<string>Import Progress</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLCDNumber" name="lcd_processing_progress">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QLabel" name="label_2">
<property name="text">
<string>%</string>
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QLabel" name="label_5">
<property name="text">
<string>%</string>
</property>
</widget>
</item>
<item row="2" column="3">
<widget class="QLabel" name="label_6">
<property name="text">
<string>%</string>
</property> </property>
</widget> </widget>
</item> </item>
@ -474,7 +430,7 @@
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>910</x> <x>910</x>
<y>150</y> <y>110</y>
<width>541</width> <width>541</width>
<height>371</height> <height>371</height>
</rect> </rect>
@ -493,7 +449,7 @@
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>910</x> <x>910</x>
<y>530</y> <y>490</y>
<width>541</width> <width>541</width>
<height>91</height> <height>91</height>
</rect> </rect>
@ -643,7 +599,8 @@
<string>Import Media</string> <string>Import Media</string>
</property> </property>
<property name="icon"> <property name="icon">
<iconset theme="drive-harddisk"/> <iconset theme="drive-harddisk">
<normaloff>.</normaloff>.</iconset>
</property> </property>
</widget> </widget>
</item> </item>
@ -685,7 +642,8 @@
<string>Images</string> <string>Images</string>
</property> </property>
<property name="icon"> <property name="icon">
<iconset theme="camera-photo"/> <iconset theme="camera-photo">
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="checked"> <property name="checked">
<bool>true</bool> <bool>true</bool>
@ -698,7 +656,8 @@
<string>Video</string> <string>Video</string>
</property> </property>
<property name="icon"> <property name="icon">
<iconset theme="camera-video"/> <iconset theme="camera-video">
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="checked"> <property name="checked">
<bool>true</bool> <bool>true</bool>
@ -711,7 +670,8 @@
<string>Audio</string> <string>Audio</string>
</property> </property>
<property name="icon"> <property name="icon">
<iconset theme="multimedia-player"/> <iconset theme="multimedia-player">
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="checked"> <property name="checked">
<bool>true</bool> <bool>true</bool>
@ -755,7 +715,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>1463</width> <width>1463</width>
<height>24</height> <height>21</height>
</rect> </rect>
</property> </property>
<widget class="QMenu" name="menuBit_Mover"> <widget class="QMenu" name="menuBit_Mover">

File diff suppressed because it is too large Load Diff

5
README.md Normal file
View File

@ -0,0 +1,5 @@
[![1](1.png)](1.png)
[![2](2.png)](2.png)
[![3](3.png)](3.png)
[![4](4.png)](4.png)
[![5](5.png)](5.png)

View File

@ -13,6 +13,9 @@ class Ui_MainWindow(object):
def setupUi(self, MainWindow): def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow") MainWindow.setObjectName("MainWindow")
MainWindow.resize(1463, 928) MainWindow.resize(1463, 928)
icon = QtGui.QIcon.fromTheme("applications-science")
MainWindow.setWindowIcon(icon)
MainWindow.setUnifiedTitleAndToolBarOnMac(True)
self.centralwidget = QtWidgets.QWidget(parent=MainWindow) self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
self.centralwidget.setObjectName("centralwidget") self.centralwidget.setObjectName("centralwidget")
self.gridLayoutWidget = QtWidgets.QWidget(parent=self.centralwidget) self.gridLayoutWidget = QtWidgets.QWidget(parent=self.centralwidget)
@ -47,7 +50,7 @@ class Ui_MainWindow(object):
self.file_list.setGeometry(QtCore.QRect(20, 160, 871, 701)) self.file_list.setGeometry(QtCore.QRect(20, 160, 871, 701))
self.file_list.setObjectName("file_list") self.file_list.setObjectName("file_list")
self.gridLayoutWidget_2 = QtWidgets.QWidget(parent=self.centralwidget) self.gridLayoutWidget_2 = QtWidgets.QWidget(parent=self.centralwidget)
self.gridLayoutWidget_2.setGeometry(QtCore.QRect(910, 650, 311, 211)) self.gridLayoutWidget_2.setGeometry(QtCore.QRect(910, 610, 541, 211))
self.gridLayoutWidget_2.setObjectName("gridLayoutWidget_2") self.gridLayoutWidget_2.setObjectName("gridLayoutWidget_2")
self.grid_metadata = QtWidgets.QGridLayout(self.gridLayoutWidget_2) self.grid_metadata = QtWidgets.QGridLayout(self.gridLayoutWidget_2)
self.grid_metadata.setContentsMargins(0, 0, 0, 0) self.grid_metadata.setContentsMargins(0, 0, 0, 0)
@ -154,7 +157,7 @@ class Ui_MainWindow(object):
self.grid_metadata.addWidget(self.l_meta_content_02, 2, 1, 1, 1) self.grid_metadata.addWidget(self.l_meta_content_02, 2, 1, 1, 1)
self.grid_metadata.setColumnStretch(1, 1) self.grid_metadata.setColumnStretch(1, 1)
self.l_exif_ffprobe_title = QtWidgets.QLabel(parent=self.centralwidget) self.l_exif_ffprobe_title = QtWidgets.QLabel(parent=self.centralwidget)
self.l_exif_ffprobe_title.setGeometry(QtCore.QRect(910, 630, 371, 16)) self.l_exif_ffprobe_title.setGeometry(QtCore.QRect(910, 590, 371, 16))
font = QtGui.QFont() font = QtGui.QFont()
font.setPointSize(18) font.setPointSize(18)
font.setBold(True) font.setBold(True)
@ -173,87 +176,48 @@ class Ui_MainWindow(object):
self.eventName.setObjectName("eventName") self.eventName.setObjectName("eventName")
self.gridLayout.addWidget(self.eventName, 0, 1, 1, 1) self.gridLayout.addWidget(self.eventName, 0, 1, 1, 1)
self.gridLayoutWidget_4 = QtWidgets.QWidget(parent=self.centralwidget) self.gridLayoutWidget_4 = QtWidgets.QWidget(parent=self.centralwidget)
self.gridLayoutWidget_4.setGeometry(QtCore.QRect(910, 50, 182, 91)) self.gridLayoutWidget_4.setGeometry(QtCore.QRect(910, 50, 541, 41))
self.gridLayoutWidget_4.setObjectName("gridLayoutWidget_4") self.gridLayoutWidget_4.setObjectName("gridLayoutWidget_4")
self.gridLayout_2 = QtWidgets.QGridLayout(self.gridLayoutWidget_4) self.gridLayout_2 = QtWidgets.QGridLayout(self.gridLayoutWidget_4)
self.gridLayout_2.setContentsMargins(0, 0, 0, 0) self.gridLayout_2.setContentsMargins(0, 0, 0, 0)
self.gridLayout_2.setObjectName("gridLayout_2") self.gridLayout_2.setObjectName("gridLayout_2")
spacerItem = QtWidgets.QSpacerItem(180, 20, QtWidgets.QSizePolicy.Policy.Maximum, QtWidgets.QSizePolicy.Policy.Minimum)
self.gridLayout_2.addItem(spacerItem, 0, 2, 1, 1)
self.label_4 = QtWidgets.QLabel(parent=self.gridLayoutWidget_4)
font = QtGui.QFont()
font.setPointSize(18)
self.label_4.setFont(font)
self.label_4.setObjectName("label_4")
self.gridLayout_2.addWidget(self.label_4, 0, 3, 1, 1)
self.lcd_files_found = QtWidgets.QLCDNumber(parent=self.gridLayoutWidget_4)
self.lcd_files_found.setMinimumSize(QtCore.QSize(80, 0))
self.lcd_files_found.setMaximumSize(QtCore.QSize(80, 16777215))
self.lcd_files_found.setObjectName("lcd_files_found")
self.gridLayout_2.addWidget(self.lcd_files_found, 0, 1, 1, 1)
self.label_3 = QtWidgets.QLabel(parent=self.gridLayoutWidget_4) self.label_3 = QtWidgets.QLabel(parent=self.gridLayoutWidget_4)
font = QtGui.QFont() font = QtGui.QFont()
font.setPointSize(18) font.setPointSize(18)
self.label_3.setFont(font) self.label_3.setFont(font)
self.label_3.setObjectName("label_3") self.label_3.setObjectName("label_3")
self.gridLayout_2.addWidget(self.label_3, 0, 0, 1, 1) 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.lcd_files_imported = QtWidgets.QLCDNumber(parent=self.gridLayoutWidget_4) self.lcd_files_imported = QtWidgets.QLCDNumber(parent=self.gridLayoutWidget_4)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.lcd_files_imported.sizePolicy().hasHeightForWidth())
self.lcd_files_imported.setSizePolicy(sizePolicy)
self.lcd_files_imported.setMinimumSize(QtCore.QSize(80, 0))
self.lcd_files_imported.setMaximumSize(QtCore.QSize(80, 16777215))
self.lcd_files_imported.setObjectName("lcd_files_imported") self.lcd_files_imported.setObjectName("lcd_files_imported")
self.gridLayout_2.addWidget(self.lcd_files_imported, 1, 1, 1, 1) self.gridLayout_2.addWidget(self.lcd_files_imported, 0, 4, 1, 1)
self.label_4 = QtWidgets.QLabel(parent=self.gridLayoutWidget_4)
font = QtGui.QFont()
font.setPointSize(18)
self.label_4.setFont(font)
self.label_4.setObjectName("label_4")
self.gridLayout_2.addWidget(self.label_4, 1, 0, 1, 1)
self.gridLayout_2.setColumnStretch(1, 1)
self.gridLayoutWidget_5 = QtWidgets.QWidget(parent=self.centralwidget)
self.gridLayoutWidget_5.setGeometry(QtCore.QRect(1100, 50, 351, 93))
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_importing_2 = QtWidgets.QProgressBar(parent=self.gridLayoutWidget_5)
self.progressBar_importing_2.setProperty("value", 24)
self.progressBar_importing_2.setObjectName("progressBar_importing_2")
self.gridLayout_3.addWidget(self.progressBar_importing_2, 2, 1, 1, 1)
self.lcd_import_progress = QtWidgets.QLCDNumber(parent=self.gridLayoutWidget_5)
self.lcd_import_progress.setFrameShape(QtWidgets.QFrame.Shape.NoFrame)
self.lcd_import_progress.setObjectName("lcd_import_progress")
self.gridLayout_3.addWidget(self.lcd_import_progress, 1, 2, 1, 1)
self.lcd_current_file_progress = QtWidgets.QLCDNumber(parent=self.gridLayoutWidget_5)
self.lcd_current_file_progress.setFrameShape(QtWidgets.QFrame.Shape.NoFrame)
self.lcd_current_file_progress.setObjectName("lcd_current_file_progress")
self.gridLayout_3.addWidget(self.lcd_current_file_progress, 2, 2, 1, 1)
self.l_proecessing_progress = QtWidgets.QLabel(parent=self.gridLayoutWidget_5)
self.l_proecessing_progress.setObjectName("l_proecessing_progress")
self.gridLayout_3.addWidget(self.l_proecessing_progress, 0, 0, 1, 1)
self.l_current_file_progress = QtWidgets.QLabel(parent=self.gridLayoutWidget_5)
self.l_current_file_progress.setObjectName("l_current_file_progress")
self.gridLayout_3.addWidget(self.l_current_file_progress, 2, 0, 1, 1)
self.progressBar_processing = QtWidgets.QProgressBar(parent=self.gridLayoutWidget_5)
self.progressBar_processing.setProperty("value", 24)
self.progressBar_processing.setObjectName("progressBar_processing")
self.gridLayout_3.addWidget(self.progressBar_processing, 0, 1, 1, 1)
self.progressBar_importing = QtWidgets.QProgressBar(parent=self.gridLayoutWidget_5)
self.progressBar_importing.setProperty("value", 24)
self.progressBar_importing.setObjectName("progressBar_importing")
self.gridLayout_3.addWidget(self.progressBar_importing, 1, 1, 1, 1)
self.l_import_progress = QtWidgets.QLabel(parent=self.gridLayoutWidget_5)
self.l_import_progress.setObjectName("l_import_progress")
self.gridLayout_3.addWidget(self.l_import_progress, 1, 0, 1, 1)
self.lcd_processing_progress = QtWidgets.QLCDNumber(parent=self.gridLayoutWidget_5)
self.lcd_processing_progress.setFrameShape(QtWidgets.QFrame.Shape.NoFrame)
self.lcd_processing_progress.setFrameShadow(QtWidgets.QFrame.Shadow.Plain)
self.lcd_processing_progress.setObjectName("lcd_processing_progress")
self.gridLayout_3.addWidget(self.lcd_processing_progress, 0, 2, 1, 1)
self.label_2 = QtWidgets.QLabel(parent=self.gridLayoutWidget_5)
self.label_2.setObjectName("label_2")
self.gridLayout_3.addWidget(self.label_2, 0, 3, 1, 1)
self.label_5 = QtWidgets.QLabel(parent=self.gridLayoutWidget_5)
self.label_5.setObjectName("label_5")
self.gridLayout_3.addWidget(self.label_5, 1, 3, 1, 1)
self.label_6 = QtWidgets.QLabel(parent=self.gridLayoutWidget_5)
self.label_6.setObjectName("label_6")
self.gridLayout_3.addWidget(self.label_6, 2, 3, 1, 1)
self.img_preview = QtWidgets.QLabel(parent=self.centralwidget) self.img_preview = QtWidgets.QLabel(parent=self.centralwidget)
self.img_preview.setGeometry(QtCore.QRect(910, 150, 541, 371)) self.img_preview.setGeometry(QtCore.QRect(910, 110, 541, 371))
self.img_preview.setAutoFillBackground(True) self.img_preview.setAutoFillBackground(True)
self.img_preview.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) self.img_preview.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.img_preview.setText("") self.img_preview.setText("")
self.img_preview.setObjectName("img_preview") self.img_preview.setObjectName("img_preview")
self.gridLayoutWidget_6 = QtWidgets.QWidget(parent=self.centralwidget) self.gridLayoutWidget_6 = QtWidgets.QWidget(parent=self.centralwidget)
self.gridLayoutWidget_6.setGeometry(QtCore.QRect(910, 530, 541, 91)) self.gridLayoutWidget_6.setGeometry(QtCore.QRect(910, 490, 541, 91))
self.gridLayoutWidget_6.setObjectName("gridLayoutWidget_6") self.gridLayoutWidget_6.setObjectName("gridLayoutWidget_6")
self.grid_metadata_2 = QtWidgets.QGridLayout(self.gridLayoutWidget_6) self.grid_metadata_2 = QtWidgets.QGridLayout(self.gridLayoutWidget_6)
self.grid_metadata_2.setContentsMargins(0, 0, 0, 0) self.grid_metadata_2.setContentsMargins(0, 0, 0, 0)
@ -298,8 +262,8 @@ class Ui_MainWindow(object):
self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget_2) self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget_2)
self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0) self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0)
self.horizontalLayout_2.setObjectName("horizontalLayout_2") self.horizontalLayout_2.setObjectName("horizontalLayout_2")
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout_2.addItem(spacerItem) self.horizontalLayout_2.addItem(spacerItem1)
self.checkBox_verify_checksum = QtWidgets.QCheckBox(parent=self.horizontalLayoutWidget_2) self.checkBox_verify_checksum = QtWidgets.QCheckBox(parent=self.horizontalLayoutWidget_2)
self.checkBox_verify_checksum.setChecked(True) self.checkBox_verify_checksum.setChecked(True)
self.checkBox_verify_checksum.setObjectName("checkBox_verify_checksum") self.checkBox_verify_checksum.setObjectName("checkBox_verify_checksum")
@ -327,8 +291,8 @@ class Ui_MainWindow(object):
self.label = QtWidgets.QLabel(parent=self.horizontalLayoutWidget_3) self.label = QtWidgets.QLabel(parent=self.horizontalLayoutWidget_3)
self.label.setObjectName("label") self.label.setObjectName("label")
self.horizontalLayout_3.addWidget(self.label) self.horizontalLayout_3.addWidget(self.label)
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout_3.addItem(spacerItem1) self.horizontalLayout_3.addItem(spacerItem2)
self.checkBox_search_for_images = QtWidgets.QCheckBox(parent=self.horizontalLayoutWidget_3) self.checkBox_search_for_images = QtWidgets.QCheckBox(parent=self.horizontalLayoutWidget_3)
icon = QtGui.QIcon.fromTheme("camera-photo") icon = QtGui.QIcon.fromTheme("camera-photo")
self.checkBox_search_for_images.setIcon(icon) self.checkBox_search_for_images.setIcon(icon)
@ -360,7 +324,7 @@ class Ui_MainWindow(object):
self.horizontalLayout_3.addWidget(self.pushButton_3_scan_dir) self.horizontalLayout_3.addWidget(self.pushButton_3_scan_dir)
MainWindow.setCentralWidget(self.centralwidget) MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(parent=MainWindow) self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 1463, 24)) self.menubar.setGeometry(QtCore.QRect(0, 0, 1463, 21))
self.menubar.setObjectName("menubar") self.menubar.setObjectName("menubar")
self.menuBit_Mover = QtWidgets.QMenu(parent=self.menubar) self.menuBit_Mover = QtWidgets.QMenu(parent=self.menubar)
self.menuBit_Mover.setObjectName("menuBit_Mover") self.menuBit_Mover.setObjectName("menuBit_Mover")
@ -377,7 +341,7 @@ class Ui_MainWindow(object):
def retranslateUi(self, MainWindow): def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate _translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) MainWindow.setWindowTitle(_translate("MainWindow", "BitMover"))
self.pushButton_src_browse.setText(_translate("MainWindow", "Browse")) self.pushButton_src_browse.setText(_translate("MainWindow", "Browse"))
self.pushButton_dst_browse.setText(_translate("MainWindow", "Browse")) self.pushButton_dst_browse.setText(_translate("MainWindow", "Browse"))
self.label_1_src_dir.setText(_translate("MainWindow", "Source Directory")) self.label_1_src_dir.setText(_translate("MainWindow", "Source Directory"))
@ -393,14 +357,8 @@ class Ui_MainWindow(object):
self.l_meta_08.setText(_translate("MainWindow", "Focal Length")) self.l_meta_08.setText(_translate("MainWindow", "Focal Length"))
self.l_exif_ffprobe_title.setText(_translate("MainWindow", "Exif / ffprobe Data")) self.l_exif_ffprobe_title.setText(_translate("MainWindow", "Exif / ffprobe Data"))
self.labelEvent.setText(_translate("MainWindow", "Event")) self.labelEvent.setText(_translate("MainWindow", "Event"))
self.label_3.setText(_translate("MainWindow", "Files Found"))
self.label_4.setText(_translate("MainWindow", "Files Imported")) self.label_4.setText(_translate("MainWindow", "Files Imported"))
self.l_proecessing_progress.setText(_translate("MainWindow", "Processing Progress")) self.label_3.setText(_translate("MainWindow", "Files Found"))
self.l_current_file_progress.setText(_translate("MainWindow", "Current File Progress"))
self.l_import_progress.setText(_translate("MainWindow", "Import Progress"))
self.label_2.setText(_translate("MainWindow", "%"))
self.label_5.setText(_translate("MainWindow", "%"))
self.label_6.setText(_translate("MainWindow", "%"))
self.l_file_source_path.setText(_translate("MainWindow", "Source Path")) self.l_file_source_path.setText(_translate("MainWindow", "Source Path"))
self.l_file_dest_path.setText(_translate("MainWindow", "Destination Path")) self.l_file_dest_path.setText(_translate("MainWindow", "Destination Path"))
self.checkBox_verify_checksum.setToolTip(_translate("MainWindow", "After copying, verify that the hash of the original file equals the hash of the copied file.")) self.checkBox_verify_checksum.setToolTip(_translate("MainWindow", "After copying, verify that the hash of the original file equals the hash of the copied file."))

153
_ComparisonDialog.ui Normal file
View File

@ -0,0 +1,153 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FileComparisonDialog</class>
<widget class="QDialog" name="FileComparisonDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1588</width>
<height>753</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<widget class="QTableWidget" name="tableWidget">
<property name="geometry">
<rect>
<x>20</x>
<y>90</y>
<width>1551</width>
<height>651</height>
</rect>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="tabKeyNavigation">
<bool>false</bool>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="rowCount">
<number>2</number>
</property>
<property name="columnCount">
<number>5</number>
</property>
<attribute name="horizontalHeaderDefaultSectionSize">
<number>300</number>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>true</bool>
</attribute>
<row/>
<row/>
<column/>
<column/>
<column/>
<column/>
<column/>
</widget>
<widget class="QWidget" name="gridLayoutWidget">
<property name="geometry">
<rect>
<x>240</x>
<y>11</y>
<width>981</width>
<height>71</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="2">
<widget class="QLabel" name="l_hash_dest">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="l_path_file_source">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="l_path_file_dest">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel" name="l_hash_source">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_title_source">
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Source</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_title_dest">
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Destination</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QLabel" name="label_title_comparing_files">
<property name="geometry">
<rect>
<x>20</x>
<y>10</y>
<width>231</width>
<height>61</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>28</pointsize>
</font>
</property>
<property name="text">
<string>Comparing Files</string>
</property>
</widget>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,50 @@
# Form implementation generated from reading ui file '_checksum_progress_dialog.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_ChecksumProgressDialog(object):
def setupUi(self, ChecksumProgressDialog):
ChecksumProgressDialog.setObjectName("ChecksumProgressDialog")
ChecksumProgressDialog.resize(640, 86)
self.gridLayoutWidget = QtWidgets.QWidget(parent=ChecksumProgressDialog)
self.gridLayoutWidget.setGeometry(QtCore.QRect(20, 20, 601, 21))
self.gridLayoutWidget.setObjectName("gridLayoutWidget")
self.gridLayout = QtWidgets.QGridLayout(self.gridLayoutWidget)
self.gridLayout.setContentsMargins(0, 0, 0, 0)
self.gridLayout.setObjectName("gridLayout")
self.l_title_getting_checksum = QtWidgets.QLabel(parent=self.gridLayoutWidget)
self.l_title_getting_checksum.setMinimumSize(QtCore.QSize(100, 1))
self.l_title_getting_checksum.setBaseSize(QtCore.QSize(100, 0))
self.l_title_getting_checksum.setLineWidth(0)
self.l_title_getting_checksum.setObjectName("l_title_getting_checksum")
self.gridLayout.addWidget(self.l_title_getting_checksum, 0, 0, 1, 1)
self.l_content_checksum_filename = QtWidgets.QLabel(parent=self.gridLayoutWidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(1)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.l_content_checksum_filename.sizePolicy().hasHeightForWidth())
self.l_content_checksum_filename.setSizePolicy(sizePolicy)
self.l_content_checksum_filename.setMinimumSize(QtCore.QSize(150, 0))
self.l_content_checksum_filename.setText("")
self.l_content_checksum_filename.setObjectName("l_content_checksum_filename")
self.gridLayout.addWidget(self.l_content_checksum_filename, 0, 1, 1, 1)
self.progressBar_getting_checksum = QtWidgets.QProgressBar(parent=ChecksumProgressDialog)
self.progressBar_getting_checksum.setGeometry(QtCore.QRect(20, 50, 611, 19))
self.progressBar_getting_checksum.setProperty("value", 0)
self.progressBar_getting_checksum.setTextVisible(True)
self.progressBar_getting_checksum.setObjectName("progressBar_getting_checksum")
self.retranslateUi(ChecksumProgressDialog)
QtCore.QMetaObject.connectSlotsByName(ChecksumProgressDialog)
def retranslateUi(self, ChecksumProgressDialog):
_translate = QtCore.QCoreApplication.translate
ChecksumProgressDialog.setWindowTitle(_translate("ChecksumProgressDialog", "Checksum Progress"))
self.l_title_getting_checksum.setText(_translate("ChecksumProgressDialog", "Getting Checksum For"))

View File

@ -0,0 +1,73 @@
# Form implementation generated from reading ui file '_ComparisonDialog.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_FileComparisonDialog(object):
def setupUi(self, FileComparisonDialog):
FileComparisonDialog.setObjectName("FileComparisonDialog")
FileComparisonDialog.resize(1588, 753)
self.tableWidget = QtWidgets.QTableWidget(parent=FileComparisonDialog)
self.tableWidget.setGeometry(QtCore.QRect(20, 90, 1551, 651))
self.tableWidget.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers)
self.tableWidget.setTabKeyNavigation(False)
self.tableWidget.setProperty("showDropIndicator", False)
self.tableWidget.setRowCount(2)
self.tableWidget.setColumnCount(5)
self.tableWidget.setObjectName("tableWidget")
self.tableWidget.horizontalHeader().setDefaultSectionSize(300)
self.tableWidget.verticalHeader().setVisible(True)
self.gridLayoutWidget = QtWidgets.QWidget(parent=FileComparisonDialog)
self.gridLayoutWidget.setGeometry(QtCore.QRect(240, 11, 981, 71))
self.gridLayoutWidget.setObjectName("gridLayoutWidget")
self.gridLayout = QtWidgets.QGridLayout(self.gridLayoutWidget)
self.gridLayout.setContentsMargins(0, 0, 0, 0)
self.gridLayout.setObjectName("gridLayout")
self.l_hash_dest = QtWidgets.QLabel(parent=self.gridLayoutWidget)
self.l_hash_dest.setText("")
self.l_hash_dest.setObjectName("l_hash_dest")
self.gridLayout.addWidget(self.l_hash_dest, 2, 2, 1, 1)
self.l_path_file_source = QtWidgets.QLabel(parent=self.gridLayoutWidget)
self.l_path_file_source.setText("")
self.l_path_file_source.setObjectName("l_path_file_source")
self.gridLayout.addWidget(self.l_path_file_source, 0, 1, 1, 1)
self.l_path_file_dest = QtWidgets.QLabel(parent=self.gridLayoutWidget)
self.l_path_file_dest.setText("")
self.l_path_file_dest.setObjectName("l_path_file_dest")
self.gridLayout.addWidget(self.l_path_file_dest, 2, 1, 1, 1)
self.l_hash_source = QtWidgets.QLabel(parent=self.gridLayoutWidget)
self.l_hash_source.setText("")
self.l_hash_source.setObjectName("l_hash_source")
self.gridLayout.addWidget(self.l_hash_source, 0, 2, 1, 1)
self.label_title_source = QtWidgets.QLabel(parent=self.gridLayoutWidget)
self.label_title_source.setMaximumSize(QtCore.QSize(80, 16777215))
self.label_title_source.setBaseSize(QtCore.QSize(0, 0))
self.label_title_source.setObjectName("label_title_source")
self.gridLayout.addWidget(self.label_title_source, 0, 0, 1, 1)
self.label_title_dest = QtWidgets.QLabel(parent=self.gridLayoutWidget)
self.label_title_dest.setMaximumSize(QtCore.QSize(80, 16777215))
self.label_title_dest.setBaseSize(QtCore.QSize(0, 0))
self.label_title_dest.setObjectName("label_title_dest")
self.gridLayout.addWidget(self.label_title_dest, 2, 0, 1, 1)
self.label_title_comparing_files = QtWidgets.QLabel(parent=FileComparisonDialog)
self.label_title_comparing_files.setGeometry(QtCore.QRect(20, 10, 231, 61))
font = QtGui.QFont()
font.setPointSize(28)
self.label_title_comparing_files.setFont(font)
self.label_title_comparing_files.setObjectName("label_title_comparing_files")
self.retranslateUi(FileComparisonDialog)
QtCore.QMetaObject.connectSlotsByName(FileComparisonDialog)
def retranslateUi(self, FileComparisonDialog):
_translate = QtCore.QCoreApplication.translate
FileComparisonDialog.setWindowTitle(_translate("FileComparisonDialog", "Dialog"))
self.label_title_source.setText(_translate("FileComparisonDialog", "Source"))
self.label_title_dest.setText(_translate("FileComparisonDialog", "Destination"))
self.label_title_comparing_files.setText(_translate("FileComparisonDialog", "Comparing Files"))

View File

@ -0,0 +1,44 @@
# Form implementation generated from reading ui file '_finding_files_dialog.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_FindProgress(object):
def setupUi(self, FindProgress):
FindProgress.setObjectName("FindProgress")
FindProgress.resize(640, 60)
self.gridLayoutWidget_5 = QtWidgets.QWidget(parent=FindProgress)
self.gridLayoutWidget_5.setGeometry(QtCore.QRect(20, 10, 601, 41))
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.l_find_progress = QtWidgets.QLabel(parent=self.gridLayoutWidget_5)
self.l_find_progress.setObjectName("l_find_progress")
self.gridLayout_3.addWidget(self.l_find_progress, 0, 0, 1, 1)
self.lcd_import_progress = QtWidgets.QLCDNumber(parent=self.gridLayoutWidget_5)
self.lcd_import_progress.setFrameShape(QtWidgets.QFrame.Shape.NoFrame)
self.lcd_import_progress.setObjectName("lcd_import_progress")
self.gridLayout_3.addWidget(self.lcd_import_progress, 0, 2, 1, 1)
self.progressBar_importing = QtWidgets.QProgressBar(parent=self.gridLayoutWidget_5)
self.progressBar_importing.setProperty("value", 24)
self.progressBar_importing.setObjectName("progressBar_importing")
self.gridLayout_3.addWidget(self.progressBar_importing, 0, 1, 1, 1)
self.label_5 = QtWidgets.QLabel(parent=self.gridLayoutWidget_5)
self.label_5.setObjectName("label_5")
self.gridLayout_3.addWidget(self.label_5, 0, 3, 1, 1)
self.retranslateUi(FindProgress)
QtCore.QMetaObject.connectSlotsByName(FindProgress)
def retranslateUi(self, FindProgress):
_translate = QtCore.QCoreApplication.translate
FindProgress.setWindowTitle(_translate("FindProgress", "Finding Files"))
self.l_find_progress.setText(_translate("FindProgress", "Progress"))
self.label_5.setText(_translate("FindProgress", "%"))

47
__model.py Normal file
View File

@ -0,0 +1,47 @@
m = {
'f5c7c1a4e55d6288': {
'date': {
'capture_date': {
'y': '2024',
'm': '09',
'd': '08'
}
},
'event': {
'name': ''
},
'extension': 'JPG',
'folders': {
'destination': '/Users/kkenny/import/testing_dst/2024/2024-09/2024-09-08/PHOTO/JPG',
'destination_original': '',
'source_path': '/Users/kkenny/import/testing_src'
},
'name': 'DSC_6481.JPG',
'source_path_hash': 'f5c7c1a4e55d6288',
'type': 'image',
'image_meta': {
'photo': {
'aperture': (0x829D) Ratio = 11 @ 736,
'camera_brand': (0x010F)ASCII = NIKON CORPORATION @ 148,
'camera_model': (0x0110)ASCII = NIKON D5100 @ 168,
'dpi': < bound method PhotoFile.get_dpi of < _photo.PhotoFile object at 0x130f05fd0 >>,
'is_jpg': True,
'is_raw': False,
'iso': (0x8827) Short = 3200 @ 274,
'lens_make': None,
'lens_model': None,
'lens_focal_length': (0x920A) Ratio = 135 @ 808,
'size': {
'height': 3264,
'width': 4928,
'width_height': '4928x3264',
'megapixels': 16084992,
'ratio': 1.5098039215686274
}
}
},
'video_meta': None,
'audio_meta': None
}
}

62
_audio.py Normal file
View File

@ -0,0 +1,62 @@
#!/usr/bin/env python
import os.path
import ffmpeg
from datetime import datetime
from _time_and_date_utils import convert_from_seconds
class AudioFile:
def __init__(self,path_file_name,*args,**kwargs):
# super(AudioFile, self).__init__(*args, **kwargs)
self.path_file_name = path_file_name
self.probe = ffmpeg.probe(self.path_file_name)
self.audio_capture_date = self.get_audio_capture_date()
self.video_stream = None
self.audio_stream = None
self.format_stream = None
for i in self.probe['streams']:
if self.video_stream is None:
if 'video' == i['codec_type'].lower():
self.video_stream = i
if self.audio_stream is None:
if 'audio' == i['codec_type'].lower():
self.audio_stream = i
try:
self.format_stream = self.probe['format']
except AttributeError as e:
print(f"{e}: Audio file has no format string")
self.format_stream = None
self.stream = {}
def get_audio_capture_date(self):
#TODO: refactor this try/except logic.
stamp = None
if self.format_stream is not None:
try:
stamp = datetime.strptime(
self.format_stream['tags']['date'],'%Y-%m-%d')
except KeyError:
try:
stamp = datetime.fromtimestamp(os.path.getctime(self.path_file_name))
except:
stamp = datetime.strptime(
str('1900:01:01 00:00:00'),
'%Y:%m:%d %H:%M:%S'
)
return stamp
def get_audio_meta(self):
self.stream = {
'audio': {
'audio_channels': self.audio_stream['channels'],
'bits_per_sample': self.audio_stream['bits_per_raw_sample'],
'codec_long_name': self.audio_stream['codec_long_name'],
'duration': convert_from_seconds(float(self.audio_stream['duration'])),
'encoding_brand': self.format_stream['tags']['encoded_by'],
'sample_rate': self.audio_stream['sample_rate']
},
'video': {},
'format': {}
}
return self.stream

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ChecksumProgressDialog</class>
<widget class="QDialog" name="ChecksumProgressDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>640</width>
<height>86</height>
</rect>
</property>
<property name="windowTitle">
<string>Checksum Progress</string>
</property>
<widget class="QWidget" name="gridLayoutWidget">
<property name="geometry">
<rect>
<x>20</x>
<y>20</y>
<width>601</width>
<height>21</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="l_title_getting_checksum">
<property name="minimumSize">
<size>
<width>100</width>
<height>1</height>
</size>
</property>
<property name="baseSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="text">
<string>Getting Checksum For</string>
</property>
<property name="margin">
<number>3</number>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="l_content_checksum_filename">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QProgressBar" name="progressBar_getting_checksum">
<property name="geometry">
<rect>
<x>20</x>
<y>50</y>
<width>611</width>
<height>19</height>
</rect>
</property>
<property name="value">
<number>0</number>
</property>
<property name="textVisible">
<bool>true</bool>
</property>
</widget>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -6,12 +6,10 @@ Load the config file from yaml file
import sys import sys
import os import os
import yaml import yaml
from _lumberjack import timber
basedir = os.path.dirname(__file__) basedir = os.path.dirname(__file__)
files = {} files = {}
CONFIG_FILE = os.path.join(basedir, 'config.yaml') CONFIG_FILE = os.path.join(basedir, 'config.yaml')
log = timber(__name__)
class Configure: class Configure:
""" Configure Class """ """ Configure Class """

View File

@ -0,0 +1,33 @@
from PyQt6.QtWidgets import QDialog
from _Window_checksum_progress_dialog import Ui_ChecksumProgressDialog
class DialogChecksumProgress(QDialog, Ui_ChecksumProgressDialog):
def __init__(self,*args,**kwargs):
super(DialogChecksumProgress,self).__init__(*args,**kwargs)
self.setupUi(self)
# self.import_dialog = UI_DialogImport()
# self.ui_import_dialog = Ui_DialogImport()
def is_shown(self):
return self.isVisible()
def open_dialog(self,should_be_open):
if should_be_open is True:
if self.is_shown() is False:
self.show()
else:
print('Checksum dialog already open.')
else:
self.close_dialog()
def close_dialog(self):
if self.is_shown():
self.hide()
def set_progress(self, n):
# print("%d%% done" % n)
self.progressBar_getting_checksum.setValue(int(n))
# self.lcd_import_progress.display(n)
def set_file(self,f):
self.l_content_checksum_filename.setText(f)

View File

@ -0,0 +1,56 @@
from PyQt6.QtWidgets import QDialog, QTableWidgetItem
from _Window_comparison_dialog import Ui_FileComparisonDialog
class DialogCompareImportedChecksums(QDialog, Ui_FileComparisonDialog):
def __init__(self,*args,**kwargs):
super(DialogCompareImportedChecksums,self).__init__(*args,**kwargs)
self.setupUi(self)
# self.import_dialog = UI_DialogImport()
# self.ui_import_dialog = Ui_DialogImport()
self.table_headers = ['Source File',
'Destination File',
'Source Hash',
'Destination Hash',
'Checksum Match']
def setup_compare_dialog(self):
self.set_table_headers()
self.tableWidget.setRowCount(0)
def is_shown(self):
return self.isVisible()
def open_dialog(self):
if self.is_shown() is False:
self.show()
else:
self.close_dialog()
def close_dialog(self):
if self.is_shown():
self.hide()
def set_label_path_file_source(self,value):
self.l_path_file_source.setText(value)
def set_label_path_file_dest(self,value):
self.l_path_file_dest.setText(value)
def set_label_hash_source(self,value):
self.l_hash_source.setText(value)
def set_label_hash_dest(self,value):
self.l_hash_dest.setText(value)
def set_table_headers(self):
self.tableWidget.setHorizontalHeaderLabels(self.table_headers)
def add_table_row(self,_dictionary):
row_count = self.tableWidget.rowCount()
self.tableWidget.insertRow(row_count)
self.tableWidget.setItem(row_count, 0, QTableWidgetItem(_dictionary['source_path_file']))
self.tableWidget.setItem(row_count, 1, QTableWidgetItem(_dictionary['dest_path_file']))
self.tableWidget.setItem(row_count, 2, QTableWidgetItem(_dictionary['source_path_hash']))
self.tableWidget.setItem(row_count, 3, QTableWidgetItem(_dictionary['dest_path_hash']))
self.tableWidget.setItem(row_count, 4, QTableWidgetItem(str(_dictionary['checksum_match'])))

24
_dialog_find_files.py Normal file
View File

@ -0,0 +1,24 @@
from PyQt6.QtWidgets import QDialog
from _Window_finding_files_dialog import Ui_FindProgress
class FindProgress(QDialog, Ui_FindProgress):
def __init__(self,*args,**kwargs):
super(FindProgress,self).__init__(*args,**kwargs)
self.setupUi(self)
# self.set_progress_finding_files(0)
def is_shown(self):
return self.isVisible()
def open_find_files_dialog(self):
if not self.is_shown():
self.show()
def close_find_files_dialog(self):
if self.is_shown():
self.hide()
def set_progress_finding_files(self,n):
# print("%d%% done" % n)
self.progressBar_importing.setValue(float(n))
self.lcd_import_progress.display(n)

View File

@ -1,5 +1,5 @@
from PyQt6.QtWidgets import QDialog from PyQt6.QtWidgets import QDialog
from _import_dialog_Window import Ui_DialogImport from _Window_import_dialog import Ui_DialogImport
class DialogImport(QDialog, Ui_DialogImport): class DialogImport(QDialog, Ui_DialogImport):
def __init__(self,*args,**kwargs): def __init__(self,*args,**kwargs):
@ -7,33 +7,30 @@ class DialogImport(QDialog, Ui_DialogImport):
self.setupUi(self) self.setupUi(self)
# self.import_dialog = UI_DialogImport() # self.import_dialog = UI_DialogImport()
# self.ui_import_dialog = Ui_DialogImport() # self.ui_import_dialog = Ui_DialogImport()
print('DialogImport')
def is_shown(self): def is_shown(self):
print(f'is_shown: {self.isVisible()}')
return self.isVisible() return self.isVisible()
def open_import_dialog(self): def open_import_dialog(self):
print(f'open_import_dialog: {self.is_shown()}') if self.is_shown() is False:
if not self.is_shown(): if not self.is_shown():
print('Inside if not self.is_shown') self.show()
print('showing window') else:
self.show() print('Import Dialog already open.')
else:
self.close_import_dialog()
def close_import_dialog(self): def close_import_dialog(self):
print('close_import_dialog')
if self.is_shown(): if self.is_shown():
print('inside self.is_shown()')
print('hiding window')
self.hide() self.hide()
def set_progress_importing(self, n): def set_progress_importing(self, n):
print("%d%% done" % n) # print("%d%% done" % n)
self.progressBar_importing.setValue(int(n)) self.progressBar_importing.setValue(int(n))
self.lcd_import_progress.display(n) self.lcd_import_progress.display(n)
def set_progress_current_file(self, n): def set_progress_current_file(self, n):
print("%d%% done" % n) # print("%d%% done" % n)
self.progressBar_importing_2.setValue(int(n)) self.progressBar_importing_2.setValue(int(n))
self.lcd_current_file_progress.display(n) self.lcd_current_file_progress.display(n)

View File

@ -9,82 +9,62 @@ import yaml
from tqdm import tqdm from tqdm import tqdm
### Local Imports ### Local Imports
# from configure import Configure, CONFIG_FILE # from _hashing import xx_hash
from _hashing import xx_hash from _verify_file_checksum import FileHash
from _lumberjack import timber
def check_log_dir(d): def check_log_dir(d):
create_folder(d) create_folder(d)
# c = Configure(CONFIG_FILE)
# config = c.load_config()
log = timber(__name__)
log.info("Starting")
def dump_yaml(dictionary,file): def dump_yaml(dictionary,file):
""" dump dictionary to yaml file """ """ dump dictionary to yaml file """
with open(file, 'w', encoding="utf-8") as f: with open(file, 'w', encoding="utf-8") as f:
yaml.dump(dictionary, f) yaml.dump(dictionary, f)
f.close() f.close()
def path_exists(p): def path_exists(p):
""" determine if the path exists """ """ determine if the path exists """
log.debug(f'path_exists({p})')
pe = os.path.exists(p) pe = os.path.exists(p)
# print(f'Path Exists: {pe}')
return pe return pe
def is_dir(d): def is_dir(d):
""" determine if object is a dir or not """ """ determine if object is a dir or not """
log.debug(f'is_dir({d})')
return os.path.isdir(d) return os.path.isdir(d)
def is_file(f): def is_file(f):
""" determine if object is a file or not """ """ determine if object is a file or not """
log.debug(f'is_file({f})')
return os.path.isfile(f) return os.path.isfile(f)
def cmp_files(f1,f2): def cmp_files(f1,f2):
""" compare two files """ """ compare two files """
log.debug(f'cmp_files({f1},{f2})')
#TODO: Determine if path is actually a file #TODO: Determine if path is actually a file
#TODO: Determine if the hash has already been stored and use it if so #TODO: Determine if the hash has already been stored and use it if so
hash1 = xx_hash(f1) hash1 = FileHash(f1).file_hash
hash2 = xx_hash(f2) hash2 = FileHash(f2).file_hash
return hash1 == hash2 return hash1 == hash2
def cmp_hashes(h1,h2):
""" compare two files """
return h1 == h2
def path_access_read(path): def path_access_read(path):
""" make sure we can read from the path """ """ make sure we can read from the path """
log.debug(f'path_access_read({path})')
val = os.access(path, os.R_OK) val = os.access(path, os.R_OK)
if val is False: if val is False:
log.error(f'Can not read from {path}') print(f'path_access_read check: cannot read from {path}')
return val return val
def path_access_write(path): def path_access_write(path):
""" make sure we can write to the path """ """ make sure we can write to the path """
log.debug(f'path_access_write({path})')
val = os.access(path, os.W_OK) val = os.access(path, os.W_OK)
if val is False: if val is False:
log.error(f'Can not write to {path}') print(f'path_access_write check: cannot write to {path}')
return val return val
def create_folder(file): def create_folder(file):
""" Function to create folder """ """ Function to create folder """
log.debug(f'create_folder({file})')
if path_exists(file) is False: if path_exists(file) is False:
os.makedirs(file) os.makedirs(file)
elif is_dir(file) is False: elif is_dir(file) is False:
@ -92,22 +72,17 @@ def create_folder(file):
def cleanup_sd(f,config): def cleanup_sd(f,config):
""" If we should clean up the SD, nuke the copied files. """ """ If we should clean up the SD, nuke the copied files. """
log.debug(f'cleanup_sd({f})')
if config['cleanup_sd'] is True: if config['cleanup_sd'] is True:
os.system('clear') os.system('clear')
for file in tqdm(f, desc = "Cleaning Up SD:"): for file in tqdm(f, desc = "Cleaning Up SD:"):
if f[file]['source_cleanable'] is True: 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'])}") print(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'])) os.remove(os.path.join(f[file]['folders']['source_path'],f[file]['name']))
else: else:
log.debug(f"Cleanup SD: Not Removing File -\n{os.path.join(f[file]['folders']['source_path'],f[file]['name'])}") print(f"Cleanup SD: Not Removing File -\n{os.path.join(f[file]['folders']['source_path'],f[file]['name'])}")
def validate_config_dir_access(config): def validate_config_dir_access(config):
""" Validate we can operate in the defined directories """ """ Validate we can operate in the defined directories """
log.debug('validate_config_dir_access')
check = path_access_write(config['folders']['destination']['base']) check = path_access_write(config['folders']['destination']['base'])
if check is False: if check is False:
accessible = False accessible = False
@ -126,3 +101,11 @@ def validate_config_dir_access(config):
accessible = True accessible = True
return accessible return accessible
def get_file_name(path_file_name):
return os.path.basename(path_file_name)
def get_dotted_file_ext(file_name):
return os.path.splitext(file_name)[1]
def get_file_ext(file_name):
return get_dotted_file_ext(file_name).split('.')[1]

59
_find_files.py Normal file
View File

@ -0,0 +1,59 @@
import os
from _file_stuff import is_file
basedir = os.path.dirname(__file__)
class FindFiles:
def __init__(self,
search_types,
src_dir,
file_total,
file_types,
*args,
**kwargs):
super(FindFiles,self).__init__(*args,**kwargs)
self.file_total = file_total
self.src_dir = src_dir
self.search_types = search_types
self.file_types = file_types
self.file_name = None
self.path_file_source = None
self.file_type = None
self.file_ext = None
def t_find_files(self,
progress_callback,
current_file_progress_callback,
imported_file_count_callback,
found_file_callback,
total_file_count_callback,
checksum_file_callback,
checksum_progress_callback,
checksum_dialog_open_callback,
compare_checksums_source_file_callback,
compare_checksums_dest_file_callback,
compare_checksums_source_hash_callback,
compare_checksums_dest_hash_callback,
compare_checksums_add_row_callback):
file_count = int(0)
if len(self.search_types) > 0:
for folder, subfolders, filename in os.walk(self.src_dir):
for self.file_type in self.search_types:
for self.file_ext in self.file_types[self.file_type]:
for self.file_name in filename:
if self.file_name.lower().endswith(self.file_ext):
self.path_file_source = os.path.join(folder, self.file_name)
if is_file(self.path_file_source):
file_count += int(1)
# process_file(current_file)
found_file_callback.emit(self.path_file_source)
else:
print(f"Skipping {self.path_file_source} as it does not look like a real file.")
total_file_count_callback.emit(file_count)
progress_callback.emit(round((file_count / self.file_total) * 100, 0))
else:
print("Nothing to search for.")

59
_finding_files_dialog.ui Normal file
View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FindProgress</class>
<widget class="QDialog" name="FindProgress">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>640</width>
<height>60</height>
</rect>
</property>
<property name="windowTitle">
<string>Finding Files</string>
</property>
<widget class="QWidget" name="gridLayoutWidget_5">
<property name="geometry">
<rect>
<x>20</x>
<y>10</y>
<width>601</width>
<height>41</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="l_find_progress">
<property name="text">
<string>Progress</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLCDNumber" name="lcd_import_progress">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QProgressBar" name="progressBar_importing">
<property name="value">
<number>24</number>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QLabel" name="label_5">
<property name="text">
<string>%</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -1,98 +0,0 @@
#!/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

View File

@ -7,51 +7,35 @@ dump the dictionary generated from findings into a yaml file for later inspectio
import os import os
import xxhash import xxhash
from tqdm import tqdm from tqdm import tqdm
# from configure import Configure, CONFIG_FILE
from _lumberjack import timber
# c = Configure(CONFIG_FILE) # def xx_hash(file):
# config = c.load_config() # """ calculates and returns file hash based on xxHash """
log = timber(__name__) # 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 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): def gen_xxhashes(f):
""" Generate xxHashes """ """ Generate xxHashes """
log.debug(f'gen_xxhashes({f})')
for file in tqdm(f, desc = "Generating xx Hashes:"): for file in tqdm(f, desc = "Generating xx Hashes:"):
os.system('clear') os.system('clear')
log.debug(f[file])
f[file]['xx_checksums'] = {} f[file]['xx_checksums'] = {}
for folder in f[file]['folders']: for folder in f[file]['folders']:
k = os.path.join(f[file]['folders'][folder], f[file]['name']) k = os.path.join(f[file]['folders'][folder], f[file]['name'])
if k != f[file]['name']: if k != f[file]['name']:
# k = f[file]['folders'][folder]
log.debug(k)
f[file]['xx_checksums'][k] = xx_hash(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): def validate_xx_checksums(f):
""" Validate Checksums """ """ Validate Checksums """
@ -67,8 +51,8 @@ def validate_xx_checksums(f):
f[file]['source_cleanable'] = True f[file]['source_cleanable'] = True
else: else:
f[file]['source_cleanable'] = False f[file]['source_cleanable'] = False
log.critical(f'FATAL: Checksum validation failed for: \ print(f'FATAL: Checksum validation failed for: \
{f[file]["name"]} \n{c[i]}\n is not equal to \n{c[p]}\n') {f[file]["name"]} \n{c[i]}\n is not equal to \n{c[p]}\n')
log.debug('\n File Meta:\n') print('\n File Meta:\n')
log.debug(f[file]) print(f'f[file]: {f[file]}')
i = i + 1 i = i + 1

22
_image_tag.py Normal file
View File

@ -0,0 +1,22 @@
import exifread
from _time_and_date_utils import get_img_date
class ImageTag:
def __init__(self,path_file_name,*args,**kwargs):
super(ImageTag,self).__init__(*args,**kwargs)
self.path_file_name = path_file_name
self.image_tags = self.get_image_tags()
self.date_time_original = get_img_date(self.image_tags)
def get_image_tag(self,t):
tag_data = None
for tag in self.image_tags:
if t.lower() in tag.lower():
tag_data = self.image_tags[tag]
break
return tag_data
def get_image_tags(self):
with open(self.path_file_name,'rb') as f:
tags = exifread.process_file(f)
return tags

View File

@ -1,124 +0,0 @@
#!/usr/bin/env python
from _media import Media
from _get_image_tag import get_exif_tag
from PIL import Image
from _raw_photo import get_raw_image_dimensions, extract_jpg_thumb
from _video import Video
class ImgPreview:
def __init__(self,*args,**kwargs):
super(ImgPreview,self).__init__()
self.args = args
self.kwargs = kwargs
self.file = kwargs['file']
self.event = kwargs['event']
self.config = kwargs['config']
self.m = Media(self.file,
self.event,
self.config)
self.file_type = self.m.file_type
self.is_jpg = False
self.is_raw = False
self.path_hash = self.m.source_path_hash
self.dtc = f'{self.m.capture_date[0]}/{self.m.capture_date[1]}/{self.m.capture_date[2]}'
self.size = None
self.mpixels = None
self.thumbnail = 'thumbnail.jpg'
self.ratio = None
self.thumbnail_width = None
self.thumbnail_height = None
self.thumbnail_ratio = None
self.video_framerate = None
self.video_bit_depth = None
self.video_duration = None
self.video_encoding = None
self.video_codec = None
self.video_profile = None
self.video_pix_format = None
if self.file_type == 'image':
self._img_preview()
print(f'size: {self.size}')
print(f'dpi: {self.dpi}')
print(f'iso: {self.iso}')
print(f'lens: {self.lens}')
print(f'zoom: {self.zoom}')
print(f'camera: {self.camera}')
print(f'aperture: {self.aperture}')
print(f'mpixels: {self.mpixels}')
elif self.file_type == 'video':
self._video_preview()
self.thumb_ratio()
self.size = f'{self.width}x{self.height}'
if self.width is not None \
and self.height is not None:
self.mpixels = round((self.width * self.height) / 1000000, 1)
else:
self.mpixels = 'Unknown :('
def _img_preview(self):
self.dpi = get_exif_tag(self.file, "xresolution")
self.iso = get_exif_tag(self.file, 'iso')
self.aperture = get_exif_tag(self.file, 'fnumber')
self.camera = get_exif_tag(self.file,'cameramodelname')
if self.camera is None:
self.camera = get_exif_tag(self.file,'image model')
self.lens = get_exif_tag(self.file,'lensmodel')
self.zoom = get_exif_tag(self.file,'focallength')
if self.file.lower().endswith("jpg") \
or self.file.lower().endswith("jpeg"):
self._jpg_preview()
else:
self._raw_preview()
def _jpg_preview(self):
self.is_jpg = True
img = Image.open(self.file)
self.width = img.width
self.height = img.height
self.gen_thumb_from_jpg()
def gen_thumb_from_jpg(self):
"""Generates a thumbnail image from the given input image."""
thumb_width = 500
thumb_size = (thumb_width, int(thumb_width // 1.5))
try:
with Image.open(self.file) as img:
img.thumbnail(thumb_size)
img.save(self.thumbnail, "JPEG")
except IOError:
print(f"Error: Cannot create thumbnail for '{self.file}'")
def _raw_preview(self):
self.is_raw = True
self.width = get_raw_image_dimensions(self.file)[1]
self.height = get_raw_image_dimensions(self.file)[0]
self.thumbnail = extract_jpg_thumb(self.file)
def _video_preview(self):
vid = Video(file=self.file)
self.thumbnail = vid.gen_video_thumbnail()
video_meta = vid.get_video_meta()
self.width = video_meta['video']['size']['width']
self.height = video_meta['video']['size']['height']
self.video_framerate = round(
int(video_meta['video']['r_frame_rate'].split('/')[0]) /
int(video_meta['video']['r_frame_rate'].split('/')[1]),2)
self.video_bit_depth = video_meta['video']['bits_per_raw_sample']
self.video_duration = video_meta['video']['duration']
self.video_encoding = video_meta['video']['encoding_brand']
self.video_codec = video_meta['video']['codec_long_name']
self.video_profile = video_meta['video']['profile']
self.video_pix_format = video_meta['video']['pix_fmt']
def thumb_ratio(self):
img = Image.open(self.thumbnail)
self.thumbnail_width = img.width
self.thumbnail_height = img.height
self.thumbnail_ratio = float(self.thumbnail_width / self.thumbnail_height)
print(self.thumbnail_width)
print(self.thumbnail_height)
print(self.thumbnail_ratio)

View File

@ -8,52 +8,39 @@ from os import system,path,rename
from tqdm import tqdm from tqdm import tqdm
from _file_stuff import create_folder, cmp_files, path_exists from _file_stuff import create_folder, cmp_files, path_exists
from _lumberjack import timber
from _hashing import xx_hash from _hashing import xx_hash
# from configure import Configure, CONFIG_FILE
# c = Configure(CONFIG_FILE) def copy_with_progress(s,d):
# config = c.load_config()
log = timber(__name__)
def copy_with_progress(s,d,f):
""" Copy a file with the progress bar """ """ Copy a file with the progress bar """
log.debug(f'copy_with_progress({s},{d},{f})')
size = path.getsize(s) # size = path.getsize(s)
with open(s, 'rb') as fs: with open(s, 'rb') as fs:
with open(d, 'wb') as fd: with open(d, 'wb') as fd:
# with tqdm(total=size, unit='B', unit_scale=True, desc=f'Copying {f}') as pbar: while True:
while True: chunk = fs.read(4096)
chunk = fs.read(4096) if not chunk:
if not chunk: break
break fd.write(chunk)
fd.write(chunk)
# pbar.update(len(chunk))
def copy_from_source(source_path,dest_path,file_name): def copy_from_source(source_path,dest_path,file_name):
""" Copy file from source to destination """ """ 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)) file_exists = path_exists(path.join(dest_path,file_name))
if file_exists is True: if file_exists is True:
log.debug(f'\nFound {file_name} at destination, checking if they match.') print(f'\nFound {file_name} at destination, checking if they match.')
check_match = cmp_files( check_match = cmp_files(
path.join(source_path,file_name), path.join(source_path,file_name),
path.join(dest_path, file_name)) path.join(dest_path, file_name))
if check_match is False: if check_match is False:
log.warn(f'\nFound duplicate for {source_path}/{file_name}, \ print(f'\nFound duplicate for {source_path}/{file_name}, \
renaming destination with hash appended.') renaming destination with hash appended.')
base, extension = path.splitext(file_name) 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)) f_xxhash = xx_hash(path.join(dest_path, file_name))
#file_name_hash = base + '_' + md5 + extension
file_name_hash = base + '_' + f_xxhash + extension file_name_hash = base + '_' + f_xxhash + extension
rename(path.join(dest_path, file_name), rename(path.join(dest_path, file_name),
path.join(dest_path, file_name_hash)) path.join(dest_path, file_name_hash))
else: else:
log.info(f'\n{file_name} hashes match') print(f'\n{file_name} hashes match')
# remove(path.join(source_path,file_name)) # remove(path.join(source_path,file_name))
# f.pop(file_name) # f.pop(file_name)
return return
@ -61,13 +48,10 @@ def copy_from_source(source_path,dest_path,file_name):
# create_folder(dest_path) # create_folder(dest_path)
# shutil.copy(os.path.join(source_path,file_name), dest_path) # shutil.copy(os.path.join(source_path,file_name), dest_path)
copy_with_progress(path.join(source_path, file_name), copy_with_progress(path.join(source_path, file_name),
path.join(dest_path, file_name), path.join(dest_path, file_name))
file_name)
system('clear')
def copy_files(f,config): def copy_files(f,config):
""" Copy Files. """ """ Copy Files. """
log.debug(f'copy_files({f})')
system('clear') system('clear')
for file in tqdm(f, desc="Copying Files:"): for file in tqdm(f, desc="Copying Files:"):
create_folder(f[file]['folders']['destination']) create_folder(f[file]['folders']['destination'])

View File

@ -1,48 +0,0 @@
#!/usr/bin/env python
"""
A class for logging... no, not timber
"""
import logging
import os
basedir = os.path.dirname(__file__)
# 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(os.path.join(basedir, "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

160
_media.py
View File

@ -1,160 +0,0 @@
#!/usr/bin/env python
"""
Create the media object
"""
import os
from os import path
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 = self.configuration['store_originals']
self.destination_originals_path = ''
self.destination_path = self.set_destination_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':
# print(f'store_originals: {self.store_originals}')
p = p + '/PHOTO'
if self.file_ext.lower() in ('jpg', 'jpeg'):
# print(f'store_originals: {self.store_originals}')
if self.store_originals is True:
self.destination_originals_path = path.join(
p,
self.configuration['folders']['originals'],
'JPG')
# print(self.destination_originals_path)
p = p + '/JPG'
else:
if self.store_originals is True:
self.destination_originals_path = path.join(
p,
self.configuration['folders']['originals'],
'RAW')
# print(self.destination_originals_path)
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(p, f_type):
""" get capture date from meta """
# log.debug(f'get_capture_date({p}, {f_type}')
if f_type == 'image':
stamp = get_image_date(p)
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(p))
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 {p}. Giving up.')
sys.exit()
elif f_type == 'audio':
try:
stamp = datetime.strptime(ffmpeg.probe(
p)['format']['tags']['date'], '%Y-%m-%d')
except KeyError as ke:
# log.warning(f'\nError: {ke} for {p}. Trying getctime...')
try:
stamp = datetime.fromtimestamp(os.path.getctime(p))
except:
# log.critical(f'\nCould not get timestamp for {p}. Giving up.')
sys.exit()
else:
try:
stamp = datetime.fromtimestamp(os.path.getctime(p))
except:
# log.critical(f'\nCould not get timestamp for {p}. Giving up.')
sys.exit()
year = stamp.strftime("%Y")
month = stamp.strftime("%m")
day = stamp.strftime("%d")
return year, month, day

160
_media_file.py Normal file
View File

@ -0,0 +1,160 @@
import os.path
import sys
from _audio import AudioFile
from _verify_file_checksum import FileHash
from _video import VideoFile
from _photo import PhotoFile
from _file_stuff import (get_file_name,
get_file_ext,
get_dotted_file_ext)
from datetime import datetime
class MediaFile:
def __init__(self,
path_file_name,
config,
event_name,
*args,**kwargs):
# super(MediaFile,self).__init__(*args,**kwargs)
self.path_file_name = path_file_name
self.config = config
self.event_name = str(event_name)
self.store_originals = self.config['store_originals']
self.src_dir = os.path.dirname(self.path_file_name)
self.base_dst_dir = self.config['folders']['destination']['base']
self.source_path_hash = FileHash(self.path_file_name).path_file
self.file_name = get_file_name(self.path_file_name)
self.dotted_file_ext = get_dotted_file_ext(self.file_name)
self.file_ext = get_file_ext(self.file_name)
self.file_type = self.get_file_type()
self.capture_date = self.get_capture_date()
self.capture_date_year = self.capture_date[0]
self.capture_date_month = self.capture_date[1]
self.capture_date_day = self.capture_date[2]
self.dst_dir = self.set_destination_path()
self.destination_originals_path = ''
self.media = {}
self.media_meta = self.get_media_meta()
# video_media = VideoFile(path_file_name=self.path_file_name)
# audio_media = AudioFile(path_file_name=self.path_file_name)
# photo_media = PhotoFile(path_file_name=self.path_file_name)
def get_file_type(self):
""" determine if the extension is in the list of file types """
for t in self.config['file_types']:
for e in self.config['file_types'][t]:
if self.file_ext.lower().endswith(e.lower()):
return t
def set_destination_path(self):
"""
set the destination path for the file
base dir structure is:
base_dst_dir/YYYY/YYYY-MM/YYYY-MM-DD[-event_name]
"""
p1 = os.path.join(self.base_dst_dir,self.capture_date_year)
p2 = f'{self.capture_date_year}-{self.capture_date_month}'
p3 = f'{self.capture_date_year}-{self.capture_date_month}-{self.capture_date_day}'
p = f'{p1}/{p2}/{p3}' # <--- Dumb.
if self.event_name:
p4 = '-' + self.event_name
p = p + p4
if self.file_type == 'image':
p = os.path.join(p,'PHOTO')
if self.file_ext.lower() in ('jpg', 'jpeg'):
if self.store_originals is True:
self.destination_originals_path = os.path.join(
p,
self.config['folders']['originals'],
'JPG'
)
p = os.path.join(p, 'JPG')
else:
if self.store_originals is True:
self.destination_originals_path = os.path.join(
p,
self.config['folders']['originals'],
'RAW')
p = os.path.join(p,'RAW')
elif self.file_type == 'video':
p = os.path.join(p,'VIDEO')
elif self.file_type == 'audio':
p = os.path.join(p,'AUDIO')
return p
def get_capture_date(self):
""" get capture date from meta """
if self.file_type == 'image':
photo_media = PhotoFile(path_file_name=self.path_file_name)
stamp = photo_media.photo_capture_date
elif self.file_type == 'video':
video_media = VideoFile(path_file_name=self.path_file_name)
stamp = video_media.get_video_capture_date()
elif self.file_type == 'audio':
audio_media = AudioFile(path_file_name=self.path_file_name)
stamp = audio_media.get_audio_capture_date()
else:
try:
stamp = datetime.fromtimestamp(
os.path.getctime(
self.path_file_name
)
)
except:
print('end of the road for finding the date.')
sys.exit()
year = stamp.strftime("%Y")
month = stamp.strftime("%m")
day = stamp.strftime("%d")
return year, month, day
def get_media_meta(self):
self.media = {
'date': {
'capture_date': {
'y': self.capture_date_year,
'm': self.capture_date_month,
'd': self.capture_date_day
}
},
'event': {
'name': self.event_name
},
'extension': self.file_ext,
'folders': {
'destination': self.dst_dir,
'destination_original': self.destination_originals_path,
'source_path': self.src_dir
},
'name': self.file_name,
'source_path_hash': self.source_path_hash,
'file_type': self.file_type
}
if self.file_type == 'video':
video_media = VideoFile(path_file_name=self.path_file_name)
self.media['video_meta'] = video_media.video_meta()
self.media['image_meta'] = None
self.media['audio_meta'] = None
if self.file_type == 'image':
photo_media = PhotoFile(path_file_name=self.path_file_name)
self.media['image_meta'] = photo_media.get_photo_meta()
self.media['video_meta'] = None
self.media['audio_meta'] = None
if self.file_type == 'audio':
audio_media = AudioFile(path_file_name=self.path_file_name)
self.media['audio_meta'] = audio_media.get_audio_meta()
self.media['image_meta'] = None
self.media['video_meta'] = None
# print(f'MediaFile, self.media: {self.media}')
return self.media

16
_media_import.py Normal file
View File

@ -0,0 +1,16 @@
from os import path,rename
from BitMover_ui import MainWindow
from _file_stuff import create_folder, path_exists, cmp_files
from _hashing import xx_hash
class MediaImporter(MainWindow):
def __init__(self,*args,**kwargs):
super(MediaImporter,self).__init__(*args,**kwargs)
self.chunk_size = 16 * 1024
self.path_file_source = None
self.path_file_destination = None
self.destination_original_path = None
self.path_file_destination_original = None
self.path_file_destination = None

171
_photo.py Normal file
View File

@ -0,0 +1,171 @@
#!/usr/bin/env python
from PIL import Image
from _raw_photo import get_raw_image_dimensions
from _image_tag import ImageTag
# https://exiftool.org/TagNames/EXIF.html
# https://exiftool.org/TagNames/Sony.html
class PhotoFile:
def __init__(self,path_file_name,*args,**kwargs):
super(PhotoFile, self).__init__(*args, **kwargs)
self.path_file_name = path_file_name
self.args = args
self.kwargs = kwargs
self.img = None
self.image_tag = ImageTag(path_file_name=self.path_file_name)
self.is_jpg = self.is_photo_jpeg()
self.is_raw = self.is_photo_raw()
self.size_width = self.get_photo_width()
self.size_height = self.get_photo_height()
self.size = self.get_photo_size()
self.mpixels = self.get_photo_megapixels()
self.size_ratio = self.get_photo_size_ratio()
self.dpi = self.get_dpi()
self.iso = self.get_iso()
self.aperture = self.get_aperture()
self.camera_brand = self.get_camera_brand()
self.camera_model = self.get_camera_model()
self.lens_make = self.get_lens_make()
self.lens_model = self.get_lens_model()
self.focal_length = self.get_focal_length()
self.photo_meta = {}
self.photo_capture_date = self.get_photo_capture_date()
def get_photo_capture_date(self):
return self.image_tag.date_time_original
def is_photo_raw(self):
# TODO: Actually inspect the mime type instead of relying on an extension.
if self.path_file_name.lower().endswith('jpg') \
or self.path_file_name.lower().endswith('.jpeg'):
self.is_raw = False
self.is_jpg = True
self.img = Image.open(self.path_file_name)
else:
self.is_raw = True
self.is_jpg = False
return self.is_raw
def is_photo_jpeg(self):
# TODO: Stop making assumption. #Life-Lessons
self.is_photo_raw()
return self.is_jpg
def get_photo_width(self):
if self.is_jpg:
width = self.img.width
elif self.is_raw:
width = get_raw_image_dimensions(self.path_file_name)[1]
else:
width = None
return width
def get_photo_height(self):
if self.is_jpg:
height = self.img.height
elif self.is_raw:
height = get_raw_image_dimensions(self.path_file_name)[0]
else:
height = None
return height
def get_photo_size(self):
if self.size_width is None:
self.size_width = self.get_photo_width()
if self.size_height is None:
self.size_height = self.get_photo_height()
return f'{self.size_width}x{self.size_height}'
def get_photo_size_ratio(self):
if self.size_width is None:
self.size_width = self.get_photo_width()
if self.size_height is None:
self.size_height = self.get_photo_height()
return float(self.size_width / self.size_height)
def get_photo_megapixels(self):
if self.size_width is None:
self.size_width = self.get_photo_width()
if self.size_height is None:
self.size_height = self.get_photo_height()
return self.size_width * self.size_height
def get_dpi(self):
return self.image_tag.get_image_tag("xresolution")
def get_iso(self):
return self.image_tag.get_image_tag( 'iso')
def get_aperture(self):
return self.image_tag.get_image_tag('fnumber')
def get_camera_brand(self):
tag = self.image_tag.get_image_tag('Make')
return tag
def get_camera_model(self):
tag = self.image_tag.get_image_tag('cameramodelname')
if tag is None:
tag = self.image_tag.get_image_tag('image model')
return tag
def get_camera_firmware(self):
tag = self.image_tag.get_image_tag('CameraFirmware')
return tag
def get_camera_serial_number(self):
tag = self.image_tag.get_image_tag('CameraSerialNumber')
return tag
def get_lens_make(self):
return self.image_tag.get_image_tag('lensmake')
def get_lens_model(self):
return self.image_tag.get_image_tag('lensmodel')
def get_lens_serial(self):
pass
def get_focal_length(self):
return self.image_tag.get_image_tag('focallength')
def get_photographer(self):
pass
def get_orientation(self):
pass
def get_sony_raw_type(self):
pass
def get_exposure_program(self):
pass
def get_photo_meta(self):
photo_meta = {
'photo': {
'aperture': self.aperture,
'camera_brand': self.camera_brand,
'camera_model': self.camera_model,
'dpi': self.dpi,
'is_jpg': self.is_jpg,
'is_raw': self.is_raw,
'iso': self.iso,
'lens_make': self.lens_make,
'lens_model': self.lens_model,
'lens_focal_length': self.focal_length,
'size': {
'height': self.size_height,
'width': self.size_width,
'width_height': self.size,
'megapixels': self.mpixels,
'ratio': self.size_ratio
}
}
}
return photo_meta

66
_preview.py Normal file
View File

@ -0,0 +1,66 @@
#!/usr/bin/env python
from PIL import Image
from _raw_photo import extract_jpg_thumb
from _video import VideoFile
from _verify_file_checksum import FileHash
class MediaPreview:
def __init__(self,path_file_name,media_files):
# super(MediaPreview, self).__init__()
self.path_file_name = path_file_name
self.media_files_list = media_files
# self.media_file = MediaFile(self.path_file_name)
self.source_path_hash = FileHash(self.path_file_name).path_hash
# print(f'_preview.py,MediaPreview:\n\tpath_file_name: {self.path_file_name}\n\thash: {self.source_path_hash}\n\tmedia_files_list: {self.media_files_list}\n')
self.thumbnail = 'thumbnail.jpg'
self.file_type = self.media_files_list[self.source_path_hash]['file_type']
if self.media_files_list[self.source_path_hash]['file_type'] == 'image':
self.thumbnail_ratio = self.media_files_list[self.source_path_hash]['image_meta']['photo']['size']['ratio']
self.mpixels = self.media_files_list[self.source_path_hash]['image_meta']['photo']['size']['megapixels']
self._img_preview()
elif self.media_files_list[self.source_path_hash]['file_type'] == 'video':
self._video_preview()
def _img_preview(self):
if self.media_files_list[self.source_path_hash]['image_meta']['photo']['is_jpg'] is True:
self._jpg_preview()
else:
self._raw_preview()
def _jpg_preview(self):
self.gen_thumb_from_jpg()
def gen_thumb_from_jpg(self):
"""Generates a thumbnail image from the given input image."""
thumb_width = 500
thumb_size = (
thumb_width,
int(thumb_width // self.media_files_list[self.source_path_hash]['image_meta']['photo']['size']['ratio'])
)
try:
with Image.open(self.path_file_name) as img:
img.thumbnail(thumb_size)
img.save(self.thumbnail, "JPEG")
except IOError:
print(f"Error: Cannot create thumbnail for '{self.path_file_name}'")
def _raw_preview(self):
self.thumbnail = extract_jpg_thumb(self.path_file_name)
def _video_preview(self):
vid = VideoFile(path_file_name=self.path_file_name)
self.thumbnail = vid.gen_video_thumbnail()
self.width = self.media_files_list[self.source_path_hash]['video_meta']['video']['size']['width']
self.height = self.media_files_list[self.source_path_hash]['video_meta']['video']['size']['height']
self.video_framerate = round(
int(self.media_files_list[self.source_path_hash]['video_meta']['video']['r_frame_rate'].split('/')[0]) /
int(self.media_files_list[self.source_path_hash]['video_meta']['video']['r_frame_rate'].split('/')[1]),2)
self.video_bit_depth = self.media_files_list[self.source_path_hash]['video_meta']['video']['bits_per_raw_sample']
self.video_duration = self.media_files_list[self.source_path_hash]['video_meta']['video']['duration']
self.video_encoding = self.media_files_list[self.source_path_hash]['video_meta']['video']['encoding_brand']
self.video_codec = self.media_files_list[self.source_path_hash]['video_meta']['video']['codec_long_name']
self.video_profile = self.media_files_list[self.source_path_hash]['video_meta']['video']['profile']
self.video_pix_format = self.media_files_list[self.source_path_hash]['video_meta']['video']['pix_fmt']

View File

@ -27,9 +27,18 @@ class WorkerSignals(QObject):
error = pyqtSignal(tuple) error = pyqtSignal(tuple)
result = pyqtSignal(object) result = pyqtSignal(object)
progress = pyqtSignal(int) progress = pyqtSignal(int)
import_progress = pyqtSignal(int)
current_file_progress = pyqtSignal(float) current_file_progress = pyqtSignal(float)
# current_import_file = pyqtSignal() imported_file_count = pyqtSignal(int)
found_file = pyqtSignal(str)
total_file_count = pyqtSignal(int)
checksum_progress = pyqtSignal(int)
checksum_file = pyqtSignal(str)
checksum_dialog_open = pyqtSignal(bool)
compare_checksums_source_file = pyqtSignal(str)
compare_checksums_dest_file = pyqtSignal(str)
compare_checksums_source_hash = pyqtSignal(str)
compare_checksums_dest_hash = pyqtSignal(str)
compare_checksums_add_row = pyqtSignal(dict)
class Worker(QRunnable): class Worker(QRunnable):
""" """
@ -55,9 +64,18 @@ class Worker(QRunnable):
# Add the callback to our kwargs # Add the callback to our kwargs
self.kwargs['progress_callback'] = self.signals.progress self.kwargs['progress_callback'] = self.signals.progress
self.kwargs['import_progress_callback'] = self.signals.import_progress
self.kwargs['current_file_progress_callback'] = self.signals.current_file_progress self.kwargs['current_file_progress_callback'] = self.signals.current_file_progress
# self.kwargs['current_import_file'] = self.signals.current_import_file self.kwargs['imported_file_count_callback'] = self.signals.imported_file_count
self.kwargs['found_file_callback'] = self.signals.found_file
self.kwargs['total_file_count_callback'] = self.signals.total_file_count
self.kwargs['checksum_file_callback'] = self.signals.checksum_file
self.kwargs['checksum_progress_callback'] = self.signals.checksum_progress
self.kwargs['checksum_dialog_open_callback'] = self.signals.checksum_dialog_open
self.kwargs['compare_checksums_source_file_callback'] = self.signals.compare_checksums_source_file
self.kwargs['compare_checksums_dest_file_callback'] = self.signals.compare_checksums_dest_file
self.kwargs['compare_checksums_source_hash_callback'] = self.signals.compare_checksums_source_hash
self.kwargs['compare_checksums_dest_hash_callback'] = self.signals.compare_checksums_dest_hash
self.kwargs['compare_checksums_add_row_callback'] = self.signals.compare_checksums_add_row
@pyqtSlot() @pyqtSlot()
def run(self): def run(self):

39
_time_and_date_utils.py Normal file
View File

@ -0,0 +1,39 @@
import time
from datetime import datetime
def convert_from_seconds(seconds):
return time.strftime("%H:%M:%S", time.gmtime(seconds))
def set_generic_date_time(datestamp='1900:01:01',
timestamp='00:00:00',
f='%Y:%m:%d %H:%M:%S'):
t = datetime.strptime(str(f'{datestamp} {timestamp}'), f)
return t
def process_img_date_tag(tag, image_tags, date_time_format):
try:
t = datetime.strptime(str(image_tags[tag]), date_time_format)
except:
t = None
return t
def get_img_date(image_tags):
t = None
for tag in image_tags:
if 'DateTime' in tag:
t = tag
break
for f in ['%Y:%m:%d %H:%M:%S',
'%Y/%m/%d %H:%M:%S',
'%Y-%m-%d-%H-%M-%S']:
dt_tag = process_img_date_tag(t,image_tags,f)
if dt_tag is not None:
break
if dt_tag is None:
dt_tag = set_generic_date_time()
return dt_tag

58
_verify_file_checksum.py Normal file
View File

@ -0,0 +1,58 @@
import os
import xxhash
class FileHash:
def __init__(self,
path_file,
chunk_size=1024 * 1024,
*args,
**kwargs):
super(FileHash,self).__init__(*args,**kwargs)
self.path_file = path_file
self._path = os.path.dirname(self.path_file)
self.chunk_size = chunk_size
self.path_hasher = xxhash.xxh64(self.path_file)
# noinspection PyArgumentList
# self.file_hash = self.get_hash(self.path_file)
self.path_hash = self.hash_path()
def get_hash(self,f,
progress_callback,
current_file_progress_callback,
imported_file_count_callback,
found_file_callback,
total_file_count_callback,
checksum_file_callback,
checksum_progress_callback,
checksum_dialog_open_callback,
compare_checksums_source_file_callback,
compare_checksums_dest_file_callback,
compare_checksums_source_hash_callback,
compare_checksums_dest_hash_callback,
compare_checksums_add_row_callback):
checksum_dialog_open_callback.emit(True)
size = os.path.getsize(f)
hasher = xxhash.xxh64()
chunk_count = 0
checksum_file_callback.emit(f)
with open(f, 'rb') as f:
for chunk in iter(lambda: f.read(self.chunk_size),b""):
hasher.update(chunk)
chunk_count += 1
hashed_size = chunk_count * self.chunk_size
checksum_progress_callback.emit(round((hashed_size / size) * 100, 1))
file_hash = hasher.hexdigest()
checksum_dialog_open_callback.emit(False)
return file_hash
def hash_path(self):
""" hashes a string passed as a path """
return self.path_hasher.hexdigest()

190
_video.py
View File

@ -1,127 +1,137 @@
#!/usr/bin/env python #!/usr/bin/env python
import os.path
import sys import sys
import ffmpeg import ffmpeg
import time import time
from datetime import datetime
class Video: class VideoFile:
def __init__(self,max_width=1024,*args,**kwargs): def __init__(self,path_file_name,max_width=1024,*args,**kwargs):
super(Video,self).__init__() # super(VideoFile, self).__init__(*args, **kwargs)
self.args = args self.path_file_name = path_file_name
self.kwargs = kwargs self.max_width = max_width
self.file = kwargs['file'] self.out = 'thumbnail.jpg'
self.out = 'thumbnail.jpg' self.probe = ffmpeg.probe(self.path_file_name)
self.max_width = max_width self.video_capture_date = self.get_video_capture_date()
self.video_stream = None
self.audio_stream = None
self.stream = { for i in self.probe['streams']:
'video': {}, if self.video_stream is None:
'format': {}, if 'video' == i['codec_type'].lower():
'audio': {} self.video_stream = i
} if self.audio_stream is None:
if 'audio' == i['codec_type'].lower():
self.audio_stream = i
self.stream['video']['duration'] = '' self.format_stream = self.probe['format']
self.stream['video']['creation_time'] = '' self.size_width = self.get_video_width()
self.stream['video']['encoding_brand'] = '' self.size_height = self.get_video_height()
self.stream['video']['codec_name'] = '' self.size = self.get_video_size()
self.stream['video']['codec_long_name'] = ''
self.stream['video']['profile'] = ''
self.stream['video']['size'] = {}
self.stream['video']['size']['width'] = int()
self.stream['video']['size']['height'] = int()
self.stream['video']['display_aspect_ratio'] = str()
self.stream['video']['level'] = ''
self.stream['video']['color_range'] = ''
self.stream['video']['field_order'] = ''
self.stream['video']['is_avc'] = ''
self.stream['video']['r_frame_rate'] = '' # -> expect for 120fps: 120000/1001
self.stream['video']['avg_frame_rate'] = '' # -> expect for 120fps: 120000/1001
self.stream['video']['time_base'] = '' # -> expect for 120fps: 1/120000
self.stream['video']['bit_rate'] = ''
self.stream['video']['bits_per_raw_sample'] = ''
self.stream['video']['nb_frames'] = ''
self.stream['video']['language'] = ''
self.stream['video']['handler_name'] = ''
self.stream['video']['encoder'] = ''
self.stream['video']['pix_fmt'] = ''
self.stream['format'] = {} self.stream = {}
self.stream['format']['major_brand'] = ''
self.stream['format']['compatible_brands'] = ''
self.stream['format']['creation_time'] = ''
self.stream['audio'] = {}
self.stream['audio']['codec_long_name'] = ''
self.stream['audio']['channels'] = ''
self.stream['audio']['sample_fmt'] = ''
self.stream['audio']['sample_rate'] = ''
self.stream['audio']['bits_per_sample'] = ''
self.stream['audio']['bit_rate'] = ''
@staticmethod @staticmethod
def convert_from_seconds(s): def convert_from_seconds(seconds):
seconds = s
return time.strftime("%H:%M:%S", time.gmtime(seconds)) return time.strftime("%H:%M:%S", time.gmtime(seconds))
def gen_video_thumbnail(self): def gen_video_thumbnail(self):
""" """
Generate a thumbnail from a video Generate a thumbnail from a video
""" """
self.get_video_meta() self.video_meta()
time = self.stream['video']['seconds'] // 5 time_seconds = self.stream['video']['seconds'] // 5
v_width = int(self.stream['video']['size']['width']) v_width = int(self.stream['video']['size']['width'])
width = self.set_thumb_width(v_width) width = self.set_thumb_width(v_width)
try: try:
( (
ffmpeg.input(self.file, ss=time) ffmpeg.input(
.filter('scale', width, -1) self.path_file_name,
.output(self.out, vframes=1) ss = time_seconds
)
.filter(
'scale',
width,
-1
)
.output(
self.out,
vframes = 1
)
.overwrite_output() .overwrite_output()
.run(capture_stdout=True, capture_stderr=True) .run(
capture_stdout = True,
capture_stderr = True
)
) )
except ffmpeg.Error as e: except ffmpeg.Error as e:
print(e.stderr.decode(), file=sys.stderr) print(e.stderr.decode(),
file = sys.stderr)
return self.out return self.out
def set_thumb_width(self, v_width): def set_thumb_width(self, v_width):
if v_width > self.max_width: if v_width > self.max_width:
width = self.max_width width = self.max_width
else: else:
width = v_width width = v_width
return width return width
def get_video_meta(self): def get_video_width(self):
probe = ffmpeg.probe(self.file) return self.video_stream['width']
if 'video' == probe['streams'][0]['codec_type'].lower(): def get_video_height(self):
video_stream = probe['streams'][0] return self.video_stream['height']
elif 'video' == probe['streams'][1]['codec_type'].lower():
video_stream = probe['streams'][1]
elif 'video' == probe['streams'][2]['codec_type'].lower():
video_stream = probe['streams'][2]
if 'audio' == probe['streams'][0]['codec_type'].lower(): def get_video_size(self):
audio_stream = probe['streams'][0] if self.size_width is None:
elif 'audio' == probe['streams'][1]['codec_type'].lower(): self.size_width = self.get_video_width()
audio_stream = probe['streams'][1] if self.size_height is None:
elif 'audio' == probe['streams'][2]['codec_type'].lower(): self.size_height = self.get_video_height()
audio_stream = probe['streams'][2] return f'{self.size_width}x{self.size_height}'
format_stream = probe['format'] def get_video_capture_date(self):
#TODO: refactor this try/except logic.
try:
stamp = datetime.strptime(
self.format_stream['tags']['creation_time'],
'%Y-%m-%dT%H:%M:%S.%f%z'
)
except:
try:
stamp = datetime.fromtimestamp(
os.path.getctime(
self.path_file_name
)
)
except:
stamp = datetime.strptime(
str('1900:01:01 00:00:00'),
'%Y:%m:%d %H:%M:%S'
)
return stamp
self.stream['video']['size']['width'] = video_stream['width'] def video_meta(self):
self.stream['video']['size']['height'] = video_stream['height'] self.stream = {
self.stream['video']['r_frame_rate'] = video_stream['r_frame_rate'] 'video': {
self.stream['video']['bits_per_raw_sample'] = video_stream['bits_per_raw_sample'] 'bits_per_raw_sample': self.video_stream['bits_per_raw_sample'],
self.stream['video']['seconds'] = float(video_stream['duration']) 'codec_long_name': self.video_stream['codec_long_name'],
self.stream['video']['duration'] = self.convert_from_seconds( 'seconds': float(self.video_stream['duration']),
self.stream['video']['seconds'] 'duration': self.convert_from_seconds(float(self.video_stream['duration'])),
) 'encoding_brand': self.format_stream['tags']['major_brand'],
self.stream['video']['encoding_brand'] = format_stream['tags']['major_brand'] 'pix_fmt': self.video_stream['pix_fmt'],
self.stream['video']['codec_long_name'] = video_stream['codec_long_name'] 'profile': self.video_stream['profile'],
self.stream['video']['profile'] = video_stream['profile'] 'r_frame_rate': self.video_stream['r_frame_rate'],
self.stream['video']['pix_fmt'] = video_stream['pix_fmt'] 'size': {
'width_height': self.size,
'height': self.video_stream['height'],
'width': self.video_stream['width']
}
},
'audio': {},
'format': {}
}
return self.stream return self.stream

View File

@ -1,2 +1,5 @@
pyuic6 BitMover.ui -o _BitMover_MainWindow.py pyuic6 BitMover.ui -o _BitMover_MainWindow.py
pyuic6 import_dialogue.ui -o _import_dialog_Window.py pyuic6 import_dialogue.ui -o _Window_import_dialog.py
pyuic6 _finding_files_dialog.ui -o _Window_finding_files_dialog.py
pyuic6 _ComparisonDialog.ui -o _Window_comparison_dialog.py
pyuic6 _checksum_progress_dialog.ui -o _Window_checksum_progress_dialog.py

1
scripts/start_qt6_designer.sh Executable file
View File

@ -0,0 +1 @@
qt6-tools designer