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