Compare commits

..

4 Commits

Author SHA1 Message Date
Kameron Kenny aa973011c6
copy files 2024-10-03 17:17:18 -04:00
Kameron Kenny 410a0e3e87
refactor 2024-10-02 16:28:10 -04:00
Kameron Kenny 3a79f22315
time and date utils 2024-10-02 16:27:13 -04:00
Kameron Kenny c5959a945f
foo 2024-09-30 15:20:03 -04:00
32 changed files with 1814 additions and 1170 deletions

View File

@ -11,7 +11,13 @@
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
<string>BitMover</string>
</property>
<property name="windowIcon">
<iconset theme="applications-science"/>
</property>
<property name="unifiedTitleAndToolBarOnMac">
<bool>true</bool>
</property>
<widget class="QWidget" name="centralwidget">
<widget class="QWidget" name="gridLayoutWidget">
@ -88,7 +94,7 @@
<property name="geometry">
<rect>
<x>910</x>
<y>650</y>
<y>610</y>
<width>311</width>
<height>211</height>
</rect>
@ -292,7 +298,7 @@
<property name="geometry">
<rect>
<x>910</x>
<y>630</y>
<y>590</y>
<width>371</width>
<height>16</height>
</rect>
@ -334,11 +340,55 @@
<rect>
<x>910</x>
<y>50</y>
<width>182</width>
<height>91</height>
<width>541</width>
<height>41</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_2" rowstretch="0,0" columnstretch="0,1">
<layout class="QGridLayout" name="gridLayout_2" rowstretch="0,0" columnstretch="0,0,0,0,0">
<item row="0" column="2">
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Maximum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>180</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="3">
<widget class="QLabel" name="label_4">
<property name="font">
<font>
<pointsize>18</pointsize>
</font>
</property>
<property name="text">
<string>Files Imported</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLCDNumber" name="lcd_files_found">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_3">
<property name="font">
@ -351,120 +401,25 @@
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLCDNumber" name="lcd_files_found"/>
</item>
<item row="1" column="1">
<widget class="QLCDNumber" name="lcd_files_imported"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="font">
<font>
<pointsize>18</pointsize>
</font>
<item row="0" column="4">
<widget class="QLCDNumber" name="lcd_files_imported">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Files Imported</string>
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="gridLayoutWidget_5">
<property name="geometry">
<rect>
<x>1100</x>
<y>50</y>
<width>351</width>
<height>93</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="2" column="1">
<widget class="QProgressBar" name="progressBar_importing_2">
<property name="value">
<number>24</number>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLCDNumber" name="lcd_import_progress">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QLCDNumber" name="lcd_current_file_progress">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="l_proecessing_progress">
<property name="text">
<string>Processing Progress</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="l_current_file_progress">
<property name="text">
<string>Current File Progress</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QProgressBar" name="progressBar_processing">
<property name="value">
<number>24</number>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QProgressBar" name="progressBar_importing">
<property name="value">
<number>24</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="l_import_progress">
<property name="text">
<string>Import Progress</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLCDNumber" name="lcd_processing_progress">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QLabel" name="label_2">
<property name="text">
<string>%</string>
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QLabel" name="label_5">
<property name="text">
<string>%</string>
</property>
</widget>
</item>
<item row="2" column="3">
<widget class="QLabel" name="label_6">
<property name="text">
<string>%</string>
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
</widget>
</item>
@ -474,7 +429,7 @@
<property name="geometry">
<rect>
<x>910</x>
<y>150</y>
<y>110</y>
<width>541</width>
<height>371</height>
</rect>
@ -493,7 +448,7 @@
<property name="geometry">
<rect>
<x>910</x>
<y>530</y>
<y>490</y>
<width>541</width>
<height>91</height>
</rect>
@ -643,7 +598,8 @@
<string>Import Media</string>
</property>
<property name="icon">
<iconset theme="drive-harddisk"/>
<iconset theme="drive-harddisk">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
@ -685,7 +641,8 @@
<string>Images</string>
</property>
<property name="icon">
<iconset theme="camera-photo"/>
<iconset theme="camera-photo">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="checked">
<bool>true</bool>
@ -698,7 +655,8 @@
<string>Video</string>
</property>
<property name="icon">
<iconset theme="camera-video"/>
<iconset theme="camera-video">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="checked">
<bool>true</bool>
@ -711,7 +669,8 @@
<string>Audio</string>
</property>
<property name="icon">
<iconset theme="multimedia-player"/>
<iconset theme="multimedia-player">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="checked">
<bool>true</bool>
@ -755,7 +714,7 @@
<x>0</x>
<y>0</y>
<width>1463</width>
<height>24</height>
<height>21</height>
</rect>
</property>
<widget class="QMenu" name="menuBit_Mover">

