diff --git a/BitMover_ui.py b/BitMover_ui.py index 45c7d1b..780dbe5 100755 --- a/BitMover_ui.py +++ b/BitMover_ui.py @@ -1,6 +1,7 @@ #!/usr/bin/env python import os import sys +import time from PyQt6.QtCore import QThreadPool from PyQt6.QtGui import QIcon, QPixmap @@ -11,15 +12,20 @@ from _configure import CONFIG_FILE, Configure from _find_files import FindFiles from _find_files_dialog import FindProgress from _import_dialog import DialogImport -from _media_import import MediaImporter +# from _media_import import MediaImporter from _preview import MediaPreview from _thread_my_stuff import Worker +from _media_file import MediaFile +from _file_stuff import (path_exists, + is_dir, + is_file) +from _hashing import hash_path + basedir = os.path.dirname(__file__) # TODO: verify source dir actually exists -# Subclass QMainWindow to customize your application's main window class MainWindow(QMainWindow, Ui_MainWindow): def __init__(self, *args, **kwargs): super(MainWindow,self).__init__(*args,**kwargs) @@ -27,22 +33,49 @@ class MainWindow(QMainWindow, Ui_MainWindow): 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.config = None + self.src_dir = None + self.dst_dir = None + self.verify_checksum = None + self.cleanup_files = None + self.store_originals = None + self.file_types = None + self.source_path_hash = None + self.search_types = None + self.chunk_size = (16 * 1024) * 1 + self.load_config() + + self.destination_original_path = None + self.path_file_source = None + self.path_file_destination = None + self.path_file_destination_original = None + # File Stuff self.total_files = 0 + self.file_total = 0 self.files = {} + + self.event_name = None + self.imp_dialog = DialogImport() self.find_files_dialog = FindProgress() self.widgets_config() + def load_config(self): + try: + 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.get('verify_checksum', False) + self.cleanup_files = self.config.get('cleanup_sd', False) + self.store_originals = self.config.get('store_originals', False) + self.file_types = self.config.get('file_types', {}) + except KeyError as e: + log.error(f"Missing configuration key: {e}") + sys.exit(1) # or provide a fallback + def widgets_config(self): # Button Setup self.pushButton_src_browse.clicked.connect(self.select_src_directory) @@ -98,6 +131,10 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.dst_dir = directory self.lineEdit_dst_dir.setText(self.dst_dir) + def get_source_path_hash(self,f): + self.source_path_hash = hash_path(f) + return self.source_path_hash + def verify_checksum_changed(self): if self.checkBox_verify_checksum.isChecked(): self.config['verify_checksum'] = True @@ -130,20 +167,20 @@ class MainWindow(QMainWindow, Ui_MainWindow): 'assets', 'preview_placeholder.jpg')) - def get_preview(self,i): - preview = MediaPreview(path_file_name = i.text(), - media_files = self.files) + def get_preview(self,path_file_name): + preview = MediaPreview(path_file_name=path_file_name, + media_files=self.files) return preview def update_preview(self,preview): - self.set_thumbnail(preview.thumbnail,ratio=preview.thumbnail_ratio) + 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( @@ -151,7 +188,9 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.l_meta_content_date_time_c.setText( f"{f_date['y']}/{f_date['m']}/{f_date['d']}" ) - if preview.file_type == 'image': + if f['file_type'] == 'image': + f_photo = f['image_meta']['photo'] + self.l_meta_01.setText('Size') self.l_meta_02.setText('dpi') self.l_meta_03.setText('ISO') @@ -160,16 +199,18 @@ 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(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'])) + 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['lens_focal_length'])) + self.l_meta_content_06.setText(str("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['size']['megapixels'])) + + elif f['file_type'] == 'video': + f_video = f['video_meta']['video'] - elif preview.file_type == 'video': self.l_meta_01.setText('Size') self.l_meta_02.setText('Frames / Second') self.l_meta_03.setText('Bit Depth') @@ -211,13 +252,43 @@ class MainWindow(QMainWindow, Ui_MainWindow): if i is None: self.set_default_thumbnail() else: - preview = self.get_preview(i) + print(f'index changed to: {i.text()}') + preview = self.get_preview(i.text()) self.update_preview(preview) self.update_metadata(preview) def get_event(self): - event_name = self.eventName.text() - return event_name + self.event_name = self.eventName.text() + return self.event_name + + 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): + file_total = 0 + 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): + file_total += int(1) + else: + print(f"Skipping {current_file} as it does not look like a real file.") + return file_total def set_total_files(self,t): total_file_count = t @@ -228,7 +299,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): @staticmethod def print_output(s): - print(s) + print(f'output: {s}') def worker_thread_started(self): print('scan thread started') @@ -253,13 +324,30 @@ class MainWindow(QMainWindow, Ui_MainWindow): def gen_file_dict(self,d): self.files = d + def process_file(self,p): + """ gather information and add to dictionary """ + media_file = MediaFile(path_file_name=p, + config=self.config, + event_name=self.event_name) + i = self.get_source_path_hash(p) + print(f'processing: {p}\nhash: {i}') + self.files[i] = media_file.media + def find_files(self): """ find files to build a dictionary out of """ self.files = {} - self.set_total_files(0) + self.get_event() + self.get_search_types() + self.file_total = self.get_t_files() + self.set_total_files(self.file_total) - file_finder = FindFiles() + time.sleep(3) + + file_finder = FindFiles(search_types=self.search_types, + src_dir=self.src_dir, + file_types=self.file_types, + file_total=self.file_total) worker = Worker(file_finder.t_find_files) worker.signals.started.connect( @@ -270,10 +358,12 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.find_files_dialog.set_progress_finding_files) worker.signals.found_file.connect( self.add_found_file_to_list) + worker.signals.found_file.connect( + self.process_file) worker.signals.total_file_count.connect( self.set_total_files) - worker.signals.result.connect( - self.gen_file_dict) + # worker.signals.result.connect( + # self.gen_file_dict) worker.signals.finished.connect( self.thread_complete) worker.signals.finished.connect( @@ -284,6 +374,108 @@ class MainWindow(QMainWindow, Ui_MainWindow): # Execute. self.threadpool.start(worker) + # From _media_import.py + 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)) + + # END from _media_import.py + def import_files(self): """ Import found files @@ -294,23 +486,17 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.imp_dialog.set_progress_importing(0) self.imp_dialog.set_progress_current_file(0) - - importer = MediaImporter() - - worker = Worker(importer.t_copy_files) - + 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.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( @@ -323,8 +509,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): def verify_checksum(self): # fh_match = FileHash() - print(self.config) - + print(f'verify_checksum,self.config: {self.config}') app = QApplication(sys.argv) diff --git a/__model.py b/__model.py index e69de29..32b7bae 100644 --- a/__model.py +++ b/__model.py @@ -0,0 +1,47 @@ +m = { + 'f5c7c1a4e55d6288': { + 'date': { + 'capture_date': { + 'y': '2024', + 'm': '09', + 'd': '08' + } + }, + 'event': { + 'name': '' + }, + 'extension': 'JPG', + 'folders': { + 'destination': '/Users/kkenny/import/testing_dst/2024/2024-09/2024-09-08/PHOTO/JPG', + 'destination_original': '', + 'source_path': '/Users/kkenny/import/testing_src' + }, + 'name': 'DSC_6481.JPG', + 'source_path_hash': 'f5c7c1a4e55d6288', + 'type': 'image', + 'image_meta': { + 'photo': { + 'aperture': (0x829D) Ratio = 11 @ 736, + 'camera_brand': (0x010F)ASCII = NIKON CORPORATION @ 148, + 'camera_model': (0x0110)ASCII = NIKON D5100 @ 168, + 'dpi': < bound method PhotoFile.get_dpi of < _photo.PhotoFile object at 0x130f05fd0 >>, + 'is_jpg': True, + 'is_raw': False, + 'iso': (0x8827) Short = 3200 @ 274, + 'lens_make': None, + 'lens_model': None, + 'lens_focal_length': (0x920A) Ratio = 135 @ 808, + 'size': { + 'height': 3264, + 'width': 4928, + 'width_height': '4928x3264', + 'megapixels': 16084992, + 'ratio': 1.5098039215686274 + } + } + }, + 'video_meta': None, + 'audio_meta': None + } +} + diff --git a/_audio.py b/_audio.py index be65c1e..ac04278 100644 --- a/_audio.py +++ b/_audio.py @@ -1,76 +1,61 @@ #!/usr/bin/env python import os.path import ffmpeg -import time from datetime import datetime +from _time_and_date_utils import convert_from_seconds -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'] +class AudioFile: + def __init__(self,path_file_name,*args,**kwargs): + # super(AudioFile, self).__init__(*args, **kwargs) + self.path_file_name = path_file_name self.probe = ffmpeg.probe(self.path_file_name) self.audio_capture_date = self.get_audio_capture_date() + self.video_stream = None + self.audio_stream = None + self.format_stream = None - 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'] + for i in self.probe['streams']: + if self.video_stream is None: + if 'video' == i['codec_type'].lower(): + self.video_stream = i + if self.audio_stream is None: + if 'audio' == i['codec_type'].lower(): + self.audio_stream = i + try: + self.format_stream = self.probe['format'] + except AttributeError as e: + print(f"{e}: Audio file has no format string") + self.format_stream = None self.stream = {} - @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: + stamp = None + if self.format_stream is not None: 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' - ) + 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): + def get_audio_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': { + 'audio_channels': self.audio_stream['channels'], + 'bits_per_sample': self.audio_stream['bits_per_raw_sample'], + 'codec_long_name': self.audio_stream['codec_long_name'], + 'duration': convert_from_seconds(float(self.audio_stream['duration'])), + 'encoding_brand': self.format_stream['tags']['encoded_by'], + 'sample_rate': self.audio_stream['sample_rate'] }, - 'audio': {}, + 'video': {}, 'format': {} } diff --git a/_file_stuff.py b/_file_stuff.py index 59224fa..af4fa0e 100644 --- a/_file_stuff.py +++ b/_file_stuff.py @@ -96,3 +96,11 @@ def validate_config_dir_access(config): accessible = True return accessible +def get_file_name(path_file_name): + return os.path.basename(path_file_name) + +def get_dotted_file_ext(file_name): + return os.path.splitext(file_name)[1] + +def get_file_ext(file_name): + return get_dotted_file_ext(file_name).split('.')[1] \ No newline at end of file diff --git a/_find_files.py b/_find_files.py index 335bf93..eca7c33 100644 --- a/_find_files.py +++ b/_find_files.py @@ -1,84 +1,51 @@ 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'] +class FindFiles: + def __init__(self, + search_types, + src_dir, + file_total, + file_types, + *args, + **kwargs): + super(FindFiles,self).__init__(*args,**kwargs) - self.file_total = 0 - self.file_list = {} + self.file_total = file_total + self.src_dir = src_dir + self.search_types = search_types + self.file_types = file_types - 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.") + self.file_name = None + self.path_file_source = None + self.file_type = None + self.file_ext = None def t_find_files(self, progress_callback, - found_file, - total_file_count): + current_file_progress_callback, + imported_file_count_callback, + found_file_callback, + total_file_count_callback): 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): + for self.file_type in self.search_types: + for self.file_ext in self.file_types[self.file_type]: + for self.file_name in filename: + if self.file_name.lower().endswith(self.file_ext): + self.path_file_source = os.path.join(folder, self.file_name) + if is_file(self.path_file_source): file_count += int(1) - self.process_file(current_file) - found_file.emit(current_file) + # process_file(current_file) + found_file_callback.emit(self.path_file_source) else: - print(f"Skipping {current_file} as it does not look like a real file.") - total_file_count.emit(file_count) + print(f"Skipping {self.path_file_source} as it does not look like a real file.") + total_file_count_callback.emit(file_count) progress_callback.emit(round((file_count / self.file_total) * 100, 0)) else: print("Nothing to search for.") - 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/_hashing.py b/_hashing.py index 63de051..d19e2ce 100644 --- a/_hashing.py +++ b/_hashing.py @@ -58,5 +58,5 @@ def validate_xx_checksums(f): print(f'FATAL: Checksum validation failed for: \ {f[file]["name"]} \n{c[i]}\n is not equal to \n{c[p]}\n') print('\n File Meta:\n') - print(f[file]) + print(f'f[file]: {f[file]}') i = i + 1 \ No newline at end of file diff --git a/_image_tag.py b/_image_tag.py index b792b21..da44ab4 100644 --- a/_image_tag.py +++ b/_image_tag.py @@ -1,14 +1,12 @@ import exifread -from datetime import datetime -from _media_file import MediaFile +from _time_and_date_utils import get_img_date -class ImageTag(MediaFile): - def __init__(self,path_file_name=None,*args,**kwargs): +class ImageTag: + def __init__(self,path_file_name,*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.path_file_name = path_file_name self.image_tags = self.get_image_tags() + self.date_time_original = get_img_date(self.image_tags) def get_image_tag(self,t): tag_data = None @@ -18,44 +16,8 @@ class ImageTag(MediaFile): 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): + print(f'get_image_tags, self.path_file_name: {self.path_file_name}') 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 + return tags \ No newline at end of file diff --git a/_media_file.py b/_media_file.py index 7d0d975..472deb4 100644 --- a/_media_file.py +++ b/_media_file.py @@ -2,56 +2,50 @@ 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 _file_stuff import (get_file_name, + get_file_ext, + get_dotted_file_ext) 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 +class MediaFile: + def __init__(self, + path_file_name, + config, + event_name, + *args,**kwargs): + # super(MediaFile,self).__init__(*args,**kwargs) + self.path_file_name = path_file_name + self.config = config + self.event_name = str(event_name) + self.store_originals = self.config['store_originals'] + self.src_dir = self.config['folders']['source']['base'] + self.base_dst_dir = self.config['folders']['destination']['base'] 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_name = get_file_name(self.path_file_name) + self.dotted_file_ext = get_dotted_file_ext(self.file_name) + self.file_ext = get_file_ext(self.file_name) self.file_type = self.get_file_type() self.capture_date = self.get_capture_date() self.capture_date_year = self.capture_date[0] self.capture_date_month = self.capture_date[1] - self.capture_Date_day = self.capture_date[2] + self.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() + self.media_meta = self.get_media_meta() - def get_file_name(self): - return os.path.basename(self.path_file_name) + # video_media = VideoFile(path_file_name=self.path_file_name) + # audio_media = AudioFile(path_file_name=self.path_file_name) + # photo_media = PhotoFile(path_file_name=self.path_file_name) - def get_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): + def get_file_type(self): """ 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): + if self.file_ext.lower().endswith(e.lower()): return t def set_destination_path(self): @@ -62,7 +56,7 @@ class MediaFile(MainWindow): """ 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}' + p3 = f'{self.capture_date_year}-{self.capture_date_month}-{self.capture_date_day}' p = f'{p1}/{p2}/{p3}' # <--- Dumb. if self.event_name: @@ -95,12 +89,16 @@ class MediaFile(MainWindow): def get_capture_date(self): """ get capture date from meta """ + if self.file_type == 'image': - stamp = self.photo_media.photo_capture_date + photo_media = PhotoFile(path_file_name=self.path_file_name) + stamp = photo_media.photo_capture_date elif self.file_type == 'video': - stamp = self.video_media.get_video_capture_date() + video_media = VideoFile(path_file_name=self.path_file_name) + stamp = video_media.get_video_capture_date() elif self.file_type == 'audio': - stamp = self.audio_media.get_audio_capture_date() + audio_media = AudioFile(path_file_name=self.path_file_name) + stamp = audio_media.get_audio_capture_date() else: try: stamp = datetime.fromtimestamp( @@ -117,13 +115,13 @@ class MediaFile(MainWindow): day = stamp.strftime("%d") return year, month, day - def media_meta(self): + def get_media_meta(self): self.media = { 'date': { 'capture_date': { 'y': self.capture_date_year, 'm': self.capture_date_month, - 'd': self.capture_Date_day + 'd': self.capture_date_day } }, 'event': { @@ -137,16 +135,26 @@ class MediaFile(MainWindow): }, 'name': self.file_name, 'source_path_hash': self.source_path_hash, - 'type': self.file_type + 'file_type': self.file_type } if self.file_type == 'video': - self.media['video_meta'] = self.video_media.get_video_meta() + video_media = VideoFile(path_file_name=self.path_file_name) + self.media['video_meta'] = video_media.video_meta() self.media['image_meta'] = None self.media['audio_meta'] = None if self.file_type == 'image': - photo = PhotoFile() - self.media['image_meta'] = photo.get_photo_meta() + photo_media = PhotoFile(path_file_name=self.path_file_name) + self.media['image_meta'] = photo_media.get_photo_meta() self.media['video_meta'] = None - self.media['audio_meta'] = None \ No newline at end of file + self.media['audio_meta'] = None + + if self.file_type == 'audio': + audio_media = AudioFile(path_file_name=self.path_file_name) + self.media['audio_meta'] = audio_media.get_audio_meta() + self.media['image_meta'] = None + self.media['video_meta'] = None + + print(f'MediaFile, self.media: {self.media}') + return self.media \ No newline at end of file diff --git a/_media_import.py b/_media_import.py index 12330a0..d2b9311 100644 --- a/_media_import.py +++ b/_media_import.py @@ -14,103 +14,3 @@ class MediaImporter(MainWindow): 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 index 11343ea..b19a57f 100644 --- a/_photo.py +++ b/_photo.py @@ -1,21 +1,22 @@ #!/usr/bin/env python from PIL import Image - -from _media_file import MediaFile +from _raw_photo import get_raw_image_dimensions from _image_tag import ImageTag # https://exiftool.org/TagNames/EXIF.html # https://exiftool.org/TagNames/Sony.html -class PhotoFile(MediaFile): - def __init__(self,*args,**kwargs): +class PhotoFile: + def __init__(self,path_file_name,*args,**kwargs): super(PhotoFile, self).__init__(*args, **kwargs) + self.path_file_name = path_file_name self.args = args self.kwargs = kwargs - self.image_tag = ImageTag() - self.is_jpg = False - self.is_raw = False + self.img = None + self.image_tag = ImageTag(path_file_name=self.path_file_name) + self.is_jpg = self.is_photo_jpeg() + self.is_raw = self.is_photo_raw() self.size_width = self.get_photo_width() self.size_height = self.get_photo_height() self.size = self.get_photo_size() @@ -29,19 +30,20 @@ class PhotoFile(MediaFile): 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'): + if self.path_file_name.lower().endswith('jpg') \ + or self.path_file_name.lower().endswith('.jpeg'): self.is_raw = False self.is_jpg = True + self.img = Image.open(self.path_file_name) else: self.is_raw = True self.is_jpg = False @@ -54,10 +56,22 @@ class PhotoFile(MediaFile): return self.is_jpg def get_photo_width(self): - return self.img.width + if self.is_jpg: + width = self.img.width + elif self.is_raw: + width = get_raw_image_dimensions(self.path_file_name)[1] + else: + width = None + return width def get_photo_height(self): - return self.img.height + if self.is_jpg: + height = self.img.height + elif self.is_raw: + height = get_raw_image_dimensions(self.path_file_name)[0] + else: + height = None + return height def get_photo_size(self): if self.size_width is None: @@ -87,7 +101,7 @@ class PhotoFile(MediaFile): return self.image_tag.get_image_tag( 'iso') def get_aperture(self): - return self.image_tag.get_image_tag( 'fnumber') + return self.image_tag.get_image_tag('fnumber') def get_camera_brand(self): tag = self.image_tag.get_image_tag('Make') @@ -122,8 +136,6 @@ class PhotoFile(MediaFile): def get_photographer(self): pass - - def get_orientation(self): pass @@ -134,7 +146,7 @@ class PhotoFile(MediaFile): pass def get_photo_meta(self): - self.photo_meta = { + photo_meta = { 'photo': { 'aperture': self.aperture, 'camera_brand': self.camera_brand, @@ -155,5 +167,5 @@ class PhotoFile(MediaFile): } } } - - return self.photo_meta \ No newline at end of file + + return photo_meta \ No newline at end of file diff --git a/_preview.py b/_preview.py index 40240dc..89ef82f 100644 --- a/_preview.py +++ b/_preview.py @@ -1,19 +1,21 @@ #!/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 +from _hashing import hash_path -class MediaPreview(MainWindow): +class MediaPreview: def __init__(self,path_file_name,media_files): - super(MediaPreview, self).__init__() + # 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.media_file = MediaFile(self.path_file_name) + self.source_path_hash = hash_path(self.path_file_name) + # print(f'_preview.py,MediaPreview:\n\tpath_file_name: {self.path_file_name}\n\thash: {self.source_path_hash}\n\tmedia_files_list: {self.media_files_list}\n') + self.thumbnail = 'thumbnail.jpg' + self.thumbnail_ratio = self.media_files_list[self.source_path_hash]['image_meta']['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': @@ -21,10 +23,10 @@ class MediaPreview(MainWindow): 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'] + self.mpixels = self.media_files_list[self.source_path_hash]['image_meta']['photo']['size']['megapixels'] def _img_preview(self): - if self.media_files_list[self.source_path_hash]['photo']['is_jpg'] is True: + if self.media_files_list[self.source_path_hash]['image_meta']['photo']['is_jpg'] is True: self._jpg_preview() else: self._raw_preview() @@ -37,7 +39,7 @@ class MediaPreview(MainWindow): thumb_width = 500 thumb_size = ( thumb_width, - int(thumb_width // self.media_files_list[self.source_path_hash]['photo']['size']['ratio']) + int(thumb_width // self.media_files_list[self.source_path_hash]['image_meta']['photo']['size']['ratio']) ) try: with Image.open(self.path_file_name) as img: @@ -53,14 +55,14 @@ class MediaPreview(MainWindow): 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.width = self.media_files_list[self.source_path_hash]['video_meta']['video']['size']['width'] + self.height = self.media_files_list[self.source_path_hash]['video_meta']['video']['size']['height'] self.video_framerate = round( - int(self.media_files_list[self.source_path_hash]['video']['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 + int(self.media_files_list[self.source_path_hash]['video_meta']['video']['r_frame_rate'].split('/')[0]) / + int(self.media_files_list[self.source_path_hash]['video_meta']['video']['r_frame_rate'].split('/')[1]),2) + self.video_bit_depth = self.media_files_list[self.source_path_hash]['video_meta']['video']['bits_per_raw_sample'] + self.video_duration = self.media_files_list[self.source_path_hash]['video_meta']['video']['duration'] + self.video_encoding = self.media_files_list[self.source_path_hash]['video_meta']['video']['encoding_brand'] + self.video_codec = self.media_files_list[self.source_path_hash]['video_meta']['video']['codec_long_name'] + self.video_profile = self.media_files_list[self.source_path_hash]['video_meta']['video']['profile'] + self.video_pix_format = self.media_files_list[self.source_path_hash]['video_meta']['video']['pix_fmt'] \ No newline at end of file diff --git a/_thread_my_stuff.py b/_thread_my_stuff.py index 031d869..ff283da 100644 --- a/_thread_my_stuff.py +++ b/_thread_my_stuff.py @@ -27,12 +27,10 @@ class WorkerSignals(QObject): error = pyqtSignal(tuple) result = pyqtSignal(object) 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): """ @@ -58,12 +56,10 @@ class Worker(QRunnable): # Add the callback to our kwargs 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 + self.kwargs['found_file_callback'] = self.signals.found_file + self.kwargs['total_file_count_callback'] = self.signals.total_file_count @pyqtSlot() def run(self): diff --git a/_video.py b/_video.py index f5a808a..c610595 100644 --- a/_video.py +++ b/_video.py @@ -5,33 +5,24 @@ import ffmpeg import time from datetime import datetime -from _media_file import MediaFile - -class VideoFile(MediaFile): - def __init__(self,max_width=1024,*args,**kwargs): - super(VideoFile, self).__init__(*args, **kwargs) - - self.args = args - self.kwargs = kwargs - self.file = kwargs['file'] - self.out = 'thumbnail.jpg' +class VideoFile: + def __init__(self,path_file_name,max_width=1024,*args,**kwargs): + # super(VideoFile, self).__init__(*args, **kwargs) + self.path_file_name = path_file_name self.max_width = max_width + self.out = 'thumbnail.jpg' self.probe = ffmpeg.probe(self.path_file_name) self.video_capture_date = self.get_video_capture_date() + self.video_stream = None + self.audio_stream = None - 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] + for i in self.probe['streams']: + if self.video_stream is None: + if 'video' == i['codec_type'].lower(): + self.video_stream = i + if self.audio_stream is None: + if 'audio' == i['codec_type'].lower(): + self.audio_stream = i self.format_stream = self.probe['format'] self.size_width = self.get_video_width() @@ -48,7 +39,6 @@ class VideoFile(MediaFile): """ Generate a thumbnail from a video """ - self.get_video_meta() time_seconds = self.stream['video']['seconds'] // 5 v_width = int(self.stream['video']['size']['width']) width = self.set_thumb_width(v_width) @@ -56,7 +46,7 @@ class VideoFile(MediaFile): try: ( ffmpeg.input( - self.file, + self.path_file_name, ss = time_seconds ) .filter( @@ -122,7 +112,7 @@ class VideoFile(MediaFile): ) return stamp - def get_video_meta(self): + def video_meta(self): self.stream = { 'video': { 'bits_per_raw_sample': self.video_stream['bits_per_raw_sample'],