diff --git a/.gitignore b/.gitignore index 46ecd9e..8bc4d10 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ *.swp *.orig config.yaml +.DS_Store +.idea +__pycache__ +log +thumbnail.jpg +files_dict.yaml diff --git a/BitMover.ui b/BitMover.ui new file mode 100644 index 0000000..2d449f5 --- /dev/null +++ b/BitMover.ui @@ -0,0 +1,418 @@ + + + MainWindow + + + + 0 + 0 + 1473 + 928 + + + + MainWindow + + + + + + 20 + 10 + 871 + 121 + + + + + + + Source Directory + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + Browse + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + Destination Directory + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + Browse + + + + + + + + 0 + 0 + + + + + 16 + + + + Scan Directory + + + + .. + + + false + + + false + + + + + + + + + 20 + 150 + 871 + 701 + + + + + + + 910 + 580 + 551 + 251 + + + + + 0 + + + 0 + + + 0 + + + 20 + + + + + Camera + + + + + + + ISO + + + + + + + + + + + + + + Date / Time Created + + + + + + + Lens + + + + + + + Resolution (DPI) + + + + + + + Aperture + + + + + + + + + + + + + + + + + + + + + + + + + + + + Megapixels + + + + + + + + + + + + + + Width / Height + + + + + + + + + + + + + + + + + + + + + Zoom + + + + + + + + + + + + + + + + + + + + + + + 910 + 550 + 371 + 16 + + + + + 18 + true + + + + Exif / ffprobe Data + + + + + + 910 + 10 + 541 + 71 + + + + + + + + + + Event Label + + + + + + + + + 910 + 90 + 221 + 41 + + + + + + + + 18 + + + + Files Found + + + + + + + + + + + + 1140 + 90 + 311 + 46 + + + + + + + 24 + + + + + + + Current Progress + + + + + + + Overall Progress + + + + + + + 24 + + + + + + + + + 910 + 150 + 541 + 371 + + + + true + + + QFrame::StyledPanel + + + + + + + + + + 0 + 0 + 1473 + 24 + + + + + Bit Mover + + + + + + + + Scan Directory + + + + + + diff --git a/BitMover_MainWindow.py b/BitMover_MainWindow.py new file mode 100644 index 0000000..2f5f861 --- /dev/null +++ b/BitMover_MainWindow.py @@ -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")) diff --git a/BitMover_ui.py b/BitMover_ui.py new file mode 100755 index 0000000..f08bdeb --- /dev/null +++ b/BitMover_ui.py @@ -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() \ No newline at end of file diff --git a/assets/forklift.png b/assets/forklift.png new file mode 100644 index 0000000..0fa43a8 Binary files /dev/null and b/assets/forklift.png differ diff --git a/assets/preview_placeholder.jpg b/assets/preview_placeholder.jpg new file mode 100644 index 0000000..a1685b8 Binary files /dev/null and b/assets/preview_placeholder.jpg differ diff --git a/bitmover.py b/bitmover.py new file mode 100644 index 0000000..8e521cb --- /dev/null +++ b/bitmover.py @@ -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']) \ No newline at end of file diff --git a/configure.py b/configure.py new file mode 100644 index 0000000..c83439f --- /dev/null +++ b/configure.py @@ -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 \ No newline at end of file diff --git a/dedup.py b/dedup.py new file mode 100755 index 0000000..1ee3935 --- /dev/null +++ b/dedup.py @@ -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']))}") \ No newline at end of file diff --git a/dedup_wrapper.sh b/dedup_wrapper.sh new file mode 100755 index 0000000..8c21149 --- /dev/null +++ b/dedup_wrapper.sh @@ -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 + + diff --git a/file_stuff.py b/file_stuff.py new file mode 100644 index 0000000..c0a6f69 --- /dev/null +++ b/file_stuff.py @@ -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 \ No newline at end of file diff --git a/get_image_tag.py b/get_image_tag.py new file mode 100644 index 0000000..8035d13 --- /dev/null +++ b/get_image_tag.py @@ -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 \ No newline at end of file diff --git a/hashing.py b/hashing.py new file mode 100644 index 0000000..ab8012a --- /dev/null +++ b/hashing.py @@ -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 \ No newline at end of file diff --git a/import_media.py b/import_media.py index b5e2759..11586dc 100755 --- a/import_media.py +++ b/import_media.py @@ -14,30 +14,22 @@ TODO: 12. Make a graphical interface """ -import os -import sys -from pprint import pprint import argparse -import shutil -import hashlib -import xxhash -from datetime import datetime +import os from tqdm import tqdm -import yaml -import exifread -import ffmpeg -CONFIG_FILE = 'config.yaml' -files = {} +### 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 cf: - config = yaml.load(cf, Loader=yaml.FullLoader) -except FileNotFoundError: - print("Configuration file not found: ", CONFIG_FILE) - print("Copy config.yaml.EXAMPLE to ", CONFIG_FILE, " and update accordingly.") - sys.exit() +c = Configure(CONFIG_FILE) +config = c.load_config() +log = timber(__name__) +log.info("Starting import_media") parser = argparse.ArgumentParser() parser.add_argument("-e", "--event", help = "Event Name") @@ -77,403 +69,42 @@ if args.destination: #if args.generate-config: # 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): """ 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): + 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): current_file = os.path.join(folder,file) + log.debug(f'Current 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: - 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(): - """ 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() +GO = validate_config_dir_access(config) if GO is True: find_files(config['folders']['source']['base']) - copy_files() - gen_xxhashes() - validate_xx_checksums() - cleanup_sd() + copy_files(files,config) + gen_xxhashes(files) + validate_xx_checksums(files) + cleanup_sd(files,config) 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') -print('done.') +# dump_yaml(files, 'files_dict.yaml') +log.info('done.') diff --git a/lumberjack.py b/lumberjack.py new file mode 100644 index 0000000..1b72ca5 --- /dev/null +++ b/lumberjack.py @@ -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 diff --git a/media.py b/media.py new file mode 100644 index 0000000..f70272c --- /dev/null +++ b/media.py @@ -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 diff --git a/qodana.yaml b/qodana.yaml new file mode 100644 index 0000000..76fb7ad --- /dev/null +++ b/qodana.yaml @@ -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: + +#Disable inspections +#exclude: +# - name: +# paths: +# - + +#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 can be found at https://plugins.jetbrains.com) + +#Specify Qodana linter for analysis (Applied in CI/CD pipeline) +linter: jetbrains/qodana-:latest diff --git a/raw_photo.py b/raw_photo.py new file mode 100644 index 0000000..eed76e4 --- /dev/null +++ b/raw_photo.py @@ -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.") \ No newline at end of file diff --git a/rename.py b/rename.py new file mode 100755 index 0000000..47333c8 --- /dev/null +++ b/rename.py @@ -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) \ No newline at end of file diff --git a/import_media_ui.py b/thread_my_stuff.py similarity index 100% rename from import_media_ui.py rename to thread_my_stuff.py diff --git a/ui_main_widgets.py b/ui_main_widgets.py new file mode 100644 index 0000000..e58c795 --- /dev/null +++ b/ui_main_widgets.py @@ -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) \ No newline at end of file