View File

@ -1,52 +1,80 @@
#!/usr/bin/env python
import os
from os import path
from os import rename
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
from _media_file import MediaFile
from _file_stuff import path_exists,is_dir,is_file,create_folder,cmp_files
from _hashing import hash_path
log = timber(__name__)
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)
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()
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.imp_dialog = DialogImport()
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)
@ -60,13 +88,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,52 +101,102 @@ 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 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
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,path_file_name):
preview = MediaPreview(path_file_name=path_file_name,
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):
if preview.file_type.lower() == 'video':
thumb_ratio = 16 / 9 # 1.77
elif preview.file_type.lower() == 'image':
thumb_ratio = preview.thumbnail_ratio
else:
thumb_ratio = 1
self.set_thumbnail(preview.thumbnail,
ratio=thumb_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']
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.l_meta_content_date_time_c.setText(
f"{f_date['y']}/{f_date['m']}/{f_date['d']}"
)
if f['file_type'] == 'image':
f_photo = f['image_meta']['photo']
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()
if preview.file_type == 'image':
self.l_meta_01.setText('Size')
self.l_meta_02.setText('dpi')
self.l_meta_03.setText('ISO')
@ -133,16 +205,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(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['lens_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['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')
@ -152,14 +226,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_frame_rate']))
self.l_meta_content_03.setText(str(f_video['bits_per_raw_sample']))
self.l_meta_content_04.setText(str(f_video['duration']))
self.l_meta_content_05.setText(str(f_video['encoding_brand']))
self.l_meta_content_06.setText(str(f_video['codec_long_name']))
self.l_meta_content_07.setText(str(f_video['profile']))
self.l_meta_content_08.setText(str(f_video['pix_fmt']))
def clear_metadata(self):
self.l_meta_01.setText('')
@ -182,244 +256,57 @@ 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)
self.process_file(i.text())
print(f'index changed to: {i.text()}')
preview = self.get_preview(i.text())
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 get_event(self):
self.event_name = self.eventName.text()
return self.event_name
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_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 set_total_files(self,t):
self.total_files = t
self.lcd_files_found.display(self.total_files)
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')))
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 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)
file_total += int(1)
else:
print(f"Skipping {current_file} as it does not look like a real file.")
return file_total
def t_find_files(self,
progress_callback,
import_progress_callback,
current_file_progress_callback,):
file_count = int(0)
search_types = []
def set_total_files(self,t):
total_file_count = t
self.lcd_files_found.display(total_file_count)
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']))
def set_imported_files(self,t):
self.lcd_files_imported.display(t)
@staticmethod
def print_output(s):
print(s)
print(f'output: {s}')
def worker_thread_started(self):
print('scan thread started')
@ -429,7 +316,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
def worker_thread_done(self):
print('scan thread complete.')
self.toggle_scan_button(True)
if 0 < len(self.files):
if 0 < len(self.file_list):
self.toggle_import_button(True)
else:
self.toggle_import_button(False)
@ -438,105 +325,225 @@ class MainWindow(QMainWindow, Ui_MainWindow):
def thread_complete():
print("THREAD COMPLETE.")
def add_found_file_to_list(self,f):
sph = self.get_source_path_hash(f)
self.files[sph] = {}
self.file_list.addItem(f)
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)
# 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()
# time.sleep(3)
# File Stuff
self.total_files = 0
self.file_total = 0
self.files = {}
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 = 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.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.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)
# From _media_import.py
def is_video(self,sph):
print(f"Should be sph: {sph}")
print(f"Should be filetype: {self.files[sph]['file_type']}")
if self.files[sph]['file_type'] == 'video':
r = True
else:
r = False
return r
def is_image(self,sph):
if self.files[sph]['file_type'] == 'image':
r = True
else:
r = False
return r
def t_copy_files(self,
progress_callback,
current_file_progress_callback,
imported_file_count_callback,
found_file_callback,
total_file_count_callback
):
""" Copy Files. """
count = int(0)
for line in range(self.file_list.count()):
item = self.file_list.item(line)
file = item.text()
self.process_file(file)
sph = self.get_source_path_hash(file)
print(sph)
self.src_dir = self.files[sph]['folders']['source_path']
self.dst_dir = self.files[sph]['folders']['destination']
self.path_file_source = path.join(self.src_dir,
self.files[sph]['name'])
self.path_file_destination = path.join(self.dst_dir,
self.files[sph]['name'])
self.destination_original_path = path.join(self.dst_dir,
self.files[sph]['folders']['destination_original'])
self.path_file_destination_original = path.join(self.dst_dir,
self.files[sph]['folders']['destination_original'],
self.files[sph]['name'])
self.imp_dialog.set_importing_file(self.path_file_source)
self.copy_a_file(
sph,
progress_callback,
current_file_progress_callback,
imported_file_count_callback,
found_file_callback,
total_file_count_callback
)
if self.check_store_original(sph) is True:
self.copy_a_file(sph,
progress_callback,
current_file_progress_callback,
imported_file_count_callback,
found_file_callback,
total_file_count_callback)
count += 1
imported_file_count_callback.emit(count)
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,
fph,
progress_callback,
current_file_progress_callback,
imported_file_count_callback,
found_file_callback,
total_file_count_callback):
size = path.getsize(self.path_file_source)
create_folder(self.dst_dir)
self.check_duplicate(fph)
if self.is_video(fph):
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,sph):
if self.config['store_originals'] is True:
if self.is_image(sph):
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,sph):
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[sph]['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
"""
# 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)
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.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(f'verify_checksum,self.config: {self.config}')
def process_file(self,p):
""" gather information and add to dictionary """
app = QApplication(sys.argv)
path_name = os.path.dirname(p)
f_name = os.path.basename(p)
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()

View File

@ -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."))

153
_ComparisonDialog.ui Normal file
View File

@ -0,0 +1,153 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FileComparisonDialog</class>
<widget class="QDialog" name="FileComparisonDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1588</width>
<height>753</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<widget class="QTableWidget" name="tableWidget">
<property name="geometry">
<rect>
<x>20</x>
<y>90</y>
<width>1551</width>
<height>651</height>
</rect>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="tabKeyNavigation">
<bool>false</bool>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="rowCount">
<number>2</number>
</property>
<property name="columnCount">
<number>5</number>
</property>
<attribute name="horizontalHeaderDefaultSectionSize">
<number>300</number>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>true</bool>
</attribute>
<row/>
<row/>
<column/>
<column/>
<column/>
<column/>
<column/>
</widget>
<widget class="QWidget" name="gridLayoutWidget">
<property name="geometry">
<rect>
<x>240</x>
<y>11</y>
<width>981</width>
<height>71</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="2">
<widget class="QLabel" name="label_4">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="label_3">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel" name="label_2">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_6">
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Source</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_7">
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Destination</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QLabel" name="label_5">
<property name="geometry">
<rect>
<x>20</x>
<y>10</y>
<width>231</width>
<height>61</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>28</pointsize>
</font>
</property>
<property name="text">
<string>Comparing Files</string>
</property>
</widget>
</widget>
<resources/>
<connections/>
</ui>

47
__model.py Normal file
View File

@ -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
}
}

62
_audio.py Normal file
View File

@ -0,0 +1,62 @@
#!/usr/bin/env python
import os.path
import ffmpeg
from datetime import datetime
from _time_and_date_utils import convert_from_seconds
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
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 = {}
def get_audio_capture_date(self):
#TODO: refactor this try/except logic.
stamp = None
if self.format_stream is not None:
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_audio_meta(self):
self.stream = {
'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']
},
'video': {},
'format': {}
}
return self.stream

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ChecksumProgressDialog</class>
<widget class="QDialog" name="ChecksumProgressDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>640</width>
<height>86</height>
</rect>
</property>
<property name="windowTitle">
<string>Checksum Progress</string>
</property>
<widget class="QWidget" name="gridLayoutWidget">
<property name="geometry">
<rect>
<x>20</x>
<y>20</y>
<width>601</width>
<height>21</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="l_title_getting_checksum">
<property name="minimumSize">
<size>
<width>100</width>
<height>1</height>
</size>
</property>
<property name="baseSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="text">
<string>Getting Checksum For</string>
</property>
<property name="margin">
<number>3</number>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="l_content_checksum_filename">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QProgressBar" name="progressBar_getting_checksum">
<property name="geometry">
<rect>
<x>20</x>
<y>50</y>
<width>611</width>
<height>19</height>
</rect>
</property>
<property name="value">
<number>0</number>
</property>
<property name="textVisible">
<bool>true</bool>
</property>
</widget>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -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"))

View File

@ -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"))

View File

@ -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 """

View File

@ -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
@ -126,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]

