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