diff --git a/BitMover.ui b/BitMover.ui index df8b75d..60cbb46 100644 --- a/BitMover.ui +++ b/BitMover.ui @@ -11,7 +11,13 @@ - MainWindow + BitMover + + + + + + true @@ -88,7 +94,7 @@ 910 - 650 + 610 311 211 @@ -292,7 +298,7 @@ 910 - 630 + 590 371 16 @@ -334,11 +340,55 @@ 910 50 - 182 - 91 + 541 + 41 - + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 180 + 20 + + + + + + + + + 18 + + + + Files Imported + + + + + + + + 80 + 0 + + + + + 80 + 16777215 + + + + @@ -351,120 +401,25 @@ - - - - - - - - - - - 18 - + + + + + 0 + 0 + - - Files Imported + + + 80 + 0 + - - - - - - - - 1100 - 50 - 351 - 93 - - - - - - - 24 - - - - - - - QFrame::NoFrame - - - - - - - QFrame::NoFrame - - - - - - - Processing Progress - - - - - - - Current File Progress - - - - - - - 24 - - - - - - - 24 - - - - - - - Import Progress - - - - - - - QFrame::NoFrame - - - QFrame::Plain - - - - - - - % - - - - - - - % - - - - - - - % + + + 80 + 16777215 + @@ -474,7 +429,7 @@ 910 - 150 + 110 541 371 @@ -493,7 +448,7 @@ 910 - 530 + 490 541 91 @@ -643,7 +598,8 @@ Import Media - + + .. @@ -685,7 +641,8 @@ Images - + + .. true @@ -698,7 +655,8 @@ Video - + + .. true @@ -711,7 +669,8 @@ Audio - + + .. true @@ -755,7 +714,7 @@ 0 0 1463 - 24 + 21 diff --git a/BitMover_ui.py b/BitMover_ui.py index 4615f43..45c7d1b 100755 --- a/BitMover_ui.py +++ b/BitMover_ui.py @@ -2,23 +2,19 @@ import os import sys -from os import path, rename - from PyQt6.QtCore import QThreadPool from PyQt6.QtGui import QIcon, QPixmap from PyQt6.QtWidgets import QMainWindow, QApplication, QFileDialog from _BitMover_MainWindow import Ui_MainWindow -from _import_dialog import DialogImport from _configure import CONFIG_FILE, Configure -from _file_stuff import is_file, create_folder, path_exists, cmp_files -from _hashing import xx_hash -from _img_preview import ImgPreview -from _lumberjack import timber -from _media import Media +from _find_files import FindFiles +from _find_files_dialog import FindProgress +from _import_dialog import DialogImport +from _media_import import MediaImporter +from _preview import MediaPreview from _thread_my_stuff import Worker -log = timber(__name__) basedir = os.path.dirname(__file__) # TODO: verify source dir actually exists @@ -30,20 +26,20 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.setupUi(self) self.setWindowTitle("BitMover") self.setWindowIcon(QIcon(os.path.join(basedir,'assets', 'forklift.ico'))) - self.threadpool = QThreadPool() - c = Configure(CONFIG_FILE) - self.config = c.load_config() - self.src_dir = self.config['folders']['source']['base'] - self.dst_dir = self.config['folders']['destination']['base'] - self.verify_checksum = self.config['verify_checksum'] - self.cleanup_files = self.config['cleanup_sd'] - self.store_originals = self.config['store_originals'] - self.file_types = self.config['file_types'] + self.threadpool = QThreadPool() + c = Configure(CONFIG_FILE) + self.config = c.load_config() + self.src_dir = self.config['folders']['source']['base'] + self.dst_dir = self.config['folders']['destination']['base'] + self.verify_checksum = self.config['verify_checksum'] + self.cleanup_files = self.config['cleanup_sd'] + self.store_originals = self.config['store_originals'] + self.file_types = self.config['file_types'] # File Stuff - self.total_files = 0 - self.file_total = 0 - self.files = {} - self.imp_dialog = DialogImport() + self.total_files = 0 + self.files = {} + self.imp_dialog = DialogImport() + self.find_files_dialog = FindProgress() self.widgets_config() @@ -60,13 +56,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.toggle_scan_button(True) self.toggle_import_button(False) self.lcd_files_found.display(int(0)) - self.set_progress_processing(0) - self.set_progress_importing(0) - self.set_progress_current_file(0) - self.img_preview.setPixmap(QPixmap(os.path.join(basedir, - 'assets', - 'preview_placeholder.jpg'))) - self.img_preview.setScaledContents(True) + self.set_default_thumbnail() self.file_list.currentItemChanged.connect(self.index_changed) self.checkBox_verify_checksum.setChecked(self.verify_checksum) self.checkBox_cleanup_files.setChecked(self.cleanup_files) @@ -79,51 +69,88 @@ class MainWindow(QMainWindow, Ui_MainWindow): # Setup thread pool print("Multithreading with maximum %d threads" % self.threadpool.maxThreadCount()) + def toggle_scan_button(self,enable=True): + print(f'toggle_scan_button.enabled: {enable}') + self.pushButton_3_scan_dir.setEnabled(enable) + + def toggle_import_button(self,enable=True): + self.pushButton_import.setEnabled(enable) + + 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 verify_checksum_changed(self): if self.checkBox_verify_checksum.isChecked(): - self.config['verify_checksum'] = True + self.config['verify_checksum'] = True else: - self.config['verify_checksum'] = False + self.config['verify_checksum'] = False print(f"verify_checksums: {self.config['verify_checksums']}") def cleanup_files_changed(self): if self.checkBox_cleanup_files.isChecked(): - self.config['cleanup_sd'] = True + self.config['cleanup_sd'] = True else: - self.config['cleanup_sd'] = False + self.config['cleanup_sd'] = False print(f"cleanup_sd: {self.config['cleanup_sd']}") def store_originals_changed(self): if self.checkBox_store_originals.isChecked(): - self.config['store_originals'] = True + self.config['store_originals'] = True else: - self.config['store_originals'] = False + self.config['store_originals'] = False print(f"store_originals: {self.config['store_originals']}") - def toggle_scan_button(self,enable=True): - self.pushButton_3_scan_dir.setEnabled(enable) + def set_thumbnail(self,thumb_file,scaled=True,ratio=None): + self.img_preview.setPixmap(QPixmap(thumb_file)) + self.img_preview.setScaledContents(scaled) + if ratio is not None: + self.img_preview.setFixedHeight(self.img_preview.width() / ratio) - def toggle_import_button(self,enable=True): - self.pushButton_import.setEnabled(enable) + def set_default_thumbnail(self): + self.set_thumbnail(os.path.join(basedir, + 'assets', + 'preview_placeholder.jpg')) - def update_preview(self,i): - preview = ImgPreview(file=i.text(), event=self.get_event(), config=self.config) + def get_preview(self,i): + preview = MediaPreview(path_file_name = i.text(), + media_files = self.files) + return preview - self.l_meta_content_date_time_c.setText(preview.dtc) - path_hash = preview.path_hash + def update_preview(self,preview): + self.set_thumbnail(preview.thumbnail,ratio=preview.thumbnail_ratio) + def update_metadata(self,preview): + self.clear_metadata() + path_hash = preview.source_path_hash + f = self.files[path_hash] + f_date = f['date']['capture_date'] + f_video = f['video'] self.l_data_file_source_path.setText( self.files[path_hash]['folders']['source_path']) self.l_data_file_dest_path.setText( self.files[path_hash]['folders']['destination']) - - self.img_preview.setPixmap(QPixmap(preview.thumbnail)) - self.img_preview.setFixedHeight(self.img_preview.width() / preview.thumbnail_ratio) - - self.update_metadata(preview) - - def update_metadata(self,preview): - self.clear_metadata() + self.l_meta_content_date_time_c.setText( + f"{f_date['y']}/{f_date['m']}/{f_date['d']}" + ) if preview.file_type == 'image': self.l_meta_01.setText('Size') self.l_meta_02.setText('dpi') @@ -133,14 +160,14 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.l_meta_06.setText('Camera') self.l_meta_07.setText('Aperture') self.l_meta_08.setText('Megapixels') - self.l_meta_content_01.setText(str(preview.size)) - self.l_meta_content_02.setText(str(preview.dpi)) - self.l_meta_content_03.setText(str(preview.iso)) - self.l_meta_content_04.setText(str(preview.lens)) - self.l_meta_content_05.setText(str(preview.zoom)) - self.l_meta_content_06.setText(str(preview.camera)) - self.l_meta_content_07.setText(str(preview.aperture)) - self.l_meta_content_08.setText(str(preview.mpixels)) + self.l_meta_content_01.setText(str(f['photo']['size']['width_height'])) + self.l_meta_content_02.setText(str(f['photo']['dpi'])) + self.l_meta_content_03.setText(str(f['photo']['iso'])) + self.l_meta_content_04.setText(str(f['photo']['lens_model'])) + self.l_meta_content_05.setText(str(f['photo']['focal_length'])) + self.l_meta_content_06.setText(str(f"{f['photo']['camera_brand']} {f['photo']['camera_model']}")) + self.l_meta_content_07.setText(str(f['photo']['aperture'])) + self.l_meta_content_08.setText(str(f['photo']['megapixels'])) elif preview.file_type == 'video': self.l_meta_01.setText('Size') @@ -152,14 +179,14 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.l_meta_07.setText('Profile') self.l_meta_08.setText('Pix Format') - self.l_meta_content_01.setText(str(preview.size)) - self.l_meta_content_02.setText(str(preview.video_framerate)) - self.l_meta_content_03.setText(str(preview.video_bit_depth)) - self.l_meta_content_04.setText(str(preview.video_duration)) - self.l_meta_content_05.setText(str(preview.video_encoding)) - self.l_meta_content_06.setText(str(preview.video_codec)) - self.l_meta_content_07.setText(str(preview.video_profile)) - self.l_meta_content_08.setText(str(preview.video_pix_format)) + self.l_meta_content_01.setText(str(f_video['size']['width_height'])) + self.l_meta_content_02.setText(str(f_video['r_framerate'])) + self.l_meta_content_03.setText(str(f_video['bit_depth'])) + self.l_meta_content_04.setText(str(f_video['duration'])) + self.l_meta_content_05.setText(str(f_video['encoding'])) + self.l_meta_content_06.setText(str(f_video['codec'])) + self.l_meta_content_07.setText(str(f_video['profile'])) + self.l_meta_content_08.setText(str(f_video['pix_format'])) def clear_metadata(self): self.l_meta_01.setText('') @@ -182,241 +209,23 @@ class MainWindow(QMainWindow, Ui_MainWindow): def index_changed(self,i): self.clear_metadata() if i is None: - self.img_preview.setPixmap(QPixmap(os.path.join(basedir, - 'assets', - 'preview_placeholder.jpg'))) + self.set_default_thumbnail() else: - self.update_preview(i) + preview = self.get_preview(i) + self.update_preview(preview) + self.update_metadata(preview) - 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 get_event(self): + event_name = self.eventName.text() + return event_name def set_total_files(self,t): - self.total_files = t - self.lcd_files_found.display(self.total_files) + total_file_count = t + self.lcd_files_found.display(total_file_count) def set_imported_files(self,t): self.lcd_files_imported.display(t) - def set_progress_processing(self, n): - # print("%d%% done" % n) - self.progressBar_processing.setValue(int(n)) - self.lcd_processing_progress.display(n) - - def set_progress_importing(self, n): - # print("%d%% done" % n) - self.progressBar_importing.setValue(int(n)) - self.lcd_import_progress.display(n) - - def set_progress_current_file(self, n): - # print("%d%% done" % n) - self.progressBar_importing_2.setValue(int(n)) - self.lcd_current_file_progress.display(n) - - def get_t_files(self,search_types): - for folder, subfolders, filename in os.walk(self.src_dir): - for f_type in search_types: - for ext in self.file_types[f_type]: - for file in filename: - if file.lower().endswith(ext): - current_file = os.path.join(folder, file) - if is_file(current_file): - self.file_total += int(1) - else: - print(f"Skipping {current_file} as it does not look like a real file.") - - def t_find_files(self, - progress_callback, - import_progress_callback, - current_file_progress_callback,): - file_count = int(0) - search_types = [] - - if self.checkBox_search_for_images.isChecked(): - search_types.append('image') - if self.checkBox_search_for_video.isChecked(): - search_types.append('video') - if self.checkBox_search_for_audio.isChecked(): - search_types.append('audio') - - self.get_t_files(search_types) - self.set_total_files(self.file_total) - - if len(search_types) > 0: - for folder, subfolders, filename in os.walk(self.src_dir): - for f_type in search_types: - for ext in self.file_types[f_type]: - 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(current_file) - else: - print(f"Skipping {current_file} as it does not look like a real file.") - - progress_callback.emit(round((file_count / self.file_total) * 100, 0)) - else: - print("Nothing to search for.") - - return "Done." - - def t_copy_files(self, - progress_callback, - import_progress_callback, - current_file_progress_callback): - """ Copy Files. """ - - # imp_dialog = DialogImport() - count = int(0) - chunk_size = 16 * 1024 - - for file in self.files: - self.imp_dialog.set_importing_file(path.join( - self.files[file]['folders']['source_path'], - self.files[file]['name'])) - - create_folder(self.files[file]['folders']['destination']) - if self.files[file]['type'] == 'video': - chunk_size = (1024 * 1024) * 5 - - file_exists = path_exists(path.join( - self.files[file]['folders']['destination'], - self.files[file]['name'])) - - if file_exists is True: - check_match = cmp_files( - path.join( - self.files[file]['folders']['source_path'], - self.files[file]['name']), - path.join( - self.files[file]['folders']['destination'], - self.files[file]['name'] - ) - ) - - if check_match is False: - print(f'\nFound duplicate for {self.files[file]["folders"]["source_path"]}/{self.files[file]["name"]}, renaming destination with hash appended.') - base, extension = path.splitext(self.files[file]['name']) - f_xxhash = xx_hash(path.join( - self.files[file]['folders']['destination'], - self.files[file]['name'])) - file_name_hash = base + '_' + f_xxhash + extension - rename(path.join( - self.files[file]['folders']['destination'], - self.files[file]['name']), - path.join( - self.files[file]['folders']['destination'], - file_name_hash)) - size = path.getsize( - path.join( - self.files[file]['folders']['source_path'], - self.files[file]['name']) - ) - - with open(path.join( - self.files[file]['folders']['source_path'], - self.files[file]['name']), 'rb') as fs: - with open( - path.join( - self.files[file]['folders']['destination'], - self.files[file]['name']), 'wb') as fd: - while True: - chunk = fs.read(chunk_size) - if not chunk: - break - fd.write(chunk) - dst_size = path.getsize( - path.join( - self.files[file]['folders']['destination'], - self.files[file]['name'] - ) - ) - current_file_progress_callback.emit(round(( dst_size / size ) * 100, 1)) - - if self.config['store_originals'] is True: - if self.files[file]['type'] == 'image': - create_folder(self.files[file]['folders']['destination_original']) - - file_exists = path_exists(path.join( - self.files[file]['folders']['destination_original'], - self.files[file]['name'])) - - if file_exists is True: - check_match = cmp_files( - path.join( - self.files[file]['folders']['source_path'], - self.files[file]['name']), - path.join( - self.files[file]['folders']['destination_original'], - self.files[file]['name'] - ) - ) - - if check_match is False: - print(f'\nFound duplicate for {self.files[file]["folders"]["source_path"]}/{self.files[file]["name"]}, renaming destination with hash appended.') - base, extension = path.splitext(self.files[file]['name']) - f_xxhash = xx_hash(path.join( - self.files[file]['folders']['destination_original'], - self.files[file]['name'])) - file_name_hash = base + '_' + f_xxhash + extension - rename(path.join( - self.files[file]['folders']['destination_original'], - self.files[file]['name']), - path.join( - self.files[file]['folders']['destination_original'], - file_name_hash)) - - size = path.getsize( - path.join( - self.files[file]['folders']['source_path'], - self.files[file]['name']) - ) - - with open(path.join( - self.files[file]['folders']['source_path'], - self.files[file]['name']), 'rb') as fs: - with open( - path.join( - self.files[file]['folders']['destination_original'], - self.files[file]['name']), 'wb') as fd: - while True: - chunk = fs.read(chunk_size) - if not chunk: - break - fd.write(chunk) - dst_size = path.getsize( - path.join( - self.files[file]['folders']['destination_original'], - self.files[file]['name'] - ) - ) - current_file_progress_callback.emit(round((dst_size / size) * 100, 1)) - count += 1 - self.set_imported_files(count) - import_progress_callback.emit(round((count / self.file_total) * 100, 1)) - self.imp_dialog.add_to_imported_list( - path.join( - self.files[file]['folders']['source_path'], - self.files[file]['name'])) - @staticmethod def print_output(s): print(s) @@ -438,29 +247,39 @@ class MainWindow(QMainWindow, Ui_MainWindow): def thread_complete(): print("THREAD COMPLETE.") + def add_found_file_to_list(self,f): + self.file_list.addItem(f) + + def gen_file_dict(self,d): + self.files = d + def find_files(self): """ find files to build a dictionary out of """ + self.files = {} - # Initialize widgets - self.lcd_files_found.display(int(0)) - self.set_progress_processing(0) - self.set_progress_importing(0) - self.img_preview.setPixmap(QPixmap(os.path.join(basedir, - 'assets', - 'preview_placeholder.jpg'))) - self.file_list.clear() + self.set_total_files(0) - # File Stuff - self.total_files = 0 - self.file_total = 0 - self.files = {} + file_finder = FindFiles() + worker = Worker(file_finder.t_find_files) - worker = Worker(self.t_find_files) - worker.signals.started.connect(self.worker_thread_started) - worker.signals.result.connect(self.print_output) - worker.signals.finished.connect(self.thread_complete) - worker.signals.finished.connect(self.worker_thread_done) - worker.signals.progress.connect(self.set_progress_processing) + worker.signals.started.connect( + self.worker_thread_started) + worker.signals.started.connect( + self.find_files_dialog.open_find_files_dialog) + worker.signals.progress.connect( + self.find_files_dialog.set_progress_finding_files) + worker.signals.found_file.connect( + self.add_found_file_to_list) + worker.signals.total_file_count.connect( + self.set_total_files) + worker.signals.result.connect( + self.gen_file_dict) + worker.signals.finished.connect( + self.thread_complete) + worker.signals.finished.connect( + self.worker_thread_done) + worker.signals.finished.connect( + self.find_files_dialog.close_find_files_dialog) # Execute. self.threadpool.start(worker) @@ -470,73 +289,46 @@ class MainWindow(QMainWindow, Ui_MainWindow): Import found files """ - # Open Dialog - # imp_dialog = DialogImport() - # imp_dialog.open_import_dialog() - # Initialize Widgets self.lcd_files_imported.display(int(0)) - self.set_progress_importing(0) - self.set_progress_current_file(0) self.imp_dialog.set_progress_importing(0) self.imp_dialog.set_progress_current_file(0) - worker = Worker(self.t_copy_files) - worker.signals.started.connect(self.worker_thread_started) - worker.signals.started.connect(self.imp_dialog.open_import_dialog) - worker.signals.result.connect(self.print_output) - worker.signals.finished.connect(self.thread_complete) - worker.signals.finished.connect(self.worker_thread_done) - worker.signals.import_progress.connect(self.set_progress_importing) - worker.signals.import_progress.connect(self.imp_dialog.set_progress_importing) - worker.signals.current_file_progress.connect(self.set_progress_current_file) - worker.signals.current_file_progress.connect(self.imp_dialog.set_progress_current_file) + importer = MediaImporter() + + worker = Worker(importer.t_copy_files) + + worker.signals.started.connect( + self.worker_thread_started) + worker.signals.started.connect( + self.imp_dialog.open_import_dialog) + + worker.signals.import_progress.connect( + self.imp_dialog.set_progress_importing) + worker.signals.current_file_progress.connect( + self.imp_dialog.set_progress_current_file) + worker.signals.imported_file_count.connect( + self.set_imported_files) + + worker.signals.result.connect( + self.print_output) + worker.signals.finished.connect( + self.thread_complete) + worker.signals.finished.connect( + self.worker_thread_done) # Execute self.threadpool.start(worker) - def get_event(self): - event_name = self.eventName.text() - return event_name + def verify_checksum(self): + # fh_match = FileHash() + print(self.config) - def process_file(self,p): - """ gather information and add to dictionary """ - path_name = os.path.dirname(p) - f_name = os.path.basename(p) +app = QApplication(sys.argv) - event = self.get_event() - c = self.config - - # print(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': {}, - 'event': {} - } - - 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.files[i]['event']['name'] = m.event_name - self.file_list.addItem(f"{self.files[i]['folders']['source_path']}/{self.files[i]['name']}") - -app = QApplication(sys.argv) - -window = MainWindow() +window = MainWindow() window.show() app.exec() \ No newline at end of file diff --git a/_BitMover_MainWindow.py b/_BitMover_MainWindow.py index 87108a0..cea5097 100644 --- a/_BitMover_MainWindow.py +++ b/_BitMover_MainWindow.py @@ -13,6 +13,9 @@ class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName("MainWindow") MainWindow.resize(1463, 928) + icon = QtGui.QIcon.fromTheme("applications-science") + MainWindow.setWindowIcon(icon) + MainWindow.setUnifiedTitleAndToolBarOnMac(True) self.centralwidget = QtWidgets.QWidget(parent=MainWindow) self.centralwidget.setObjectName("centralwidget") self.gridLayoutWidget = QtWidgets.QWidget(parent=self.centralwidget) @@ -47,7 +50,7 @@ class Ui_MainWindow(object): self.file_list.setGeometry(QtCore.QRect(20, 160, 871, 701)) self.file_list.setObjectName("file_list") self.gridLayoutWidget_2 = QtWidgets.QWidget(parent=self.centralwidget) - self.gridLayoutWidget_2.setGeometry(QtCore.QRect(910, 650, 311, 211)) + self.gridLayoutWidget_2.setGeometry(QtCore.QRect(910, 610, 311, 211)) self.gridLayoutWidget_2.setObjectName("gridLayoutWidget_2") self.grid_metadata = QtWidgets.QGridLayout(self.gridLayoutWidget_2) self.grid_metadata.setContentsMargins(0, 0, 0, 0) @@ -154,7 +157,7 @@ class Ui_MainWindow(object): self.grid_metadata.addWidget(self.l_meta_content_02, 2, 1, 1, 1) self.grid_metadata.setColumnStretch(1, 1) self.l_exif_ffprobe_title = QtWidgets.QLabel(parent=self.centralwidget) - self.l_exif_ffprobe_title.setGeometry(QtCore.QRect(910, 630, 371, 16)) + self.l_exif_ffprobe_title.setGeometry(QtCore.QRect(910, 590, 371, 16)) font = QtGui.QFont() font.setPointSize(18) font.setBold(True) @@ -173,87 +176,48 @@ class Ui_MainWindow(object): self.eventName.setObjectName("eventName") self.gridLayout.addWidget(self.eventName, 0, 1, 1, 1) self.gridLayoutWidget_4 = QtWidgets.QWidget(parent=self.centralwidget) - self.gridLayoutWidget_4.setGeometry(QtCore.QRect(910, 50, 182, 91)) + self.gridLayoutWidget_4.setGeometry(QtCore.QRect(910, 50, 541, 41)) self.gridLayoutWidget_4.setObjectName("gridLayoutWidget_4") self.gridLayout_2 = QtWidgets.QGridLayout(self.gridLayoutWidget_4) self.gridLayout_2.setContentsMargins(0, 0, 0, 0) self.gridLayout_2.setObjectName("gridLayout_2") + spacerItem = QtWidgets.QSpacerItem(180, 20, QtWidgets.QSizePolicy.Policy.Maximum, QtWidgets.QSizePolicy.Policy.Minimum) + self.gridLayout_2.addItem(spacerItem, 0, 2, 1, 1) + self.label_4 = QtWidgets.QLabel(parent=self.gridLayoutWidget_4) + font = QtGui.QFont() + font.setPointSize(18) + self.label_4.setFont(font) + self.label_4.setObjectName("label_4") + self.gridLayout_2.addWidget(self.label_4, 0, 3, 1, 1) + self.lcd_files_found = QtWidgets.QLCDNumber(parent=self.gridLayoutWidget_4) + self.lcd_files_found.setMinimumSize(QtCore.QSize(80, 0)) + self.lcd_files_found.setMaximumSize(QtCore.QSize(80, 16777215)) + self.lcd_files_found.setObjectName("lcd_files_found") + self.gridLayout_2.addWidget(self.lcd_files_found, 0, 1, 1, 1) self.label_3 = QtWidgets.QLabel(parent=self.gridLayoutWidget_4) 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.lcd_files_imported = QtWidgets.QLCDNumber(parent=self.gridLayoutWidget_4) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.lcd_files_imported.sizePolicy().hasHeightForWidth()) + self.lcd_files_imported.setSizePolicy(sizePolicy) + self.lcd_files_imported.setMinimumSize(QtCore.QSize(80, 0)) + self.lcd_files_imported.setMaximumSize(QtCore.QSize(80, 16777215)) self.lcd_files_imported.setObjectName("lcd_files_imported") - self.gridLayout_2.addWidget(self.lcd_files_imported, 1, 1, 1, 1) - self.label_4 = QtWidgets.QLabel(parent=self.gridLayoutWidget_4) - font = QtGui.QFont() - font.setPointSize(18) - self.label_4.setFont(font) - self.label_4.setObjectName("label_4") - self.gridLayout_2.addWidget(self.label_4, 1, 0, 1, 1) - self.gridLayout_2.setColumnStretch(1, 1) - self.gridLayoutWidget_5 = QtWidgets.QWidget(parent=self.centralwidget) - self.gridLayoutWidget_5.setGeometry(QtCore.QRect(1100, 50, 351, 93)) - self.gridLayoutWidget_5.setObjectName("gridLayoutWidget_5") - self.gridLayout_3 = QtWidgets.QGridLayout(self.gridLayoutWidget_5) - self.gridLayout_3.setContentsMargins(0, 0, 0, 0) - self.gridLayout_3.setObjectName("gridLayout_3") - self.progressBar_importing_2 = QtWidgets.QProgressBar(parent=self.gridLayoutWidget_5) - self.progressBar_importing_2.setProperty("value", 24) - self.progressBar_importing_2.setObjectName("progressBar_importing_2") - self.gridLayout_3.addWidget(self.progressBar_importing_2, 2, 1, 1, 1) - self.lcd_import_progress = QtWidgets.QLCDNumber(parent=self.gridLayoutWidget_5) - self.lcd_import_progress.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) - self.lcd_import_progress.setObjectName("lcd_import_progress") - self.gridLayout_3.addWidget(self.lcd_import_progress, 1, 2, 1, 1) - self.lcd_current_file_progress = QtWidgets.QLCDNumber(parent=self.gridLayoutWidget_5) - self.lcd_current_file_progress.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) - self.lcd_current_file_progress.setObjectName("lcd_current_file_progress") - self.gridLayout_3.addWidget(self.lcd_current_file_progress, 2, 2, 1, 1) - self.l_proecessing_progress = QtWidgets.QLabel(parent=self.gridLayoutWidget_5) - self.l_proecessing_progress.setObjectName("l_proecessing_progress") - self.gridLayout_3.addWidget(self.l_proecessing_progress, 0, 0, 1, 1) - self.l_current_file_progress = QtWidgets.QLabel(parent=self.gridLayoutWidget_5) - self.l_current_file_progress.setObjectName("l_current_file_progress") - self.gridLayout_3.addWidget(self.l_current_file_progress, 2, 0, 1, 1) - self.progressBar_processing = QtWidgets.QProgressBar(parent=self.gridLayoutWidget_5) - self.progressBar_processing.setProperty("value", 24) - self.progressBar_processing.setObjectName("progressBar_processing") - self.gridLayout_3.addWidget(self.progressBar_processing, 0, 1, 1, 1) - self.progressBar_importing = QtWidgets.QProgressBar(parent=self.gridLayoutWidget_5) - self.progressBar_importing.setProperty("value", 24) - self.progressBar_importing.setObjectName("progressBar_importing") - self.gridLayout_3.addWidget(self.progressBar_importing, 1, 1, 1, 1) - self.l_import_progress = QtWidgets.QLabel(parent=self.gridLayoutWidget_5) - self.l_import_progress.setObjectName("l_import_progress") - self.gridLayout_3.addWidget(self.l_import_progress, 1, 0, 1, 1) - self.lcd_processing_progress = QtWidgets.QLCDNumber(parent=self.gridLayoutWidget_5) - self.lcd_processing_progress.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) - self.lcd_processing_progress.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) - self.lcd_processing_progress.setObjectName("lcd_processing_progress") - self.gridLayout_3.addWidget(self.lcd_processing_progress, 0, 2, 1, 1) - self.label_2 = QtWidgets.QLabel(parent=self.gridLayoutWidget_5) - self.label_2.setObjectName("label_2") - self.gridLayout_3.addWidget(self.label_2, 0, 3, 1, 1) - self.label_5 = QtWidgets.QLabel(parent=self.gridLayoutWidget_5) - self.label_5.setObjectName("label_5") - self.gridLayout_3.addWidget(self.label_5, 1, 3, 1, 1) - self.label_6 = QtWidgets.QLabel(parent=self.gridLayoutWidget_5) - self.label_6.setObjectName("label_6") - self.gridLayout_3.addWidget(self.label_6, 2, 3, 1, 1) + self.gridLayout_2.addWidget(self.lcd_files_imported, 0, 4, 1, 1) self.img_preview = QtWidgets.QLabel(parent=self.centralwidget) - self.img_preview.setGeometry(QtCore.QRect(910, 150, 541, 371)) + self.img_preview.setGeometry(QtCore.QRect(910, 110, 541, 371)) self.img_preview.setAutoFillBackground(True) self.img_preview.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) self.img_preview.setText("") self.img_preview.setObjectName("img_preview") self.gridLayoutWidget_6 = QtWidgets.QWidget(parent=self.centralwidget) - self.gridLayoutWidget_6.setGeometry(QtCore.QRect(910, 530, 541, 91)) + self.gridLayoutWidget_6.setGeometry(QtCore.QRect(910, 490, 541, 91)) self.gridLayoutWidget_6.setObjectName("gridLayoutWidget_6") self.grid_metadata_2 = QtWidgets.QGridLayout(self.gridLayoutWidget_6) self.grid_metadata_2.setContentsMargins(0, 0, 0, 0) @@ -298,8 +262,8 @@ class Ui_MainWindow(object): self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget_2) self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0) self.horizontalLayout_2.setObjectName("horizontalLayout_2") - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) - self.horizontalLayout_2.addItem(spacerItem) + spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.horizontalLayout_2.addItem(spacerItem1) self.checkBox_verify_checksum = QtWidgets.QCheckBox(parent=self.horizontalLayoutWidget_2) self.checkBox_verify_checksum.setChecked(True) self.checkBox_verify_checksum.setObjectName("checkBox_verify_checksum") @@ -327,8 +291,8 @@ class Ui_MainWindow(object): self.label = QtWidgets.QLabel(parent=self.horizontalLayoutWidget_3) self.label.setObjectName("label") self.horizontalLayout_3.addWidget(self.label) - spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) - self.horizontalLayout_3.addItem(spacerItem1) + spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.horizontalLayout_3.addItem(spacerItem2) self.checkBox_search_for_images = QtWidgets.QCheckBox(parent=self.horizontalLayoutWidget_3) icon = QtGui.QIcon.fromTheme("camera-photo") self.checkBox_search_for_images.setIcon(icon) @@ -360,7 +324,7 @@ class Ui_MainWindow(object): self.horizontalLayout_3.addWidget(self.pushButton_3_scan_dir) MainWindow.setCentralWidget(self.centralwidget) self.menubar = QtWidgets.QMenuBar(parent=MainWindow) - self.menubar.setGeometry(QtCore.QRect(0, 0, 1463, 24)) + self.menubar.setGeometry(QtCore.QRect(0, 0, 1463, 21)) self.menubar.setObjectName("menubar") self.menuBit_Mover = QtWidgets.QMenu(parent=self.menubar) self.menuBit_Mover.setObjectName("menuBit_Mover") @@ -377,7 +341,7 @@ class Ui_MainWindow(object): def retranslateUi(self, MainWindow): _translate = QtCore.QCoreApplication.translate - MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) + MainWindow.setWindowTitle(_translate("MainWindow", "BitMover")) self.pushButton_src_browse.setText(_translate("MainWindow", "Browse")) self.pushButton_dst_browse.setText(_translate("MainWindow", "Browse")) self.label_1_src_dir.setText(_translate("MainWindow", "Source Directory")) @@ -393,14 +357,8 @@ class Ui_MainWindow(object): self.l_meta_08.setText(_translate("MainWindow", "Focal Length")) self.l_exif_ffprobe_title.setText(_translate("MainWindow", "Exif / ffprobe Data")) self.labelEvent.setText(_translate("MainWindow", "Event")) - self.label_3.setText(_translate("MainWindow", "Files Found")) self.label_4.setText(_translate("MainWindow", "Files Imported")) - self.l_proecessing_progress.setText(_translate("MainWindow", "Processing Progress")) - self.l_current_file_progress.setText(_translate("MainWindow", "Current File Progress")) - self.l_import_progress.setText(_translate("MainWindow", "Import Progress")) - self.label_2.setText(_translate("MainWindow", "%")) - self.label_5.setText(_translate("MainWindow", "%")) - self.label_6.setText(_translate("MainWindow", "%")) + self.label_3.setText(_translate("MainWindow", "Files Found")) self.l_file_source_path.setText(_translate("MainWindow", "Source Path")) self.l_file_dest_path.setText(_translate("MainWindow", "Destination Path")) self.checkBox_verify_checksum.setToolTip(_translate("MainWindow", "After copying, verify that the hash of the original file equals the hash of the copied file.")) diff --git a/_ComparisonDialog.ui b/_ComparisonDialog.ui new file mode 100644 index 0000000..626a083 --- /dev/null +++ b/_ComparisonDialog.ui @@ -0,0 +1,153 @@ + + + FileComparisonDialog + + + + 0 + 0 + 1588 + 753 + + + + Dialog + + + + + 20 + 90 + 1551 + 651 + + + + QAbstractItemView::NoEditTriggers + + + false + + + false + + + 2 + + + 5 + + + 300 + + + true + + + + + + + + + + + + + 240 + 11 + 981 + 71 + + + + + + + TextLabel + + + + + + + TextLabel + + + + + + + TextLabel + + + + + + + TextLabel + + + + + + + + 80 + 16777215 + + + + + 0 + 0 + + + + Source + + + + + + + + 80 + 16777215 + + + + + 0 + 0 + + + + Destination + + + + + + + + + 20 + 10 + 231 + 61 + + + + + 28 + + + + Comparing Files + + + + + + diff --git a/_audio.py b/_audio.py new file mode 100644 index 0000000..be65c1e --- /dev/null +++ b/_audio.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +import os.path +import ffmpeg +import time +from datetime import datetime + +from _media_file import MediaFile + +class AudioFile(MediaFile): + def __init__(self,*args,**kwargs): + super(AudioFile, self).__init__(*args, **kwargs) + + self.args = args + self.kwargs = kwargs + self.file = kwargs['file'] + self.probe = ffmpeg.probe(self.path_file_name) + self.audio_capture_date = self.get_audio_capture_date() + + if 'video' == self.probe['streams'][0]['codec_type'].lower(): + self.video_stream = self.probe['streams'][0] + elif 'video' == self.probe['streams'][1]['codec_type'].lower(): + self.video_stream = self.probe['streams'][1] + elif 'video' == self.probe['streams'][2]['codec_type'].lower(): + self.video_stream = self.probe['streams'][2] + + if 'audio' == self.probe['streams'][0]['codec_type'].lower(): + self.audio_stream = self.probe['streams'][0] + elif 'audio' == self.probe['streams'][1]['codec_type'].lower(): + self.audio_stream = self.probe['streams'][1] + elif 'audio' == self.probe['streams'][2]['codec_type'].lower(): + self.audio_stream = self.probe['streams'][2] + + self.format_stream = self.probe['format'] + self.stream = {} + + @staticmethod + def convert_from_seconds(seconds): + return time.strftime("%H:%M:%S", time.gmtime(seconds)) + + def get_audio_capture_date(self): + #TODO: refactor this try/except logic. + try: + stamp = datetime.strptime( + self.format_stream['tags']['date'], + '%Y-%m-%d' + ) + except KeyError: + try: + stamp = datetime.fromtimestamp(os.path.getctime(self.path_file_name)) + except: + stamp = datetime.strptime( + str('1900:01:01 00:00:00'), + '%Y:%m:%d %H:%M:%S' + ) + return stamp + + def get_video_meta(self): + self.stream = { + 'video': { + 'bits_per_raw_sample': self.video_stream['bits_per_raw_sample'], + 'codec_long_name': self.video_stream['codec_long_name'], + 'duration': self.convert_from_seconds(float(self.video_stream['duration'])), + 'encoding_brand': self.format_stream['tags']['major_brand'], + 'pix_fmt': self.video_stream['pix_fmt'], + 'profile': self.video_stream['profile'], + 'r_frame_rate': self.video_stream['r_frame_rate'], + 'size': { + 'width_height': self.size, + 'height': self.video_stream['height'], + 'width': self.video_stream['width'] + } + }, + 'audio': {}, + 'format': {} + } + + return self.stream \ No newline at end of file diff --git a/_checksum_progress_dialog.ui b/_checksum_progress_dialog.ui new file mode 100644 index 0000000..6b04bd5 --- /dev/null +++ b/_checksum_progress_dialog.ui @@ -0,0 +1,91 @@ + + + ChecksumProgressDialog + + + + 0 + 0 + 640 + 86 + + + + Checksum Progress + + + + + 20 + 20 + 601 + 21 + + + + + + + + 100 + 1 + + + + + 100 + 0 + + + + 0 + + + Getting Checksum For + + + 3 + + + + + + + + 1 + 0 + + + + + 150 + 0 + + + + + + + + + + + + + 20 + 50 + 611 + 19 + + + + 0 + + + true + + + + + + diff --git a/_checksum_progress_dialog_Window.py b/_checksum_progress_dialog_Window.py new file mode 100644 index 0000000..1f09684 --- /dev/null +++ b/_checksum_progress_dialog_Window.py @@ -0,0 +1,50 @@ +# Form implementation generated from reading ui file '_checksum_progress_dialog.ui' +# +# Created by: PyQt6 UI code generator 6.4.2 +# +# WARNING: Any manual changes made to this file will be lost when pyuic6 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt6 import QtCore, QtGui, QtWidgets + + +class Ui_ChecksumProgressDialog(object): + def setupUi(self, ChecksumProgressDialog): + ChecksumProgressDialog.setObjectName("ChecksumProgressDialog") + ChecksumProgressDialog.resize(640, 86) + self.gridLayoutWidget = QtWidgets.QWidget(parent=ChecksumProgressDialog) + self.gridLayoutWidget.setGeometry(QtCore.QRect(20, 20, 601, 21)) + self.gridLayoutWidget.setObjectName("gridLayoutWidget") + self.gridLayout = QtWidgets.QGridLayout(self.gridLayoutWidget) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setObjectName("gridLayout") + self.l_title_getting_checksum = QtWidgets.QLabel(parent=self.gridLayoutWidget) + self.l_title_getting_checksum.setMinimumSize(QtCore.QSize(100, 1)) + self.l_title_getting_checksum.setBaseSize(QtCore.QSize(100, 0)) + self.l_title_getting_checksum.setLineWidth(0) + self.l_title_getting_checksum.setObjectName("l_title_getting_checksum") + self.gridLayout.addWidget(self.l_title_getting_checksum, 0, 0, 1, 1) + self.l_content_checksum_filename = QtWidgets.QLabel(parent=self.gridLayoutWidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) + sizePolicy.setHorizontalStretch(1) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.l_content_checksum_filename.sizePolicy().hasHeightForWidth()) + self.l_content_checksum_filename.setSizePolicy(sizePolicy) + self.l_content_checksum_filename.setMinimumSize(QtCore.QSize(150, 0)) + self.l_content_checksum_filename.setText("") + self.l_content_checksum_filename.setObjectName("l_content_checksum_filename") + self.gridLayout.addWidget(self.l_content_checksum_filename, 0, 1, 1, 1) + self.progressBar_getting_checksum = QtWidgets.QProgressBar(parent=ChecksumProgressDialog) + self.progressBar_getting_checksum.setGeometry(QtCore.QRect(20, 50, 611, 19)) + self.progressBar_getting_checksum.setProperty("value", 0) + self.progressBar_getting_checksum.setTextVisible(True) + self.progressBar_getting_checksum.setObjectName("progressBar_getting_checksum") + + self.retranslateUi(ChecksumProgressDialog) + QtCore.QMetaObject.connectSlotsByName(ChecksumProgressDialog) + + def retranslateUi(self, ChecksumProgressDialog): + _translate = QtCore.QCoreApplication.translate + ChecksumProgressDialog.setWindowTitle(_translate("ChecksumProgressDialog", "Checksum Progress")) + self.l_title_getting_checksum.setText(_translate("ChecksumProgressDialog", "Getting Checksum For")) diff --git a/_comparison_dialog_Window.py b/_comparison_dialog_Window.py new file mode 100644 index 0000000..bc859b6 --- /dev/null +++ b/_comparison_dialog_Window.py @@ -0,0 +1,73 @@ +# Form implementation generated from reading ui file '_ComparisonDialog.ui' +# +# Created by: PyQt6 UI code generator 6.4.2 +# +# WARNING: Any manual changes made to this file will be lost when pyuic6 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt6 import QtCore, QtGui, QtWidgets + + +class Ui_FileComparisonDialog(object): + def setupUi(self, FileComparisonDialog): + FileComparisonDialog.setObjectName("FileComparisonDialog") + FileComparisonDialog.resize(1588, 753) + self.tableWidget = QtWidgets.QTableWidget(parent=FileComparisonDialog) + self.tableWidget.setGeometry(QtCore.QRect(20, 90, 1551, 651)) + self.tableWidget.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers) + self.tableWidget.setTabKeyNavigation(False) + self.tableWidget.setProperty("showDropIndicator", False) + self.tableWidget.setRowCount(2) + self.tableWidget.setColumnCount(5) + self.tableWidget.setObjectName("tableWidget") + self.tableWidget.horizontalHeader().setDefaultSectionSize(300) + self.tableWidget.verticalHeader().setVisible(True) + self.gridLayoutWidget = QtWidgets.QWidget(parent=FileComparisonDialog) + self.gridLayoutWidget.setGeometry(QtCore.QRect(240, 11, 981, 71)) + self.gridLayoutWidget.setObjectName("gridLayoutWidget") + self.gridLayout = QtWidgets.QGridLayout(self.gridLayoutWidget) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setObjectName("gridLayout") + self.label_4 = QtWidgets.QLabel(parent=self.gridLayoutWidget) + self.label_4.setObjectName("label_4") + self.gridLayout.addWidget(self.label_4, 2, 2, 1, 1) + self.label = QtWidgets.QLabel(parent=self.gridLayoutWidget) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 0, 1, 1, 1) + self.label_3 = QtWidgets.QLabel(parent=self.gridLayoutWidget) + self.label_3.setObjectName("label_3") + self.gridLayout.addWidget(self.label_3, 2, 1, 1, 1) + self.label_2 = QtWidgets.QLabel(parent=self.gridLayoutWidget) + self.label_2.setObjectName("label_2") + self.gridLayout.addWidget(self.label_2, 0, 2, 1, 1) + self.label_6 = QtWidgets.QLabel(parent=self.gridLayoutWidget) + self.label_6.setMaximumSize(QtCore.QSize(80, 16777215)) + self.label_6.setBaseSize(QtCore.QSize(0, 0)) + self.label_6.setObjectName("label_6") + self.gridLayout.addWidget(self.label_6, 0, 0, 1, 1) + self.label_7 = QtWidgets.QLabel(parent=self.gridLayoutWidget) + self.label_7.setMaximumSize(QtCore.QSize(80, 16777215)) + self.label_7.setBaseSize(QtCore.QSize(0, 0)) + self.label_7.setObjectName("label_7") + self.gridLayout.addWidget(self.label_7, 2, 0, 1, 1) + self.label_5 = QtWidgets.QLabel(parent=FileComparisonDialog) + self.label_5.setGeometry(QtCore.QRect(20, 10, 231, 61)) + font = QtGui.QFont() + font.setPointSize(28) + self.label_5.setFont(font) + self.label_5.setObjectName("label_5") + + self.retranslateUi(FileComparisonDialog) + QtCore.QMetaObject.connectSlotsByName(FileComparisonDialog) + + def retranslateUi(self, FileComparisonDialog): + _translate = QtCore.QCoreApplication.translate + FileComparisonDialog.setWindowTitle(_translate("FileComparisonDialog", "Dialog")) + self.label_4.setText(_translate("FileComparisonDialog", "TextLabel")) + self.label.setText(_translate("FileComparisonDialog", "TextLabel")) + self.label_3.setText(_translate("FileComparisonDialog", "TextLabel")) + self.label_2.setText(_translate("FileComparisonDialog", "TextLabel")) + self.label_6.setText(_translate("FileComparisonDialog", "Source")) + self.label_7.setText(_translate("FileComparisonDialog", "Destination")) + self.label_5.setText(_translate("FileComparisonDialog", "Comparing Files")) diff --git a/_configure.py b/_configure.py index a9774a6..07e5db2 100644 --- a/_configure.py +++ b/_configure.py @@ -6,12 +6,10 @@ Load the config file from yaml file import sys import os import yaml -from _lumberjack import timber basedir = os.path.dirname(__file__) files = {} CONFIG_FILE = os.path.join(basedir, 'config.yaml') -log = timber(__name__) class Configure: """ Configure Class """ diff --git a/_file_stuff.py b/_file_stuff.py index 87ab790..59224fa 100644 --- a/_file_stuff.py +++ b/_file_stuff.py @@ -9,49 +9,32 @@ 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 @@ -61,30 +44,22 @@ def cmp_files(f1,f2): 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}') - + print(f'path_access_read check: cannot 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}') - + print(f'path_access_write check: cannot 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: @@ -92,22 +67,17 @@ def create_folder(file): 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'])}") - + print(f"Cleanup SD: Removing File -\n{os.path.join(f[file]['folders']['source_path'],f[file]['name'])}") os.remove(os.path.join(f[file]['folders']['source_path'],f[file]['name'])) else: - log.debug(f"Cleanup SD: Not Removing File -\n{os.path.join(f[file]['folders']['source_path'],f[file]['name'])}") + print(f"Cleanup SD: Not Removing File -\n{os.path.join(f[file]['folders']['source_path'],f[file]['name'])}") def validate_config_dir_access(config): """ 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 diff --git a/_find_files.py b/_find_files.py new file mode 100644 index 0000000..335bf93 --- /dev/null +++ b/_find_files.py @@ -0,0 +1,84 @@ +import os + +from PyQt6.QtGui import QPixmap + +from _file_stuff import is_file +from BitMover_ui import MainWindow +from _media_file import MediaFile + +basedir = os.path.dirname(__file__) + +class FindFiles(MainWindow): + def __init__(self): + super(FindFiles,self).__init__() + self.search_types = None + self.event = self.get_event() + self.src_dir = self.config['folders']['source']['base'] + self.dst_dir = self.config['folders']['destination']['base'] + self.file_types = self.config['file_types'] + + self.file_total = 0 + self.file_list = {} + + def get_search_types(self): + self.search_types = [] + if self.checkBox_search_for_images.isChecked(): + self.search_types.append('image') + if self.checkBox_search_for_video.isChecked(): + self.search_types.append('video') + if self.checkBox_search_for_audio.isChecked(): + self.search_types.append('audio') + return self.search_types + + def get_t_files(self): + self.file_list.clear() + self.img_preview.setPixmap(QPixmap(os.path.join(basedir, + 'assets', + 'preview_placeholder.jpg'))) + + for folder, subfolders, filename in os.walk(self.src_dir): + for f_type in self.search_types: + for ext in self.file_types[f_type]: + for file in filename: + if file.lower().endswith(ext): + current_file = os.path.join(folder, file) + if is_file(current_file): + self.file_total += int(1) + else: + print(f"Skipping {current_file} as it does not look like a real file.") + + def t_find_files(self, + progress_callback, + found_file, + total_file_count): + file_count = int(0) + self.search_types = self.get_search_types() + self.get_t_files() + + if len(self.search_types) > 0: + for folder, subfolders, filename in os.walk(self.src_dir): + for f_type in self.search_types: + for ext in self.file_types[f_type]: + 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(current_file) + found_file.emit(current_file) + else: + print(f"Skipping {current_file} as it does not look like a real file.") + total_file_count.emit(file_count) + progress_callback.emit(round((file_count / self.file_total) * 100, 0)) + else: + print("Nothing to search for.") + + return self.file_list + + def process_file(self, p): + """ gather information and add to dictionary """ + + media_file = MediaFile(p) + i = media_file.source_path_hash + + self.file_list[i] = media_file.media_meta() \ No newline at end of file diff --git a/_find_files_dialog.py b/_find_files_dialog.py new file mode 100644 index 0000000..93c7fc6 --- /dev/null +++ b/_find_files_dialog.py @@ -0,0 +1,31 @@ +from PyQt6.QtWidgets import QDialog +from _finding_files_dialog_Window import Ui_FindProgress + +class FindProgress(QDialog, Ui_FindProgress): + def __init__(self,*args,**kwargs): + super(FindProgress,self).__init__(*args,**kwargs) + self.setupUi(self) + # self.set_progress_finding_files(0) + + def is_shown(self): + print(f'is_shown: {self.isVisible()}') + return self.isVisible() + + def open_find_files_dialog(self): + print(f'open_import_dialog: {self.is_shown()}') + if not self.is_shown(): + print('Inside if not self.is_shown') + print('showing window') + self.show() + + def close_find_files_dialog(self): + print('close_import_dialog') + if self.is_shown(): + print('inside self.is_shown()') + print('hiding window') + self.hide() + + def set_progress_finding_files(self,n): + print("%d%% done" % n) + self.progressBar_importing.setValue(float(n)) + self.lcd_import_progress.display(n) \ No newline at end of file diff --git a/_finding_files_dialog.ui b/_finding_files_dialog.ui new file mode 100644 index 0000000..e03d87e --- /dev/null +++ b/_finding_files_dialog.ui @@ -0,0 +1,59 @@ + + + FindProgress + + + + 0 + 0 + 640 + 60 + + + + Finding Files + + + + + 20 + 10 + 601 + 41 + + + + + + + Progress + + + + + + + QFrame::NoFrame + + + + + + + 24 + + + + + + + % + + + + + + + + + diff --git a/_finding_files_dialog_Window.py b/_finding_files_dialog_Window.py new file mode 100644 index 0000000..cbb6562 --- /dev/null +++ b/_finding_files_dialog_Window.py @@ -0,0 +1,44 @@ +# Form implementation generated from reading ui file '_finding_files_dialog.ui' +# +# Created by: PyQt6 UI code generator 6.4.2 +# +# WARNING: Any manual changes made to this file will be lost when pyuic6 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt6 import QtCore, QtGui, QtWidgets + + +class Ui_FindProgress(object): + def setupUi(self, FindProgress): + FindProgress.setObjectName("FindProgress") + FindProgress.resize(640, 60) + self.gridLayoutWidget_5 = QtWidgets.QWidget(parent=FindProgress) + self.gridLayoutWidget_5.setGeometry(QtCore.QRect(20, 10, 601, 41)) + self.gridLayoutWidget_5.setObjectName("gridLayoutWidget_5") + self.gridLayout_3 = QtWidgets.QGridLayout(self.gridLayoutWidget_5) + self.gridLayout_3.setContentsMargins(0, 0, 0, 0) + self.gridLayout_3.setObjectName("gridLayout_3") + self.l_find_progress = QtWidgets.QLabel(parent=self.gridLayoutWidget_5) + self.l_find_progress.setObjectName("l_find_progress") + self.gridLayout_3.addWidget(self.l_find_progress, 0, 0, 1, 1) + self.lcd_import_progress = QtWidgets.QLCDNumber(parent=self.gridLayoutWidget_5) + self.lcd_import_progress.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) + self.lcd_import_progress.setObjectName("lcd_import_progress") + self.gridLayout_3.addWidget(self.lcd_import_progress, 0, 2, 1, 1) + self.progressBar_importing = QtWidgets.QProgressBar(parent=self.gridLayoutWidget_5) + self.progressBar_importing.setProperty("value", 24) + self.progressBar_importing.setObjectName("progressBar_importing") + self.gridLayout_3.addWidget(self.progressBar_importing, 0, 1, 1, 1) + self.label_5 = QtWidgets.QLabel(parent=self.gridLayoutWidget_5) + self.label_5.setObjectName("label_5") + self.gridLayout_3.addWidget(self.label_5, 0, 3, 1, 1) + + self.retranslateUi(FindProgress) + QtCore.QMetaObject.connectSlotsByName(FindProgress) + + def retranslateUi(self, FindProgress): + _translate = QtCore.QCoreApplication.translate + FindProgress.setWindowTitle(_translate("FindProgress", "Finding Files")) + self.l_find_progress.setText(_translate("FindProgress", "Progress")) + self.label_5.setText(_translate("FindProgress", "%")) diff --git a/_get_image_tag.py b/_get_image_tag.py deleted file mode 100644 index 216748b..0000000 --- a/_get_image_tag.py +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env python - -""" -Get EXIF information from image -""" -from uu import Error - -import exifread -from datetime import datetime -from _lumberjack import timber - -log = timber(__name__) - -def get_exif_date(tags,tag,f): - t = '' - log.debug(f'function: get_exif_tag(tags:{tags},tag:{tag},format:{f}') - try: - t = datetime.strptime(str(tags[tag]),f) - except: - pass - # log.debug(f'Error: {e}. Format: {f}') - - return t - -def get_os_ctime(path): - # Don't pull the file ctime anymore, more often than not, it's wrong. - # t = datetime.fromtimestamp(os.path.getctime(path)) - - #raise an error so it will except and move on. - raise ValueError(f"{path}: Using ctime like this is usually not good.") - - -def set_generic_date_time(date = '1900:01:01', - time = '00:00:00', - f = '%Y:%m:%d %H:%M:%S'): - - t = datetime.strptime(str(f'{date} {time}'),f) - return t - - -def get_img_date(p): - with open(p, 'rb') as file: - tags = exifread.process_file(file) - - if 'Composite DateTimeOriginal' in tags: - get_exif_date(tags,'Composite DateTimeOriginal','%Y:%m:%d %H:%M') - -def get_exif_tag(p,t): - with open(p, "rb") as f: - try: - tags = exifread.process_file(f) - # print(f'{p}: {tags}') - except Error as e: - return f'Received Error: {e} when trying to open {p}' - finally: - f.close() - - for tag in tags: - # print(tag) - if t in tag.lower(): - # print(tags[tag]) - return tags[tag] - else: - pass - -def get_image_date(path): - t = '' - exif_dt = '' - - with open(path, "rb") as file: - try: - tags = exifread.process_file(file) - except Error as e: - log.error(e) - finally: - file.close() - - for tag in tags: - if "DateTime" in tag: - t = tag - break - - if '' == t: - exif_dt = set_generic_date_time() - else: - for f in ['%Y:%m:%d %H:%M:%S', - '%Y/%m/%d %H:%M:%S', - '%Y-%m-%d-%H-%M-%S']: - log.debug(f'Trying... {t}, {f}, {path} ') - exif_dt = get_exif_date(tags, t, f) - - if '' != exif_dt: - break - - if '' == exif_dt: - exif_dt = set_generic_date_time() - # s = get_os_ctime(path) # This could produce wildly incorrect results - return exif_dt \ No newline at end of file diff --git a/_hashing.py b/_hashing.py index 1b5550e..63de051 100644 --- a/_hashing.py +++ b/_hashing.py @@ -7,12 +7,6 @@ dump the dictionary generated from findings into a yaml file for later inspectio 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 """ @@ -39,19 +33,13 @@ def hash_path(path): 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 """ @@ -67,8 +55,8 @@ def validate_xx_checksums(f): f[file]['source_cleanable'] = True else: f[file]['source_cleanable'] = False - log.critical(f'FATAL: Checksum validation failed for: \ + print(f'FATAL: Checksum validation failed for: \ {f[file]["name"]} \n{c[i]}\n is not equal to \n{c[p]}\n') - log.debug('\n File Meta:\n') - log.debug(f[file]) + print('\n File Meta:\n') + print(f[file]) i = i + 1 \ No newline at end of file diff --git a/_image_tag.py b/_image_tag.py new file mode 100644 index 0000000..b792b21 --- /dev/null +++ b/_image_tag.py @@ -0,0 +1,61 @@ +import exifread +from datetime import datetime +from _media_file import MediaFile + +class ImageTag(MediaFile): + def __init__(self,path_file_name=None,*args,**kwargs): + super(ImageTag,self).__init__(*args,**kwargs) + if path_file_name is not None: + self.path_file_name = path_file_name + self.date_time_original = self.get_img_date() + self.image_tags = self.get_image_tags() + + def get_image_tag(self,t): + tag_data = None + for tag in self.image_tags: + if t.lower() in tag.lower(): + tag_data = self.image_tags[tag] + break + return tag_data + + @staticmethod + 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_image_tags(self): + with open(self.path_file_name,'rb') as f: + tags = exifread.process_file(f) + f.close() + return tags + + def process_img_date_tag(self, tag, date_time_format): + t = None + try: + t = datetime.strptime(str(self.image_tags[tag]), date_time_format) + except: + pass + return t + + def get_img_date(self): + t = None + dt_tag = None + for tag in self.image_tags: + if 'DateTime' in tag: + t = tag + break + if t is None: + dt_tag = self.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']: + dt_tag = self.process_img_date_tag(t,f) + + if dt_tag is not None: + break + if dt_tag is None: + dt_tag = self.set_generic_date_time() + return dt_tag \ No newline at end of file diff --git a/_img_preview.py b/_img_preview.py deleted file mode 100644 index 18971cd..0000000 --- a/_img_preview.py +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env python - -from _media import Media -from _get_image_tag import get_exif_tag -from PIL import Image -from _raw_photo import get_raw_image_dimensions, extract_jpg_thumb -from _video import Video - -class ImgPreview: - def __init__(self,*args,**kwargs): - super(ImgPreview,self).__init__() - self.args = args - self.kwargs = kwargs - self.file = kwargs['file'] - self.event = kwargs['event'] - self.config = kwargs['config'] - self.m = Media(self.file, - self.event, - self.config) - self.file_type = self.m.file_type - self.is_jpg = False - self.is_raw = False - self.path_hash = self.m.source_path_hash - self.dtc = f'{self.m.capture_date[0]}/{self.m.capture_date[1]}/{self.m.capture_date[2]}' - self.size = None - self.mpixels = None - self.thumbnail = 'thumbnail.jpg' - self.ratio = None - self.thumbnail_width = None - self.thumbnail_height = None - self.thumbnail_ratio = None - self.video_framerate = None - self.video_bit_depth = None - self.video_duration = None - self.video_encoding = None - self.video_codec = None - self.video_profile = None - self.video_pix_format = None - - if self.file_type == 'image': - self._img_preview() - print(f'size: {self.size}') - print(f'dpi: {self.dpi}') - print(f'iso: {self.iso}') - print(f'lens: {self.lens}') - print(f'zoom: {self.zoom}') - print(f'camera: {self.camera}') - print(f'aperture: {self.aperture}') - print(f'mpixels: {self.mpixels}') - elif self.file_type == 'video': - self._video_preview() - - self.thumb_ratio() - self.size = f'{self.width}x{self.height}' - if self.width is not None \ - and self.height is not None: - self.mpixels = round((self.width * self.height) / 1000000, 1) - else: - self.mpixels = 'Unknown :(' - - def _img_preview(self): - self.dpi = get_exif_tag(self.file, "xresolution") - self.iso = get_exif_tag(self.file, 'iso') - self.aperture = get_exif_tag(self.file, 'fnumber') - self.camera = get_exif_tag(self.file,'cameramodelname') - if self.camera is None: - self.camera = get_exif_tag(self.file,'image model') - self.lens = get_exif_tag(self.file,'lensmodel') - self.zoom = get_exif_tag(self.file,'focallength') - - if self.file.lower().endswith("jpg") \ - or self.file.lower().endswith("jpeg"): - self._jpg_preview() - else: - self._raw_preview() - - def _jpg_preview(self): - self.is_jpg = True - img = Image.open(self.file) - self.width = img.width - self.height = img.height - self.gen_thumb_from_jpg() - - def gen_thumb_from_jpg(self): - """Generates a thumbnail image from the given input image.""" - thumb_width = 500 - thumb_size = (thumb_width, int(thumb_width // 1.5)) - try: - with Image.open(self.file) as img: - img.thumbnail(thumb_size) - img.save(self.thumbnail, "JPEG") - except IOError: - print(f"Error: Cannot create thumbnail for '{self.file}'") - - def _raw_preview(self): - self.is_raw = True - self.width = get_raw_image_dimensions(self.file)[1] - self.height = get_raw_image_dimensions(self.file)[0] - self.thumbnail = extract_jpg_thumb(self.file) - - def _video_preview(self): - vid = Video(file=self.file) - self.thumbnail = vid.gen_video_thumbnail() - video_meta = vid.get_video_meta() - self.width = video_meta['video']['size']['width'] - self.height = video_meta['video']['size']['height'] - self.video_framerate = round( - int(video_meta['video']['r_frame_rate'].split('/')[0]) / - int(video_meta['video']['r_frame_rate'].split('/')[1]),2) - self.video_bit_depth = video_meta['video']['bits_per_raw_sample'] - self.video_duration = video_meta['video']['duration'] - self.video_encoding = video_meta['video']['encoding_brand'] - self.video_codec = video_meta['video']['codec_long_name'] - self.video_profile = video_meta['video']['profile'] - self.video_pix_format = video_meta['video']['pix_fmt'] - - def thumb_ratio(self): - img = Image.open(self.thumbnail) - self.thumbnail_width = img.width - self.thumbnail_height = img.height - self.thumbnail_ratio = float(self.thumbnail_width / self.thumbnail_height) - print(self.thumbnail_width) - print(self.thumbnail_height) - print(self.thumbnail_ratio) \ No newline at end of file diff --git a/_import_files.py b/_import_files.py index 8f2222e..7653b32 100644 --- a/_import_files.py +++ b/_import_files.py @@ -8,52 +8,39 @@ 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): +def copy_with_progress(s,d): """ Copy a file with the progress bar """ - log.debug(f'copy_with_progress({s},{d},{f})') - size = path.getsize(s) + # size = path.getsize(s) with open(s, 'rb') as fs: with open(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)) + while True: + chunk = fs.read(4096) + if not chunk: + break + fd.write(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.') + print(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}, \ + print(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') + print(f'\n{file_name} hashes match') # remove(path.join(source_path,file_name)) # f.pop(file_name) return @@ -61,13 +48,10 @@ def copy_from_source(source_path,dest_path,file_name): # 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') + path.join(dest_path, file_name)) 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']) diff --git a/_lumberjack.py b/_lumberjack.py deleted file mode 100644 index c38d5ba..0000000 --- a/_lumberjack.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python - -""" -A class for logging... no, not timber -""" - - -import logging -import os - -basedir = os.path.dirname(__file__) -# class Logger(object): -# level_relations = { -# 'debug': logging.DEBUG, -# 'info': logging.INFO, -# 'warning': logging.WARNING, -# 'error': logging.ERROR, -# 'crit': logging.CRITICAL -# } # relationship mapping -# -# def __init__(self, filename, level='info', when='D', backCount=3, -# fmt='%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s'): -# self.logger = logging.getLogger(filename) -# format_str = logging.Formatter(fmt) # Setting the log format -# self.logger.setLevel(self.level_relations.get(level)) # Setting the log level -# console_handler = logging.StreamHandler() # on-screen output -# console_handler .setFormatter(format_str) # Setting the format -# th = logging.handlers.TimedRotatingFileHandler(filename=filename, when=when, backupCount=backCount,encoding='utf-8') # automatically generates the file at specified intervals -# th.setFormatter(format_str) # Setting the format -# self.logger.addHandler(console_handler) # Add the object to the logger -# self.logger.addHandler(th) - -def timber(name): - file_formatter = logging.Formatter('%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s') - file_handler = logging.FileHandler(os.path.join(basedir, "log", "all.log")) - file_handler.setLevel(logging.DEBUG) - file_handler.setFormatter(file_formatter) - - console_handler = logging.StreamHandler() - console_handler.setLevel(logging.INFO) - console_handler.setFormatter(file_formatter) - - logger = logging.getLogger(name) - logger.addHandler(file_handler) - logger.addHandler(console_handler) - logger.setLevel(logging.DEBUG) - - return logger diff --git a/_media.py b/_media.py deleted file mode 100644 index 7e1c029..0000000 --- a/_media.py +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env python - -""" -Create the media object -""" -import os -from os import path -from datetime import datetime -import ffmpeg -import sys - -#Local Imports -from _hashing import xx_hash, hash_path -from _get_image_tag import get_image_date -# from configure import files -from _lumberjack import timber - -log = timber(__name__) - -class Media: - """ media class """ - - def __init__(self, source_path,event,config): - """ init """ - - self.configuration = config - self.source_path_dir = os.path.dirname(source_path) - self.source_path_hash = hash_path(source_path) - self.event_name = event - self.file_name = os.path.basename(source_path) - self.dotted_file_ext = os.path.splitext(self.file_name)[1].lower() - self.file_ext = self.dotted_file_ext.split('.')[1] - self.file_type = self.get_file_type() - self.capture_date = get_capture_date(source_path,self.file_type) - self.store_originals = self.configuration['store_originals'] - self.destination_originals_path = '' - self.destination_path = self.set_destination_path() - - - self.filehash = '' # Only populate this one if it's called upon. - - def set_destination_path(self): - """ set the destination path for the file """ - - p = self.configuration['folders']['destination']['base'] + '/' \ - + self.capture_date[0] + '/' \ - + self.capture_date[0] + '-' \ - + self.capture_date[1] + '/' \ - + self.capture_date[0] + '-' \ - + self.capture_date[1] + '-' \ - + self.capture_date[2] - - if self.event_name: - p = p + '-' + self.event_name - - if self.file_type == 'image': - # print(f'store_originals: {self.store_originals}') - p = p + '/PHOTO' - if self.file_ext.lower() in ('jpg', 'jpeg'): - # print(f'store_originals: {self.store_originals}') - if self.store_originals is True: - self.destination_originals_path = path.join( - p, - self.configuration['folders']['originals'], - 'JPG') - # print(self.destination_originals_path) - p = p + '/JPG' - else: - if self.store_originals is True: - self.destination_originals_path = path.join( - p, - self.configuration['folders']['originals'], - 'RAW') - # print(self.destination_originals_path) - p = p + '/RAW' - elif self.file_type == 'video': - p = p + '/VIDEO' - elif self.file_type == 'audio': - p = p + '/AUDIO' - else: - log.critical(f'{self.file_type} is not known. You should have never landed here.') - - return p - - def generate_hash(self): - """ generate the hash and store it to self.filehash """ - return xx_hash(self.source_path_dir) - - def set_hash(self): - """set the hash for the file """ - self.filehash = self.generate_hash() - - def get_hash(self): - """ a function to get return the hash """ - - if not self.filehash: - self.set_hash() - - return self.filehash - - def get_file_type(self): - """ determine if the extension is in the list of file types """ - # log.debug(f'get_file_type():') - for t in self.configuration['file_types']: - # log.debug(f'Checking if file is part of {t}') - - for e in self.configuration['file_types'][t]: - # log.debug(f'Checking if {self.file_ext.lower()} ends with {e}') - - if self.file_ext.lower().endswith(e): - # log.debug(self.file_ext + ' ends with ' + e) - return t - # else: - # log.debug(self.file_ext + f' does not end with {e}') - -def get_capture_date(p, f_type): - """ get capture date from meta """ - # log.debug(f'get_capture_date({p}, {f_type}') - - if f_type == 'image': - stamp = get_image_date(p) - - elif f_type == 'video': - try: - stamp = datetime.strptime( - ffmpeg.probe(path)['format']['tags']['creation_time'], - '%Y-%m-%dT%H:%M:%S.%f%z') - except: - try: - stamp = datetime.fromtimestamp(os.path.getctime(p)) - except: - try: - stamp = datetime.strptime( - str('1900:01:01 00:00:00'), '%Y:%m:%d %H:%M:%S') - except: - # log.critical(f'\nCould not get timestamp for {p}. Giving up.') - sys.exit() - - elif f_type == 'audio': - try: - stamp = datetime.strptime(ffmpeg.probe( - p)['format']['tags']['date'], '%Y-%m-%d') - except KeyError as ke: - # log.warning(f'\nError: {ke} for {p}. Trying getctime...') - try: - stamp = datetime.fromtimestamp(os.path.getctime(p)) - except: - # log.critical(f'\nCould not get timestamp for {p}. Giving up.') - sys.exit() - else: - try: - stamp = datetime.fromtimestamp(os.path.getctime(p)) - except: - # log.critical(f'\nCould not get timestamp for {p}. Giving up.') - sys.exit() - - year = stamp.strftime("%Y") - month = stamp.strftime("%m") - day = stamp.strftime("%d") - return year, month, day \ No newline at end of file diff --git a/_media_file.py b/_media_file.py new file mode 100644 index 0000000..7d0d975 --- /dev/null +++ b/_media_file.py @@ -0,0 +1,152 @@ +import os.path +import sys + +from _audio import AudioFile +from BitMover_ui import MainWindow +from _hashing import hash_path +from _video import VideoFile +from _photo import PhotoFile +from datetime import datetime + +class MediaFile(MainWindow): + def __init__(self,path_file_name,*args,**kwargs): + super(MediaFile,self).__init__(*args,**kwargs) + self.event_name = self.get_event() + self.src_dir = self.config['folders']['source']['base'] + self.base_dst_dir = self.config['folders']['destination']['base'] + self.file_types = self.config['file_types'] + self.path_file_name = path_file_name + self.source_path_hash = hash_path(self.path_file_name) + self.file_name = self.get_file_name() + self.dotted_file_ext = self.get_dotted_file_ext() + self.file_ext = self.get_file_ext() + self.file_type = self.get_file_type() + self.capture_date = self.get_capture_date() + self.capture_date_year = self.capture_date[0] + self.capture_date_month = self.capture_date[1] + self.capture_Date_day = self.capture_date[2] + self.dst_dir = self.set_destination_path() + self.destination_originals_path = '' + self.media = {} + self.video_media = VideoFile() + self.audio_media = AudioFile() + self.photo_media = PhotoFile() + + def get_file_name(self): + return os.path.basename(self.path_file_name) + + def get_dotted_file_ext(self): + return os.path.splitext(self.file_name)[1].lower() + + def get_file_ext(self): + return self.dotted_file_ext.split('.')[1] + + def get_file_type(self,file=None): + """ determine if the extension is in the list of file types """ + if file is not None: + self.file_name = file + + if not self.file_ext: + self.file_ext = self.get_file_ext() + + for t in self.config['file_types']: + for e in self.config['file_types'][t]: + if self.file_ext.lower().endswith(e): + return t + + def set_destination_path(self): + """ + set the destination path for the file + base dir structure is: + base_dst_dir/YYYY/YYYY-MM/YYYY-MM-DD[-event_name] + """ + p1 = os.path.join(self.base_dst_dir,self.capture_date_year) + p2 = f'{self.capture_date_year}-{self.capture_date_month}' + p3 = f'{self.capture_date_year}-{self.capture_date_month}-{self.capture_Date_day}' + + p = f'{p1}/{p2}/{p3}' # <--- Dumb. + if self.event_name: + p4 = '-' + self.event_name + p = os.path.join(p,p4) + + if self.file_type == 'image': + p = os.path.join(p,'PHOTO') + if self.file_ext.lower() in ('jpg', 'jpeg'): + if self.store_originals is True: + self.destination_originals_path = os.path.join( + p, + self.config['folders']['originals'], + 'JPG' + ) + p = os.path.join(p, 'JPG') + else: + if self.store_originals is True: + self.destination_originals_path = os.path.join( + p, + self.config['folders']['originals'], + 'RAW') + p = os.path.join(p,'RAW') + elif self.file_type == 'video': + p = os.path.join(p,'VIDEO') + elif self.file_type == 'audio': + p = os.path.join(p,'AUDIO') + + return p + + def get_capture_date(self): + """ get capture date from meta """ + if self.file_type == 'image': + stamp = self.photo_media.photo_capture_date + elif self.file_type == 'video': + stamp = self.video_media.get_video_capture_date() + elif self.file_type == 'audio': + stamp = self.audio_media.get_audio_capture_date() + else: + try: + stamp = datetime.fromtimestamp( + os.path.getctime( + self.path_file_name + ) + ) + except: + print('end of the road for finding the date.') + sys.exit() + + year = stamp.strftime("%Y") + month = stamp.strftime("%m") + day = stamp.strftime("%d") + return year, month, day + + def media_meta(self): + self.media = { + 'date': { + 'capture_date': { + 'y': self.capture_date_year, + 'm': self.capture_date_month, + 'd': self.capture_Date_day + } + }, + 'event': { + 'name': self.event_name + }, + 'extension': self.file_ext, + 'folders': { + 'destination': self.dst_dir, + 'destination_original': self.destination_originals_path, + 'source_path': self.src_dir + }, + 'name': self.file_name, + 'source_path_hash': self.source_path_hash, + 'type': self.file_type + } + + if self.file_type == 'video': + self.media['video_meta'] = self.video_media.get_video_meta() + self.media['image_meta'] = None + self.media['audio_meta'] = None + + if self.file_type == 'image': + photo = PhotoFile() + self.media['image_meta'] = photo.get_photo_meta() + self.media['video_meta'] = None + self.media['audio_meta'] = None \ No newline at end of file diff --git a/_media_import.py b/_media_import.py new file mode 100644 index 0000000..12330a0 --- /dev/null +++ b/_media_import.py @@ -0,0 +1,116 @@ +from os import path,rename + +from BitMover_ui import MainWindow +from _file_stuff import create_folder, path_exists, cmp_files +from _hashing import xx_hash + +class MediaImporter(MainWindow): + def __init__(self,*args,**kwargs): + super(MediaImporter,self).__init__(*args,**kwargs) + self.chunk_size = 16 * 1024 + self.path_file_source = None + self.path_file_destination = None + self.destination_original_path = None + self.path_file_destination_original = None + self.path_file_destination = None + + def is_video(self,f): + if self.files[f]['type'] == 'video': + r = True + else: + r = False + + return r + + def is_image(self,f): + if self.files[f]['type'] == 'image': + r = True + else: + r = False + + return r + + def t_copy_files(self, + import_progress_callback, + current_file_progress_callback, + imported_file_count_callback + ): + """ Copy Files. """ + + count = int(0) + + for file in self.files: + self.src_dir = self.files[file]['folders']['source_path'] + self.dst_dir = self.files[file]['folders']['destination'] + self.path_file_source = path.join(self.src_dir, + self.files[file]['name']) + self.path_file_destination = path.join(self.dst_dir, + self.files[file]['name']) + self.destination_original_path = path.join(self.dst_dir, + self.files[file]['folders']['destination_original']) + self.path_file_destination_original = path.join(self.dst_dir, + self.files[file]['folders']['destination_original'], + self.files[file]['name']) + + self.imp_dialog.set_importing_file(self.path_file_source) + + self.copy_a_file( + file, + current_file_progress_callback) + + if self.check_store_original(file) is True: + self.copy_a_file(file, + current_file_progress_callback) + + count += 1 + + imported_file_count_callback.emit(count) + import_progress_callback.emit(round((count / self.file_total) * 100, 1)) + self.imp_dialog.add_to_imported_list(self.path_file_source) + + def copy_a_file(self, + _f, + current_file_progress_callback): + + size = path.getsize(self.path_file_source) + create_folder(self.dst_dir) + + self.check_duplicate(_f) + + if self.is_video(_f): + self.chunk_size = (1024 * 1024) * 5 + else: + self.chunk_size = (16 * 1024) * 1 + + with open(self.path_file_source, 'rb') as fs: + with open(self.path_file_destination, 'wb') as fd: + while True: + chunk = fs.read(self.chunk_size) + if not chunk: + break + fd.write(chunk) + dst_size = path.getsize(self.path_file_destination) + current_file_progress_callback.emit(round((dst_size / size) * 100, 1)) + + def check_store_original(self,_f): + if self.config['store_originals'] is True: + if self.is_image(_f): + self.dst_dir = self.destination_original_path + self.path_file_destination = self.path_file_destination_original + r = True + else: + r = False + else: + r = False + + return r + + def check_duplicate(self,_f): + if path_exists(self.path_file_destination): + check_match = cmp_files(self.path_file_source, self.path_file_destination) + if check_match is False: + print(f'\nFound duplicate for {self.path_file_source}, renaming destination with hash appended.') + base, extension = path.splitext(self.files[_f]['name']) + f_xxhash = xx_hash(self.path_file_destination) + file_name_hash = base + '_' + f_xxhash + extension + rename(self.path_file_destination, path.join(self.dst_dir, file_name_hash)) \ No newline at end of file diff --git a/_photo.py b/_photo.py new file mode 100644 index 0000000..11343ea --- /dev/null +++ b/_photo.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python + +from PIL import Image + +from _media_file import MediaFile +from _image_tag import ImageTag + +# https://exiftool.org/TagNames/EXIF.html +# https://exiftool.org/TagNames/Sony.html + +class PhotoFile(MediaFile): + def __init__(self,*args,**kwargs): + super(PhotoFile, self).__init__(*args, **kwargs) + self.args = args + self.kwargs = kwargs + self.image_tag = ImageTag() + self.is_jpg = False + self.is_raw = False + self.size_width = self.get_photo_width() + self.size_height = self.get_photo_height() + self.size = self.get_photo_size() + self.mpixels = self.get_photo_megapixels() + self.size_ratio = self.get_photo_size_ratio() + self.dpi = self.get_dpi() + self.iso = self.get_iso() + self.aperture = self.get_aperture() + self.camera_brand = self.get_camera_brand() + self.camera_model = self.get_camera_model() + self.lens_make = self.get_lens_make() + self.lens_model = self.get_lens_model() + self.focal_length = self.get_focal_length() + self.img = Image.open(self.path_file_name) + self.photo_meta = {} + self.photo_capture_date = self.get_photo_capture_date() + + def get_photo_capture_date(self): + return self.image_tag.date_time_original + + def is_photo_raw(self): + # TODO: Actually inspect the mime type instead of relying on an extension. + if self.file_name.lower().endswith('jpg') \ + or self.file_name.lower().endswith('.jpeg'): + self.is_raw = False + self.is_jpg = True + else: + self.is_raw = True + self.is_jpg = False + + return self.is_raw + + def is_photo_jpeg(self): + # TODO: Stop making assumption. #Life-Lessons + self.is_photo_raw() + return self.is_jpg + + def get_photo_width(self): + return self.img.width + + def get_photo_height(self): + return self.img.height + + def get_photo_size(self): + if self.size_width is None: + self.size_width = self.get_photo_width() + if self.size_height is None: + self.size_height = self.get_photo_height() + return f'{self.size_width}x{self.size_height}' + + def get_photo_size_ratio(self): + if self.size_width is None: + self.size_width = self.get_photo_width() + if self.size_height is None: + self.size_height = self.get_photo_height() + return float(self.size_width / self.size_height) + + def get_photo_megapixels(self): + if self.size_width is None: + self.size_width = self.get_photo_width() + if self.size_height is None: + self.size_height = self.get_photo_height() + return self.size_width * self.size_height + + def get_dpi(self): + return self.image_tag.get_image_tag("xresolution") + + def get_iso(self): + return self.image_tag.get_image_tag( 'iso') + + def get_aperture(self): + return self.image_tag.get_image_tag( 'fnumber') + + def get_camera_brand(self): + tag = self.image_tag.get_image_tag('Make') + return tag + + def get_camera_model(self): + tag = self.image_tag.get_image_tag('cameramodelname') + if tag is None: + tag = self.image_tag.get_image_tag('image model') + return tag + + def get_camera_firmware(self): + tag = self.image_tag.get_image_tag('CameraFirmware') + return tag + + def get_camera_serial_number(self): + tag = self.image_tag.get_image_tag('CameraSerialNumber') + return tag + + def get_lens_make(self): + return self.image_tag.get_image_tag('lensmake') + + def get_lens_model(self): + return self.image_tag.get_image_tag('lensmodel') + + def get_lens_serial(self): + pass + + def get_focal_length(self): + return self.image_tag.get_image_tag('focallength') + + def get_photographer(self): + pass + + + + def get_orientation(self): + pass + + def get_sony_raw_type(self): + pass + + def get_exposure_program(self): + pass + + def get_photo_meta(self): + self.photo_meta = { + 'photo': { + 'aperture': self.aperture, + 'camera_brand': self.camera_brand, + 'camera_model': self.camera_model, + 'dpi': self.dpi, + 'is_jpg': self.is_jpg, + 'is_raw': self.is_raw, + 'iso': self.iso, + 'lens_make': self.lens_make, + 'lens_model': self.lens_model, + 'lens_focal_length': self.focal_length, + 'size': { + 'height': self.size_height, + 'width': self.size_width, + 'width_height': self.size, + 'megapixels': self.mpixels, + 'ratio': self.size_ratio + } + } + } + + return self.photo_meta \ No newline at end of file diff --git a/_preview.py b/_preview.py new file mode 100644 index 0000000..40240dc --- /dev/null +++ b/_preview.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python +from PIL import Image + +from BitMover_ui import MainWindow +from _media_file import MediaFile +from _raw_photo import extract_jpg_thumb +from _video import VideoFile + +class MediaPreview(MainWindow): + def __init__(self,path_file_name,media_files): + super(MediaPreview, self).__init__() + self.path_file_name = path_file_name + self.media_files_list = media_files + self.media_file = MediaFile(self.path_file_name) + self.source_path_hash = self.media_file.source_path_hash + self.thumbnail_ratio = self.media_files_list[self.source_path_hash]['photo']['size']['ratio'] + self.file_type = self.media_files_list[self.source_path_hash]['file_type'] + + if self.media_files_list[self.source_path_hash]['file_type'] == 'image': + self._img_preview() + elif self.media_files_list[self.source_path_hash]['file_type'] == 'video': + self._video_preview() + + self.mpixels = self.media_files_list[self.source_path_hash]['photo']['megapixels'] + + def _img_preview(self): + if self.media_files_list[self.source_path_hash]['photo']['is_jpg'] is True: + self._jpg_preview() + else: + self._raw_preview() + + def _jpg_preview(self): + self.gen_thumb_from_jpg() + + def gen_thumb_from_jpg(self): + """Generates a thumbnail image from the given input image.""" + thumb_width = 500 + thumb_size = ( + thumb_width, + int(thumb_width // self.media_files_list[self.source_path_hash]['photo']['size']['ratio']) + ) + try: + with Image.open(self.path_file_name) as img: + img.thumbnail(thumb_size) + img.save(self.thumbnail, "JPEG") + except IOError: + print(f"Error: Cannot create thumbnail for '{self.path_file_name}'") + + def _raw_preview(self): + self.thumbnail = extract_jpg_thumb(self.path_file_name) + + def _video_preview(self): + vid = VideoFile(file=self.path_file_name) + self.thumbnail = vid.gen_video_thumbnail() + + self.width = self.media_files_list[self.source_path_hash]['video']['size']['width'] + self.height = self.media_files_list[self.source_path_hash]['video']['size']['height'] + self.video_framerate = round( + int(self.media_files_list[self.source_path_hash]['video']['r_frame_rate'].split('/')[0]) / + int(self.media_files_list[self.source_path_hash]['video']['r_frame_rate'].split('/')[1]),2) + self.video_bit_depth = self.media_files_list[self.source_path_hash]['video']['bits_per_raw_sample'] + self.video_duration = self.media_files_list[self.source_path_hash]['video']['duration'] + self.video_encoding = self.media_files_list[self.source_path_hash]['video']['encoding_brand'] + self.video_codec = self.media_files_list[self.source_path_hash]['video']['codec_long_name'] + self.video_profile = self.media_files_list[self.source_path_hash]['video']['profile'] + self.video_pix_format = self.media_files_list[self.source_path_hash]['video']['pix_fmt'] \ No newline at end of file diff --git a/_thread_my_stuff.py b/_thread_my_stuff.py index ff9499c..031d869 100644 --- a/_thread_my_stuff.py +++ b/_thread_my_stuff.py @@ -29,6 +29,9 @@ class WorkerSignals(QObject): progress = pyqtSignal(int) import_progress = pyqtSignal(int) current_file_progress = pyqtSignal(float) + imported_file_count = pyqtSignal(int) + found_file = pyqtSignal(str) + total_file_count = pyqtSignal(int) # current_import_file = pyqtSignal() class Worker(QRunnable): @@ -57,6 +60,9 @@ class Worker(QRunnable): self.kwargs['progress_callback'] = self.signals.progress self.kwargs['import_progress_callback'] = self.signals.import_progress self.kwargs['current_file_progress_callback'] = self.signals.current_file_progress + self.kwargs['imported_file_count_callback'] = self.signals.imported_file_count + self.kwargs['found_file'] = self.signals.found_file + self.kwargs['total_file_count'] = self.signals.total_file_count # self.kwargs['current_import_file'] = self.signals.current_import_file @pyqtSlot() diff --git a/_verify_file_checksum.py b/_verify_file_checksum.py new file mode 100644 index 0000000..31394d7 --- /dev/null +++ b/_verify_file_checksum.py @@ -0,0 +1,48 @@ +import os +import xxhash + +class FileHash: + def __init__(self,files,*args,**kwargs): + super(FileHash,self).__init__(*args,**kwargs) + self.files = files + self.chunk_size = kwargs['chunk_size'] + + @staticmethod + def xx_hash(self,f,progress_callback): + size = os.path.getsize(f) + hasher = xxhash.xxh64() + + #todo: add callbacks + chunk_count = 0 + with open(f, 'rb') as f: + for chunk in iter(lambda: f.read(self.chunk_size),b""): + hasher.update(chunk) + chunk_count += 1 + hashed_size = chunk_count * self.chunk_size + progress_callback.emit(round((hashed_size / size) * 100, 1)) + + file_hash = hasher.hexdigest() + return file_hash + + @staticmethod + def t_verify_checksum(self, + progress_callback, + import_progress_callback, + current_file_progress_callback, + imported_file_count_callback, + found_file, + total_file_count): + + for file in self.files: + i = 0 + c = {} + for checksum in self.files[file]['xx_checksum']: + c[i] = self.files[file]['xx_checksum'][checksum] + if i > 0: + p = i - 1 + if c[i] == c[p]: + self.files[file]['checksum_match'] = True + else: + self.files[file]['checksum_match'] = False + i += 1 + return self.files \ No newline at end of file diff --git a/_video.py b/_video.py index f81e0b1..f5a808a 100644 --- a/_video.py +++ b/_video.py @@ -1,65 +1,47 @@ #!/usr/bin/env python - +import os.path import sys import ffmpeg import time +from datetime import datetime -class Video: +from _media_file import MediaFile + +class VideoFile(MediaFile): def __init__(self,max_width=1024,*args,**kwargs): - super(Video,self).__init__() - self.args = args - self.kwargs = kwargs - self.file = kwargs['file'] - self.out = 'thumbnail.jpg' - self.max_width = max_width + super(VideoFile, self).__init__(*args, **kwargs) - self.stream = { - 'video': {}, - 'format': {}, - 'audio': {} - } + self.args = args + self.kwargs = kwargs + self.file = kwargs['file'] + self.out = 'thumbnail.jpg' + self.max_width = max_width + self.probe = ffmpeg.probe(self.path_file_name) + self.video_capture_date = self.get_video_capture_date() - self.stream['video']['duration'] = '' - self.stream['video']['creation_time'] = '' - self.stream['video']['encoding_brand'] = '' - self.stream['video']['codec_name'] = '' - self.stream['video']['codec_long_name'] = '' - self.stream['video']['profile'] = '' - self.stream['video']['size'] = {} - self.stream['video']['size']['width'] = int() - self.stream['video']['size']['height'] = int() - self.stream['video']['display_aspect_ratio'] = str() - self.stream['video']['level'] = '' - self.stream['video']['color_range'] = '' - self.stream['video']['field_order'] = '' - self.stream['video']['is_avc'] = '' - self.stream['video']['r_frame_rate'] = '' # -> expect for 120fps: 120000/1001 - self.stream['video']['avg_frame_rate'] = '' # -> expect for 120fps: 120000/1001 - self.stream['video']['time_base'] = '' # -> expect for 120fps: 1/120000 - self.stream['video']['bit_rate'] = '' - self.stream['video']['bits_per_raw_sample'] = '' - self.stream['video']['nb_frames'] = '' - self.stream['video']['language'] = '' - self.stream['video']['handler_name'] = '' - self.stream['video']['encoder'] = '' - self.stream['video']['pix_fmt'] = '' + if 'video' == self.probe['streams'][0]['codec_type'].lower(): + self.video_stream = self.probe['streams'][0] + elif 'video' == self.probe['streams'][1]['codec_type'].lower(): + self.video_stream = self.probe['streams'][1] + elif 'video' == self.probe['streams'][2]['codec_type'].lower(): + self.video_stream = self.probe['streams'][2] - self.stream['format'] = {} - self.stream['format']['major_brand'] = '' - self.stream['format']['compatible_brands'] = '' - self.stream['format']['creation_time'] = '' + if 'audio' == self.probe['streams'][0]['codec_type'].lower(): + self.audio_stream = self.probe['streams'][0] + elif 'audio' == self.probe['streams'][1]['codec_type'].lower(): + self.audio_stream = self.probe['streams'][1] + elif 'audio' == self.probe['streams'][2]['codec_type'].lower(): + self.audio_stream = self.probe['streams'][2] - self.stream['audio'] = {} - self.stream['audio']['codec_long_name'] = '' - self.stream['audio']['channels'] = '' - self.stream['audio']['sample_fmt'] = '' - self.stream['audio']['sample_rate'] = '' - self.stream['audio']['bits_per_sample'] = '' - self.stream['audio']['bit_rate'] = '' + self.format_stream = self.probe['format'] + self.size_width = self.get_video_width() + self.size_height = self.get_video_height() + self.size = self.get_video_size() + + self.stream = {} @staticmethod - def convert_from_seconds(s): - seconds = s + def convert_from_seconds(seconds): return time.strftime("%H:%M:%S", time.gmtime(seconds)) def gen_video_thumbnail(self): @@ -67,61 +49,97 @@ class Video: Generate a thumbnail from a video """ self.get_video_meta() - time = self.stream['video']['seconds'] // 5 - v_width = int(self.stream['video']['size']['width']) - width = self.set_thumb_width(v_width) + time_seconds = self.stream['video']['seconds'] // 5 + v_width = int(self.stream['video']['size']['width']) + width = self.set_thumb_width(v_width) try: ( - ffmpeg.input(self.file, ss=time) - .filter('scale', width, -1) - .output(self.out, vframes=1) + ffmpeg.input( + self.file, + ss = time_seconds + ) + .filter( + 'scale', + width, + -1 + ) + .output( + self.out, + vframes = 1 + ) .overwrite_output() - .run(capture_stdout=True, capture_stderr=True) + .run( + capture_stdout = True, + capture_stderr = True + ) ) except ffmpeg.Error as e: - print(e.stderr.decode(), file=sys.stderr) + print(e.stderr.decode(), + file = sys.stderr) return self.out def set_thumb_width(self, v_width): if v_width > self.max_width: - width = self.max_width + width = self.max_width else: - width = v_width + width = v_width return width + def get_video_width(self): + return self.video_stream['width'] + + def get_video_height(self): + return self.video_stream['height'] + + def get_video_size(self): + if self.size_width is None: + self.size_width = self.get_video_width() + if self.size_height is None: + self.size_height = self.get_video_height() + return f'{self.size_width}x{self.size_height}' + + def get_video_capture_date(self): + #TODO: refactor this try/except logic. + try: + stamp = datetime.strptime( + self.format_stream['tags']['creation_time'], + '%Y-%m-%dT%H:%M:%S.%f%z' + ) + except: + try: + stamp = datetime.fromtimestamp( + os.path.getctime( + self.path_file_name + ) + ) + except: + stamp = datetime.strptime( + str('1900:01:01 00:00:00'), + '%Y:%m:%d %H:%M:%S' + ) + return stamp + def get_video_meta(self): - probe = ffmpeg.probe(self.file) + self.stream = { + 'video': { + 'bits_per_raw_sample': self.video_stream['bits_per_raw_sample'], + 'codec_long_name': self.video_stream['codec_long_name'], + 'duration': self.convert_from_seconds(float(self.video_stream['duration'])), + 'encoding_brand': self.format_stream['tags']['major_brand'], + 'pix_fmt': self.video_stream['pix_fmt'], + 'profile': self.video_stream['profile'], + 'r_frame_rate': self.video_stream['r_frame_rate'], + 'size': { + 'width_height': self.size, + 'height': self.video_stream['height'], + 'width': self.video_stream['width'] + } + }, + 'audio': {}, + 'format': {} + } - if 'video' == probe['streams'][0]['codec_type'].lower(): - video_stream = probe['streams'][0] - elif 'video' == probe['streams'][1]['codec_type'].lower(): - video_stream = probe['streams'][1] - elif 'video' == probe['streams'][2]['codec_type'].lower(): - video_stream = probe['streams'][2] - - if 'audio' == probe['streams'][0]['codec_type'].lower(): - audio_stream = probe['streams'][0] - elif 'audio' == probe['streams'][1]['codec_type'].lower(): - audio_stream = probe['streams'][1] - elif 'audio' == probe['streams'][2]['codec_type'].lower(): - audio_stream = probe['streams'][2] - - format_stream = probe['format'] - - self.stream['video']['size']['width'] = video_stream['width'] - self.stream['video']['size']['height'] = video_stream['height'] - self.stream['video']['r_frame_rate'] = video_stream['r_frame_rate'] - self.stream['video']['bits_per_raw_sample'] = video_stream['bits_per_raw_sample'] - self.stream['video']['seconds'] = float(video_stream['duration']) - self.stream['video']['duration'] = self.convert_from_seconds( - self.stream['video']['seconds'] - ) - self.stream['video']['encoding_brand'] = format_stream['tags']['major_brand'] - self.stream['video']['codec_long_name'] = video_stream['codec_long_name'] - self.stream['video']['profile'] = video_stream['profile'] - self.stream['video']['pix_fmt'] = video_stream['pix_fmt'] - - return self.stream + return self.stream \ No newline at end of file diff --git a/scripts/gen_gui_py.sh b/scripts/gen_gui_py.sh index 2d7b022..e861824 100755 --- a/scripts/gen_gui_py.sh +++ b/scripts/gen_gui_py.sh @@ -1,2 +1,5 @@ pyuic6 BitMover.ui -o _BitMover_MainWindow.py pyuic6 import_dialogue.ui -o _import_dialog_Window.py +pyuic6 _finding_files_dialog.ui -o _finding_files_dialog_Window.py +pyuic6 _ComparisonDialog.ui -o _comparison_dialog_Window.py +pyuic6 _checksum_progress_dialog.ui -o _checksum_progress_dialog_Window.py diff --git a/scripts/start_qt6_designer.sh b/scripts/start_qt6_designer.sh new file mode 100755 index 0000000..4906212 --- /dev/null +++ b/scripts/start_qt6_designer.sh @@ -0,0 +1 @@ +qt6-tools designer