51
_find_files.py Normal file
View File

@ -0,0 +1,51 @@
import os
from _file_stuff import is_file
basedir = os.path.dirname(__file__)
class FindFiles:
def __init__(self,
search_types,
src_dir,
file_total,
file_types,
*args,
**kwargs):
super(FindFiles,self).__init__(*args,**kwargs)
self.file_total = file_total
self.src_dir = src_dir
self.search_types = search_types
self.file_types = file_types
self.file_name = None
self.path_file_source = None
self.file_type = None
self.file_ext = None
def t_find_files(self,
progress_callback,
current_file_progress_callback,
imported_file_count_callback,
found_file_callback,
total_file_count_callback):
file_count = int(0)
if len(self.search_types) > 0:
for folder, subfolders, filename in os.walk(self.src_dir):
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)
# process_file(current_file)
found_file_callback.emit(self.path_file_source)
else:
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.")

31
_find_files_dialog.py Normal file
View File

@ -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)

59
_finding_files_dialog.ui Normal file
View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FindProgress</class>
<widget class="QDialog" name="FindProgress">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>640</width>
<height>60</height>
</rect>
</property>
<property name="windowTitle">
<string>Finding Files</string>
</property>
<widget class="QWidget" name="gridLayoutWidget_5">
<property name="geometry">
<rect>
<x>20</x>
<y>10</y>
<width>601</width>
<height>41</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="l_find_progress">
<property name="text">
<string>Progress</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLCDNumber" name="lcd_import_progress">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QProgressBar" name="progressBar_importing">
<property name="value">
<number>24</number>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QLabel" name="label_5">
<property name="text">
<string>%</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -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", "%"))

View File

@ -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

View File

@ -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'f[file]: {f[file]}')
i = i + 1

23
_image_tag.py Normal file
View File

@ -0,0 +1,23 @@
import exifread
from _time_and_date_utils import get_img_date
class ImageTag:
def __init__(self,path_file_name,*args,**kwargs):
super(ImageTag,self).__init__(*args,**kwargs)
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
for tag in self.image_tags:
if t.lower() in tag.lower():
tag_data = self.image_tags[tag]
break
return tag_data
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)
return tags

View File

@ -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)

View File

@ -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'])

View File

@ -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

160
_media.py
View File

@ -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

160
_media_file.py Normal file
View File

@ -0,0 +1,160 @@
import os.path
import sys
from _audio import AudioFile
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:
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 = os.path.dirname(self.path_file_name)
self.base_dst_dir = self.config['folders']['destination']['base']
self.source_path_hash = hash_path(self.path_file_name)
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.dst_dir = self.set_destination_path()
self.destination_originals_path = ''
self.media = {}
self.media_meta = self.get_media_meta()
# 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_file_type(self):
""" determine if the extension is in the list of file types """
for t in self.config['file_types']:
for e in self.config['file_types'][t]:
if self.file_ext.lower().endswith(e.lower()):
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 = 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':
photo_media = PhotoFile(path_file_name=self.path_file_name)
stamp = photo_media.photo_capture_date
elif self.file_type == 'video':
video_media = VideoFile(path_file_name=self.path_file_name)
stamp = video_media.get_video_capture_date()
elif self.file_type == 'audio':
audio_media = AudioFile(path_file_name=self.path_file_name)
stamp = 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 get_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,
'file_type': self.file_type
}
if self.file_type == 'video':
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_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
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

16
_media_import.py Normal file
View File

@ -0,0 +1,16 @@
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

171
_photo.py Normal file
View File

@ -0,0 +1,171 @@
#!/usr/bin/env python
from PIL import Image
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:
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.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()
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.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.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
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):
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):
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:
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):
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 photo_meta

68
_preview.py Normal file
View File

@ -0,0 +1,68 @@
#!/usr/bin/env python
from PIL import Image
from _media_file import MediaFile
from _raw_photo import extract_jpg_thumb
from _video import VideoFile
from _hashing import hash_path
class MediaPreview:
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 = 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.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.thumbnail_ratio = self.media_files_list[self.source_path_hash]['image_meta']['photo']['size']['ratio']
self.mpixels = self.media_files_list[self.source_path_hash]['image_meta']['photo']['size']['megapixels']
self._img_preview()
elif self.media_files_list[self.source_path_hash]['file_type'] == 'video':
self._video_preview()
def _img_preview(self):
if self.media_files_list[self.source_path_hash]['image_meta']['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]['image_meta']['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(path_file_name=self.path_file_name)
self.thumbnail = vid.gen_video_thumbnail()
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_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']

View File

@ -27,9 +27,10 @@ class WorkerSignals(QObject):
error = pyqtSignal(tuple)
result = pyqtSignal(object)
progress = pyqtSignal(int)
import_progress = pyqtSignal(int)
current_file_progress = pyqtSignal(float)
# current_import_file = pyqtSignal()
imported_file_count = pyqtSignal(int)
found_file = pyqtSignal(str)
total_file_count = pyqtSignal(int)
class Worker(QRunnable):
"""
@ -55,9 +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['current_import_file'] = self.signals.current_import_file
self.kwargs['imported_file_count_callback'] = self.signals.imported_file_count
self.kwargs['found_file_callback'] = self.signals.found_file
self.kwargs['total_file_count_callback'] = self.signals.total_file_count
@pyqtSlot()
def run(self):

39
_time_and_date_utils.py Normal file
View File

@ -0,0 +1,39 @@
import time
from datetime import datetime
def convert_from_seconds(seconds):
return time.strftime("%H:%M:%S", time.gmtime(seconds))
def set_generic_date_time(datestamp='1900:01:01',
timestamp='00:00:00',
f='%Y:%m:%d %H:%M:%S'):
t = datetime.strptime(str(f'{datestamp} {timestamp}'), f)
return t
def process_img_date_tag(tag, image_tags, date_time_format):
try:
t = datetime.strptime(str(image_tags[tag]), date_time_format)
except:
t = None
return t
def get_img_date(image_tags):
t = None
for tag in image_tags:
if 'DateTime' in tag:
t = tag
break
for f in ['%Y:%m:%d %H:%M:%S',
'%Y/%m/%d %H:%M:%S',
'%Y-%m-%d-%H-%M-%S']:
dt_tag = process_img_date_tag(t,image_tags,f)
if dt_tag is not None:
break
if dt_tag is None:
dt_tag = set_generic_date_time()
return dt_tag

48
_verify_file_checksum.py Normal file
View File

@ -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

192
_video.py
View File

@ -1,127 +1,137 @@
#!/usr/bin/env python
import os.path
import sys
import ffmpeg
import time
from datetime import datetime
class Video:
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
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
self.stream = {
'video': {},
'format': {},
'audio': {}
}
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.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'] = ''
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['format'] = {}
self.stream['format']['major_brand'] = ''
self.stream['format']['compatible_brands'] = ''
self.stream['format']['creation_time'] = ''
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.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):
"""
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)
self.video_meta()
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.path_file_name,
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_meta(self):
probe = ffmpeg.probe(self.file)
def get_video_width(self):
return self.video_stream['width']
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]
def get_video_height(self):
return self.video_stream['height']
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]
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}'
format_stream = probe['format']
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
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']
def 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'],
'seconds': float(self.video_stream['duration']),
'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
return self.stream

View File

@ -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

1
scripts/start_qt6_designer.sh Executable file
View File

@ -0,0 +1 @@
qt6-tools designer