diff --git a/LOG-PICT_numtool.py.log b/LOG-PICT_numtool.py.log index 17e5148..bc63b02 100755 --- a/LOG-PICT_numtool.py.log +++ b/LOG-PICT_numtool.py.log @@ -3548,3 +3548,26 @@ 01-08 17:26:17|INFO |click_Renumber |535 : Renamed [/Volumes/TIZIANO-SSD1/Don-Ambrogio-50esimo-WIP/Selezione Fiaccolate/PICTtool-tmp/PICTtmp-1slbb8qn] ==> [/Volumes/TIZIANO-SSD1/Don-Ambrogio-50esimo-WIP/Selezione Fiaccolate/19840507-Fiaccolata-Loreto-Final/19840507-Fiaccolata-Orvieto-Final-0082.jpeg] 01-08 17:26:17|INFO |click_Renumber |535 : Renamed [/Volumes/TIZIANO-SSD1/Don-Ambrogio-50esimo-WIP/Selezione Fiaccolate/PICTtool-tmp/PICTtmp-j88qmrnd] ==> [/Volumes/TIZIANO-SSD1/Don-Ambrogio-50esimo-WIP/Selezione Fiaccolate/19840507-Fiaccolata-Loreto-Final/19840507-Fiaccolata-Orvieto-Final-0083.jpeg] 01-08 17:26:17|INFO |click_Renumber |535 : Renamed [/Volumes/TIZIANO-SSD1/Don-Ambrogio-50esimo-WIP/Selezione Fiaccolate/PICTtool-tmp/PICTtmp-7hozo2i2] ==> [/Volumes/TIZIANO-SSD1/Don-Ambrogio-50esimo-WIP/Selezione Fiaccolate/19840507-Fiaccolata-Loreto-Final/19840507-Fiaccolata-Orvieto-Final-0084.jpeg] +01-09 00:06:53|INFO | |765 : PICT_numtool.py: Ver v2.1 - Date: 2023:08:17 +01-09 00:07:23|INFO | |765 : PICT_numtool.py: Ver v2.1 - Date: 2023:08:17 +01-09 00:08:08|INFO | |765 : PICT_numtool.py: Ver v2.1 - Date: 2023:08:17 +01-09 00:08:23|INFO | |765 : PICT_numtool.py: Ver v2.1 - Date: 2023:08:17 +01-09 00:09:34|INFO | |765 : PICT_numtool.py: Ver v2.1 - Date: 2023:08:17 +01-09 00:17:02|INFO | |765 : PICT_numtool.py: Ver v2.1 - Date: 2023:08:17 +01-09 00:20:55|INFO | |765 : PICT_numtool.py: Ver v2.1 - Date: 2023:08:17 +01-09 00:21:29|INFO |click_Open |312 : Reading Metadata from: >1980-Fiaccolata-Milano-010.jpeg< +01-09 00:21:29|INFO |click_Open |312 : Reading Metadata from: >1980-Fiaccolata-Milano-020.jpeg< +01-09 00:21:29|INFO |click_Open |312 : Reading Metadata from: >1980-Fiaccolata-Milano-030.jpeg< +01-09 00:21:29|INFO |click_Open |312 : Reading Metadata from: >1980-Fiaccolata-Milano-040.jpeg< +01-09 00:21:29|INFO |click_Open |312 : Reading Metadata from: >1980-Fiaccolata-Milano-050.jpeg< +01-09 00:21:29|INFO |click_Open |312 : Reading Metadata from: >1980-Fiaccolata-Milano-060.jpeg< +01-09 00:21:50|INFO |click_Renumber |542 : Renamed [/Volumes/TIZIANO-SSD1/ZZ-Don-Ambrogio-50esimo-Finale/Selezione Fiaccolate/PICTtool-tmp/PICTtmp-40sjnw37] ==> [/Volumes/TIZIANO-SSD1/ZZ-Don-Ambrogio-50esimo-Finale/Selezione Fiaccolate/1980-Fiaccolata-Milano copy/20250109-TEST-0001.jpeg] +01-09 00:21:50|INFO |click_Renumber |542 : Renamed [/Volumes/TIZIANO-SSD1/ZZ-Don-Ambrogio-50esimo-Finale/Selezione Fiaccolate/PICTtool-tmp/PICTtmp-s25fn5q2] ==> [/Volumes/TIZIANO-SSD1/ZZ-Don-Ambrogio-50esimo-Finale/Selezione Fiaccolate/1980-Fiaccolata-Milano copy/20250109-TEST-0008.jpeg] +01-09 00:21:50|INFO |click_Renumber |542 : Renamed [/Volumes/TIZIANO-SSD1/ZZ-Don-Ambrogio-50esimo-Finale/Selezione Fiaccolate/PICTtool-tmp/PICTtmp-i3o5mggf] ==> [/Volumes/TIZIANO-SSD1/ZZ-Don-Ambrogio-50esimo-Finale/Selezione Fiaccolate/1980-Fiaccolata-Milano copy/20250109-TEST-0015.jpeg] +01-09 00:21:50|INFO |click_Renumber |542 : Renamed [/Volumes/TIZIANO-SSD1/ZZ-Don-Ambrogio-50esimo-Finale/Selezione Fiaccolate/PICTtool-tmp/PICTtmp-02evf12b] ==> [/Volumes/TIZIANO-SSD1/ZZ-Don-Ambrogio-50esimo-Finale/Selezione Fiaccolate/1980-Fiaccolata-Milano copy/20250109-TEST-0022.jpeg] +01-09 00:21:50|INFO |click_Renumber |542 : Renamed [/Volumes/TIZIANO-SSD1/ZZ-Don-Ambrogio-50esimo-Finale/Selezione Fiaccolate/PICTtool-tmp/PICTtmp-1mhyu4eg] ==> [/Volumes/TIZIANO-SSD1/ZZ-Don-Ambrogio-50esimo-Finale/Selezione Fiaccolate/1980-Fiaccolata-Milano copy/20250109-TEST-0029.jpeg] +01-09 00:21:50|INFO |click_Renumber |542 : Renamed [/Volumes/TIZIANO-SSD1/ZZ-Don-Ambrogio-50esimo-Finale/Selezione Fiaccolate/PICTtool-tmp/PICTtmp-x1fdhfij] ==> [/Volumes/TIZIANO-SSD1/ZZ-Don-Ambrogio-50esimo-Finale/Selezione Fiaccolate/1980-Fiaccolata-Milano copy/20250109-TEST-0036.jpeg] +01-09 00:23:20|INFO | |765 : PICT_numtool.py: Ver v2.1 - Date: 2023:08:17 +01-09 00:26:34|INFO | |765 : PICT_numtool.py: Ver v2.1 - Date: 2023:08:17 +01-09 00:27:23|INFO | |765 : PICT_numtool.py: Ver v2.1 - Date: 2023:08:17 +01-09 00:28:20|INFO | |765 : PICT_numtool.py: Ver v2.1 - Date: 2023:08:17 diff --git a/PICT_numtool.py b/PICT_numtool.py index c1ffb09..3d3b80d 100755 --- a/PICT_numtool.py +++ b/PICT_numtool.py @@ -13,6 +13,7 @@ Tiziano Trabattoni | 2023/05/22 | Porting su MacOS per pyqt6 Tiziano Trabattoni | 2023/08/14 | Sorting by Creation date with exiftool Tiziano Trabattoni | 2024/08/19 | Changed movie tags to include filemodification date for .mp4 from Android Tiziano Trabattoni | 2024/12/28 | Introduced possibility to ignore metadata and sort only by filename for Undetermined Metadata (old slides) +Tiziano Trabattoni | 2025/01/08 | Added steps for numbering ####### Developed under poetry (with pyqt6 M1 ARM64 code available) ~/Develop/Personal/PICT_poetry » poetry remove pyqt5 @@ -49,6 +50,10 @@ from collections import OrderedDict ''' PyQT6 imports''' from PICT_numtool_ui import Ui_MainWindow +# from PyQt import QtGui +# from PyQt.QtWidgets import QTreeView, QLabel, QTreeWidgetItem, QMainWindow, QApplication, QFileDialog, QAbstractItemView +# from PyQt.QtGui import QPixmap +from PICT_numtool_ui import Ui_MainWindow from PyQt6 import QtGui from PyQt6.QtWidgets import QTreeView, QLabel, QTreeWidgetItem, QMainWindow, QApplication, QFileDialog, QAbstractItemView from PyQt6.QtGui import QPixmap @@ -98,7 +103,7 @@ BCOL_BLACK = "background-color: black;" ''' Default instances ''' DEF_INSTANCE = 'select' -NON_FILE_CHARACTERS = '\|/*$?:;<>+"''' +NON_FILE_CHARACTERS = '\\|/*$?:;<>+\"\'' ''' Ordering dictionary ''' WorkData = {} @@ -189,7 +194,9 @@ class main(QMainWindow): self.ui.cmb_Month.addItem(str(i)) for i in range(YEARS_RANGE[0],YEARS_RANGE[1]+1): self.ui.cmb_Year.addItem(str(i)) - + for i in range(1,11): + self.ui.cmb_Step.addItem(str(i)) + ''' set date and time ''' # print datetime.now().strftime('%d/%m/%Y') self.day = int(datetime.now().strftime('%d')) @@ -544,7 +551,7 @@ class main(QMainWindow): 'rename' : True }, }) - count += 1 + count += int(self.ui.cmb_Step.currentText()) self.pb.end() @@ -722,7 +729,7 @@ class PB(): actv = self.progressbar.value() newv = actv + self.step if newv <= self.max: - self.progressbar.setValue(newv) + self.progressbar.setValue(int(newv)) '''==========================================================================================================================''' diff --git a/PICT_numtool_241228_save.py b/PICT_numtool_241228_save.py new file mode 100755 index 0000000..c1ffb09 --- /dev/null +++ b/PICT_numtool_241228_save.py @@ -0,0 +1,764 @@ +#!/usr/bin/python +''' +========================================================================================================= +T&T Software and Systems +Project: Renumbering tool +Module: Main module +Description: Tool to renumber ohoto and video replace VB one + + +Tiziano Trabattoni | 2016/12/10 | first version +Tiziano Trabattoni | 2022/07/31 | Porting su MacOS, PyQt5, python3 +Tiziano Trabattoni | 2023/05/22 | Porting su MacOS per pyqt6 +Tiziano Trabattoni | 2023/08/14 | Sorting by Creation date with exiftool +Tiziano Trabattoni | 2024/08/19 | Changed movie tags to include filemodification date for .mp4 from Android +Tiziano Trabattoni | 2024/12/28 | Introduced possibility to ignore metadata and sort only by filename for Undetermined Metadata (old slides) + +####### Developed under poetry (with pyqt6 M1 ARM64 code available) +~/Develop/Personal/PICT_poetry » poetry remove pyqt5 +~/Develop/Personal/PICT_poetry » poetry add pyqt6 + +~/Develop/Personal/PICT_poetry » pip freeze ttrabatt@TTRABATT-M-3QXG +PyQt6==6.5.0 +PyQt6-Qt6==6.5.0 +PyQt6-sip==13.5.1 +(pict-numtool-py3.9) ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +~/Develop/Personal/PICT_poetry » poetry show ttrabatt@TTRABATT-M-3QXG +pyqt6 6.5.0 Python bindings for the Qt cross platform application toolkit +pyqt6-qt6 6.5.0 The subset of a Qt installation needed by PyQt6. +pyqt6-sip 13.5.1 The sip module support for PyQt6 +(pict-numtool-py3.9) ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +~/Develop/Personal/PICT_poetry » + + + +========================================================================================================= +''' +import sys +import os +import shutil +import time +import logging +import threading +import subprocess +import tempfile +import json +from datetime import datetime +from collections import OrderedDict + + +''' PyQT6 imports''' +from PICT_numtool_ui import Ui_MainWindow +from PyQt6 import QtGui +from PyQt6.QtWidgets import QTreeView, QLabel, QTreeWidgetItem, QMainWindow, QApplication, QFileDialog, QAbstractItemView +from PyQt6.QtGui import QPixmap +#from PyQt5.QtCore import * +#from PyQt6 import QtCore +global LOGGER + +''' Module version ''' +mod_ver = "v2.1" +mod_dver = "2023:08:17" + +''' Logger Variables ''' +LOG_FORMAT = '%(asctime)s|%(levelname)-7s|%(funcName)-15s|%(lineno)-5d: %(message)-50s' +LOG_TIME_FORMAT = '%m-%d %H:%M:%S' + +CONSOLE_DEBUG = logging.INFO +FILE_DEBUG = logging.INFO + +PHOTO_filter = 'Images (*.jpg *.jpeg *.png *.tiff *.heic, *.tif)' +MOVIE_filter = 'Movies (*.mov *.mp4)' + +# file format and extension variables +EXIFTOOL = "exiftool" +EXIF_PHOTO_EXT = (".jpg", ".JPG", ".jpeg", ".JPEG", ".png", ".PNG", ".heic", ".HEIC", ".tiff", ".TIFF", ".tif", ".TIF") +EXIF_MOVIE_EXT = (".mov", ".MOV", ".mp4", ".MP4",) +EXIF_PHOTO_TAGS = ("-datetimeoriginal", "-filecreatedate", "-filemodifydate",) +EXIF_MOVIE_TAGS = ("-mediacreatedate","-filemodifydate",) + +FILE_TREE_CNTL = [('Filename.', 300, QTreeView), + ('FullNAme', 800, QtGui.QTextItem), ('--', 20, QLabel)] + +YEARS_RANGE = [1970, 2040] + +COL_WHITE = "QPushButton { color: white;}" +COL_RED = "QPushButton { color: red;}" +COL_GREEN = "QPushButton { color: green;}" +COL_YELLOW = "QPushButton { color: yellow;}" +COL_BLUE = "QPushButton { color: blue;}" +COL_BLACK = "QPushButton { color: black;}" + +BCOL_WHITE = "background-color: white;" +BCOL_RED = "background-color: red;" +BCOL_GREEN = "background-color: green;" +BCOL_YELLOW = "background-color: yellow;" +BCOL_CYAN = "background-color: cyan;" +BCOL_BLACK = "background-color: black;" + +''' Default instances ''' +DEF_INSTANCE = 'select' +NON_FILE_CHARACTERS = '\|/*$?:;<>+"''' + +''' Ordering dictionary ''' +WorkData = {} +WorkDir = '' +TmpDir = '' + +class PICTitem(QTreeWidgetItem): + ''' + Custom QTreeWidgetItem with Widgets + ''' + _basename = "" + _name = "" + + @property + def basename(self): + return self._basename + + @property + def name(self): + return self._name + + def __init__(self, parent, basename, name): + ''' + parent (QTreeWidget) : Item's QTreeWidget parent. + value (str) : Item's name. just an example. + ''' + + ## Init super class ( QtGui.QTreeWidgetItem ) + super(PICTitem, self).__init__(parent) + + ''' ''' + self.setText(0, basename) + #self.setTextAlignment(0, QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) + self.setText(1, name) + #self.setTextAlignment(1, QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) + self._name=name + self._basename=basename + + def __repr__(self) -> str: + return str(self) + + def __str__(self): + return f" Filename:{self.basename}" + + +''' ======================== MAIN WINDOW CLASS with all methods ================================ ''' +class main(QMainWindow): + def __init__(self): + QMainWindow.__init__(self) + self.ui = Ui_MainWindow() + self.ui.setupUi(self) + + ''' create Progressbar element ''' + self.pb = PB(self.ui.pb_Run,mi = 0, ma =100) + + ''' set default values ''' + self.day = 0 + self.month = 0 + self.year = 1900 + self.name = '' + self.fullName = '' + self.ineditname = False + self.filecriteria = PHOTO_filter + self.TmpDir = '' + self.WorkDir = '' + self.WorkData = {} + self.toRename = [] + + + ''' set the tree_OUT widget''' + self.ui.tree_OUT.setColumnCount(len(FILE_TREE_CNTL)) + self.ui.tree_OUT.setHeaderLabels(self.set_qtree_headers(FILE_TREE_CNTL)) + self.ui.tree_OUT.setUniformRowHeights(True) + #self.ui.tree_OUT.setSelectionMode(QabsW.ExtendedSelection) + + ''' set all Methods ''' + self.ui.btn_Renumber.setEnabled(False) + self.ui.btn_Renumber.clicked.connect(self.click_Renumber) + self.ui.btn_Open.clicked.connect(self.click_Open) + self.ui.btn_Exit.clicked.connect(self.click_Exit) + #self.ui.txt_Name.editingFinished.connect(self.edited_Name) + self.ui.rbtn_Photo.setChecked(True) + + ''' set all combo ''' + for i in range(1,32): + self.ui.cmb_Day.addItem(str(i)) + for i in range(1,13): + self.ui.cmb_Month.addItem(str(i)) + for i in range(YEARS_RANGE[0],YEARS_RANGE[1]+1): + self.ui.cmb_Year.addItem(str(i)) + + ''' set date and time ''' + # print datetime.now().strftime('%d/%m/%Y') + self.day = int(datetime.now().strftime('%d')) + self.month = int(datetime.now().strftime('%m')) + self.year = int(datetime.now().strftime('%Y')) + self.ui.cmb_Day.setCurrentIndex(self.day - 1) + self.ui.cmb_Month.setCurrentIndex(self.month - 1) + self.ui.cmb_Year.setCurrentIndex(self.year - YEARS_RANGE[0]) + self.ui.date_Day.setDate(datetime.now()) + self.ui.txt_Name.textChanged.connect(self.changed_Name) + + ######## These are OLD PyQT4 signal sintax ########### + #''' set Object signals ''' + #''' visualization selection via Radio Button ''' + #self.ui.rbtn_Movies.connect(self.ui.rbtn_Movies, QtCore.SIGNAL("clicked()"),self.selected_rbtn_Movies) + #self.ui.rbtn_Photo.connect(self.ui.rbtn_Photo, QtCore.SIGNAL("clicked()"), self.selected_rbtn_Photos) + #self.ui.date_Day.connect(self.ui.date_Day, QtCore.SIGNAL("dateChanged(QDate)"), self.change_date) +# + #''' Signals ''' + #self.ui.cmb_Day.connect(self.ui.cmb_Day, QtCore.SIGNAL("currentIndexChanged(const QString&)"), self.choose_Day) + #self.ui.cmb_Month.connect(self.ui.cmb_Month, QtCore.SIGNAL("currentIndexChanged(const QString&)"), self.choose_Month) + #self.ui.cmb_Year.connect(self.ui.cmb_Year, QtCore.SIGNAL("currentIndexChanged(const QString&)"), self.choose_Year) + ## self.ui.txt_Name.connect(self.ui.txt_Name, QtCore.SIGNAL("insertPlainText (const Qstring&"), self.changed_Name) + + ''' set Object signals ''' + ''' visualization selection via Radio Button ''' + self.ui.rbtn_Movies.clicked.connect(self.selected_rbtn_Movies) + self.ui.rbtn_Photo.clicked.connect(self.selected_rbtn_Photos) + self.ui.date_Day.dateChanged.connect(self.change_date) + self.ui.tree_OUT.itemSelectionChanged.connect(self.on_selectionChanged) + + ''' Signals ''' + self.ui.cmb_Day.currentIndexChanged.connect(self.choose_Day) + self.ui.cmb_Month.currentIndexChanged.connect(self.choose_Month) + self.ui.cmb_Year.currentIndexChanged.connect(self.choose_Year) + + ''' other setting ''' + self.ui.pb_Run.setMinimum(0) + self.ui.pb_Run.setMaximum(100) + self.ui.pb_Run.setVisible(False) + self.ui.chk_IgnMeta.setChecked(False) + + def click_Exit(self): + sys.exit(0) + return + + def QaddFILEChild(self, parent, basename, name): + item = PICTitem(parent, basename, name) + return item + + + def on_selectionChanged(self): + selected = [ s.basename for s in self.ui.tree_OUT.selectedItems() ] + for f in self.WorkData: + if f in selected: + self.WorkData[f]['rename'] = True + else: + self.WorkData[f]['rename'] = False + return + + + def click_Open(self): + ''' open source directory ''' + #files = QFileDialog.getOpenFileNames(self, "Open Directory", "" , self.filecriteria, None, QFileDialog.DontUseNativeDialog) + files = QFileDialog.getOpenFileNames(self, "Open Directory", "" , self.filecriteria, None) + nf = len(files) + # SHIT now QfileDialog return a Tuple instead of list + # Has to be addressed as [0] + # because files[1] contains the Type: , - Value >Images (*.jpg *.jpeg *.png *.tiff) + + + if len(files[0]) > 0: + ''' clear and set the tree_OUT widget''' + self.ui.tree_OUT.clear() + self.ui.tree_OUT.setColumnCount(len(FILE_TREE_CNTL)) + self.ui.tree_OUT.setHeaderLabels(self.set_qtree_headers(FILE_TREE_CNTL)) + self.ui.tree_OUT.setUniformRowHeights(True) + + self.pb.start(steps=nf) + + ''' initialized work list dictionary ''' + self.WorkData = {} + + # LOGGER.debug("Type of files[] >%s<" % (str(type(files)))) + # self.WorkDir = os.path.dirname(str(files[0])) + # SHIT now QfileDialog return a Tuple instead of list + # Has to be addressed as [0] + # because files[1] contains the Type: , - Value >Images (*.jpg *.jpeg *.png *.tiff) + fileuno = files[0][0] + #LOGGER.debug("fileuno >%s<" % fileuno) + + self.WorkDir = os.path.dirname(fileuno) + LOGGER.debug(f"Workdir >{self.WorkDir}<") + + ''' tempory dir must be on same disk otherwise is too slow''' + basedir = os.path.dirname(self.WorkDir) + self.TmpDir = basedir + '/PICTtool-tmp' + if not os.path.exists(self.TmpDir): + os.mkdir(self.TmpDir) + count = 1 + + #for fname in files[0]: + # LOGGER.debug("Type: %s, - Value >%s<" % (type(fname), fname)) + + ''' for all selected files ''' + for fname in files[0]: + self.pb.incr() + self.repaint() + ''' add an item to Qtree including resizing of pictures ''' + # self.add_Qtree_item(fname) + + basename = os.path.basename(str(fname)) + LOGGER.info(f"Reading Metadata from: >{basename}< ") + + # Get File Metadata via external exiftool + # requires installation + # https://exiftool.org/index.html#running + # + + if self.ui.chk_IgnMeta: + # dont look at meta data of file just sort for name + ''' fill dictionary ''' + self.WorkData.update({ + # int(time.mktime(time.strptime(dt, '%Y:%m:%d|%H:%M:%S'))) : + basename : + { + #'itemNum' : str(count), + 'fname' : basename, + 'fileExt' : self.get_extension(str(fname)), + 'tmpName' : self.TmpDir + '/PICTtmp-' + next(tempfile._get_candidate_names()), + 'rename' : True, + 'date' : "", + 'time' : "" + } + }) + count += 1 + + + else: + # Normal way looking at Metadata to get file sosted with creation time + + # exiftool has produced with json and TAGS + if basename.endswith(EXIF_MOVIE_EXT): + EXIF_TAGS = EXIF_MOVIE_TAGS + else: + EXIF_TAGS = EXIF_PHOTO_TAGS + + args=[EXIFTOOL, "-json", *EXIF_TAGS, os.path.join(self.WorkDir,basename)] + LOGGER.debug(str(args)) + process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + res=json.loads(process.stdout.read()) + LOGGER.debug(json.dumps(res,indent=2)) + dat = res[0] + + # reset of all tag values + dt = odate = fmdate = fcdate = mcdate = None + if basename.endswith(EXIF_MOVIE_EXT): + # MOVIEs + try: + fmdate = dat['FileModifyDate'] + except KeyError: + LOGGER.debug(f"File {basename} does not contain tag FileModifyDate") + except Exception as e: + LOGGER.error(f"File {basename} parsing error {e}") + continue # for fname in files[0]: + + try: + mcdate = dat['MediaCreateDate'] + except KeyError: + LOGGER.debug(f"File {basename} does not contain tag MediaCreateDate") + except Exception as e: + LOGGER.error(f"File {basename} parsing error {e}") + continue + + if not mcdate and not fmdate: + LOGGER.error(f"File {basename} does NOT contain expected metadata tags, please verify results") + continue # for fname in files[0]: + + + if fmdate: + try: + # dt = f"{mcdate[0]}:{mcdate[1]}:{mcdate[2].split(' ')[0]}|{mcdate[2].split(' ')[1]}:{mcdate[3]}:{(mcdate[4].split('+'))[0]}".strip() + pd = int(time.mktime(time.strptime(fmdate, '%Y:%m:%d %H:%M:%S%z'))) + pod = datetime.fromtimestamp(pd) + dt = pod.strftime('%Y:%m:%d|%H:%M:%S') + except Exception as e: + LOGGER.error(f"File {basename} parsing tag value: {e} ...Skipping...") + continue # for fname in files[0]: + else: + try: + # dt = f"{mcdate[0]}:{mcdate[1]}:{mcdate[2].split(' ')[0]}|{mcdate[2].split(' ')[1]}:{mcdate[3]}:{(mcdate[4].split('+'))[0]}".strip() + pd = int(time.mktime(time.strptime(mcdate, '%Y:%m:%d %H:%M:%S'))) + pod = datetime.fromtimestamp(pd) + dt = pod.strftime('%Y:%m:%d|%H:%M:%S') + except Exception as e: + LOGGER.error(f"File {basename} parsing tag value: {e} ...Skipping...") + continue # for fname in files[0]: + else: + # PICTUREs + try: + odate = dat['DateTimeOriginal'] + except KeyError: + LOGGER.debug(f"File {basename} does not contain tag DateTimeOriginal") + except Exception as e: + LOGGER.error(f"File {basename} parsing error {e}") + continue # for fname in files[0]: + + try: + fcdate = dat['FileCreateDate'] + except KeyError: + LOGGER.debug(f"File {basename} does not contain tag FileCreateDate") + except Exception as e: + LOGGER.error(f"File {basename} parsing error {e}") + continue # for fname in files[0]: + + try: + fmdate = dat['FileModifyDate'] + except KeyError: + LOGGER.debug(f"File {basename} does not contain tag FileModifyDate") + except Exception as e: + LOGGER.error(f"File {basename} parsing error {e}") + continue # for fname in files[0]: + + if not odate and not fcdate and not fmdate: + LOGGER.error(f"File {basename} does NOT contain expected metadata tags, please verify results") + continue # for fname in files[0]: + + if odate: + #take DateTimeOriginal + od = int(time.mktime(time.strptime(odate, '%Y:%m:%d %H:%M:%S'))) + ood = datetime.fromtimestamp(od) + dt = ood.strftime('%Y:%m:%d|%H:%M:%S') + elif fcdate and fmdate: + # take older between FileCreateDate and FileModifyDate + fcfmdate = min(int(time.mktime(time.strptime(fcdate, '%Y:%m:%d %H:%M:%S%z'))), int(time.mktime(time.strptime(fmdate, '%Y:%m:%d %H:%M:%S%z')))) + ofcfmdate = datetime.fromtimestamp(fcfmdate) + dt = ofcfmdate.strftime('%Y:%m:%d|%H:%M:%S') + elif fcdate and not fmdate: + # take fcdate + fc = int(time.mktime(time.strptime(fcdate, '%Y:%m:%d %H:%M:%S%z'))) + dt = fc.strftime('%Y:%m:%d|%H:%M:%S') + else: + # take fmdate + fm = int(time.mktime(time.strptime(fmdate, '%Y:%m:%d %H:%M:%S%z'))) + dt = fm.strftime('%Y:%m:%d|%H:%M:%S') + + if not dt: + LOGGER.error(f"File {basename} does NOT contain expected metadata tags, please verify results") + continue # for fname in files[0]: + + # check if same modification time exist so it loops adding one second to modification time + adt = int(time.mktime(time.strptime(dt, '%Y:%m:%d|%H:%M:%S'))) + + while adt in self.WorkData.keys(): + adt+=1 + + LOGGER.info(f"Time: {dt} --> {basename}") + + ''' fill dictionary ''' + self.WorkData.update({ + # int(time.mktime(time.strptime(dt, '%Y:%m:%d|%H:%M:%S'))) : + adt : + { + #'itemNum' : str(count), + 'fname' : basename, + 'fileExt' : self.get_extension(str(fname)), + 'tmpName' : self.TmpDir + '/PICTtmp-' + next(tempfile._get_candidate_names()), + 'rename' : True, + 'date': dt.split('|')[0], + 'time': dt.split('|')[1] + } + }) + count += 1 + # end of sorting with meta data + self.pb.end() + # print unsorted dictionary + # print(f"{json.dumps(self.WorkData, indent=4)}") + + # transpose disctionary and sort it by date of creation of the file + flts = dict(sorted(self.WorkData.items())) + # self.WorkData = flts + + # print(f"{json.dumps(self.WorkData, indent=4)}") + + for _,v in self.WorkData.items(): + ''' add an item to Qtree including resizing of pictures ''' + # self.add_Qtree_item(f"{v['fname']}-->{v['date']}|{v['time']}") + self.add_Qtree_item(f"{v['fname']}") + + return + + def click_Renumber(self): + ''' renumber and create a new WorkData for next phases ''' + newWorkData = {} + + ''' only if WorkData Contains elements ''' + nf = len(self.WorkData) + if nf > 0: + self.pb.start(steps=nf * 2) + + ''' for all files move to temp to avoid overlapping''' + for _,v in self.WorkData.items(): + LOGGER.debug(f"Moving file {v}") + if v['rename']: + self.pb.incr() + try: + tmpfile = v['tmpName'] + origfile = self.WorkDir + '/' + v['fname'] + except Exception as e: + LOGGER.error(f"Parsing file attributes [{v['fname']} : {e} ... Skipping ...") + else: + try: + shutil.move(origfile, tmpfile) + #os.rename(origfile, tmpfile) + except Exception as e: + LOGGER.error(f"Renaming file [{origfile}] to tmpfile: {tmpfile} : {e} ... Skipping ...") + else: + # LOGGER.debug(f"Origfile [{origfile}] ==> TmpFile [{tmpfile}]') + pass + + ''' for all files move to current directory with new name''' + count = 1 + for _,v in self.WorkData.items(): + LOGGER.debug(f"Moving file {v}") + if v['rename']: + self.pb.incr() + try: + tmpfile = v['tmpName'] + extension = v['fileExt'] + #count = int(self.WorkData[fname]['itemNum']) + except Exception as e: + LOGGER.error(f"Parsing file attributes [{v['fname']} : {e} ... Skipping ...") + else: + targetfile = self.WorkDir + '/' + self.fullName + '-{0:04d}'.format(count) + extension + targetfname = self.fullName + '-{0:04d}'.format(count) + extension + try: + shutil.move(tmpfile, targetfile) + #os.rename(tmpfile, targetfile) + except Exception as e: + LOGGER.error(f"Renaming tmpfile [{tmpfile}] to: {targetfname} : {e} ... Skipping ...") + else: + #LOGGER.debug('TmpFile [%s] ==> TargetFIle [%s]' % (tmpfile, targetfile)) + LOGGER.info(f"Renamed [{tmpfile}] ==> [{targetfile}]") + pass + + ''' fill dictionary ''' + newWorkData.update({ + targetfname : { + 'itemNum' : str(count), + 'fileExt' : self.get_extension(str(targetfname)), + 'tmpName' : self.TmpDir + '/PICTtmp-' + next(tempfile._get_candidate_names()), + 'rename' : True + }, + }) + count += 1 + + self.pb.end() + + ''' also rename directory ''' + basedir = os.path.dirname(self.WorkDir) + olddir = self.WorkDir + newdir = basedir + '/' + self.fullName + try: + shutil.move(olddir, newdir) + except Exception as e: + LOGGER.error(f"Rename of directory to [{basedir}] failed {e} ...Skipping...") + + ''' sel new Current working directory and Workdata to new dictionary ''' + self.WorkDir = newdir + os.chdir(self.WorkDir) + self.WorkData = newWorkData + + ''' empties Qtree ''' + self.ui.tree_OUT.clear() + self.ui.tree_OUT.setColumnCount(len(FILE_TREE_CNTL)) + self.ui.tree_OUT.setHeaderLabels(self.set_qtree_headers(FILE_TREE_CNTL)) + self.ui.tree_OUT.setUniformRowHeights(True) + + ''' and refill it again ''' + nf = len (self.WorkData) + WorkData_s = OrderedDict(self.WorkData) + + # redraw the dialog + self.pb.start(steps=nf) + for fname in WorkData_s: + self.pb.incr() + self.add_Qtree_item(self.WorkDir + '/' + fname) + + self.pb.end() + + return + + def add_Qtree_item(self, fname): + basefile = os.path.basename(str(fname)) + + if self.ui.chk_Preview.isChecked(): + ''' review is activated need to elaborate thumbnail ''' + item = self.QaddFILEChild(self.ui.tree_OUT, basefile, str(fname)) + try: + ''' rezize file ''' + #img = QtGui.QImage(fname) + # qim = img.scaled(64, 64, QtCore.Qt.KeepAspectRatioByExpanding, QtCore.Qt.SmoothTransformation) + # qim = img.scaled(64, 64) + # pict = QPixmap.fromImage(qim) + pict = QPixmap(fname) + except Exception as e: + LOGGER.error(f"Unable to process image [{fname}] {e} ... Skipping ...") + label = QLabel() + label.setText("Preview Error") + else: + ''' create a Label with picture as background ''' + label = QLabel() + label.setPixmap(pict) + + ''' add column 2 a Label with picture in pixmap ''' + item.treeWidget().setItemWidget(item, 2, label) + else: + item = self.QaddFILEChild(self.ui.tree_OUT, basefile, str(fname)) + return item + + def get_extension(self, fname): + basename = os.path.basename(fname) # os independent + ext = '.'.join(basename.split('.')[1:]) + return '.' + ext if ext else None + + def change_date(self): + self.day = self.ui.date_Day.date().day() + self.month = self.ui.date_Day.date().month() + self.year = self.ui.date_Day.date().year() + self.ui.cmb_Day.setCurrentIndex(self.day - 1) + self.ui.cmb_Month.setCurrentIndex(self.month - 1) + self.ui.cmb_Year.setCurrentIndex(self.year - YEARS_RANGE[0] ) + + ''' set fullname ''' + self.set_fullName() + + return + + def changed_Name(self): + if self.ineditname: + return + else: + tc = self.ui.txt_Name.textCursor() + name = str(self.ui.txt_Name.toPlainText()) + filename = "".join(i for i in name if i not in NON_FILE_CHARACTERS) + self.ineditname = True + self.ui.txt_Name.setPlainText(filename) + self.ui.txt_Name.setTextCursor(tc) + self.ineditname = False + self.name = filename + self.set_fullName() + return + + def selected_rbtn_Movies(self): + self.filecriteria = MOVIE_filter + return + + def selected_rbtn_Photos(self): + self.filecriteria = PHOTO_filter + return + + def choose_Day(self): + self.day = int(self.ui.cmb_Day.currentText()) + self.set_fullName() + return + + def choose_Month(self): + self.month = int(self.ui.cmb_Month.currentText()) + self.set_fullName() + return + + def choose_Year(self): + self.year = int(self.ui.cmb_Year.currentText()) + self.set_fullName() + return + + def set_qtree_headers(self, tree_ctnl): + tree_HEADERS = [] + i = 0 + for treeitem in tree_ctnl: + tree_HEADERS.append(treeitem[0]) + self.ui.tree_OUT.setColumnWidth(i, treeitem[1]) + i += 1 + return tree_HEADERS + + def set_fullName(self): + self.fullName = '{0:4d}{1:02d}{2:02d}-{3:s}'.format(self.year, self.month, self.day, self.name) + self.ui.txt_FullName.setText(self.fullName) + if len(self.fullName) > 9: + self.ui.btn_Renumber.setEnabled(True) + else: + self.ui.btn_Renumber.setEnabled(False) + + # def msg_print(self, msg): + # self.ui.txt_Error.setText(str(msg)) + # self.repaint() + +''' ========================= Other Functions ===================================================''' +''' progress bar class and methods ''' +class PB(): + def __init__(self, progressbar, mi = 0, ma = 100 ): + # super(PB, self).__init__(parent) + self.val = 0 + self.min = mi + self.max = ma + self.steps = 10 + self.step = (self.max - self.min)/self.steps + self.progressbar = progressbar + self.progressbar.setMinimum(self.min) + self.progressbar.setMaximum(self.max) + + def start(self, steps=10): + self.steps = (steps if steps > 0 else 10) + self.progressbar.setVisible(True) + self.progressbar.setValue(0) + + def end(self): + self.progressbar.setValue(self.max) + # start a timer to make progressbar invisible + threading.Timer(3,self.clear).start() + + def clear(self): + self.progressbar.setVisible(False) + + def upd(self, val): + self.val = val + self.progressbar.setValue(self.val) + + def incr(self): + actv = self.progressbar.value() + newv = actv + self.step + if newv <= self.max: + self.progressbar.setValue(newv) + + +'''==========================================================================================================================''' +''' Main Program ''' +if __name__ == '__main__': + + ''' Enabling Logger + ''' + prog_name = os.path.basename(sys.argv[0:][0]) + LOGGER = logging.getLogger(__name__) + LOGGER.setLevel(logging.DEBUG) + LOGGER.propagate = False + logging.captureWarnings(True) + formatter = logging.Formatter(LOG_FORMAT, LOG_TIME_FORMAT) + + ''' File logging + ''' + log_name = "LOG-" + prog_name + ".log" + fh = logging.FileHandler(log_name) + fh.setLevel(FILE_DEBUG) + fh.setFormatter(formatter) + LOGGER.addHandler(fh) + + ''' Console logging + ''' + cl = logging.StreamHandler(sys.stdout) + cl.setLevel(CONSOLE_DEBUG) + cl.setFormatter(formatter) + LOGGER.addHandler(cl) + + time.sleep(1) + + LOGGER.info("%s: Ver %s - Date: %s" % (prog_name, mod_ver, mod_dver)) + + ''' Open window and give control to QTPY ''' + app = QApplication(sys.argv) + window = main() + window.show() + sys.exit(app.exec()) diff --git a/PICT_numtool_ui.py b/PICT_numtool_ui.py index 58f8578..b1e5aaa 100755 --- a/PICT_numtool_ui.py +++ b/PICT_numtool_ui.py @@ -201,13 +201,22 @@ class Ui_MainWindow(object): sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.chk_Preview.sizePolicy().hasHeightForWidth()) self.chk_Preview.setSizePolicy(sizePolicy) - self.chk_Preview.setMinimumSize(QtCore.QSize(150, 20)) + self.chk_Preview.setMinimumSize(QtCore.QSize(110, 20)) self.chk_Preview.setObjectName("chk_Preview") self.horizontalLayout.addWidget(self.chk_Preview) self.btn_Open = QtWidgets.QPushButton(parent=self.wdg_Main) self.btn_Open.setMinimumSize(QtCore.QSize(100, 30)) self.btn_Open.setObjectName("btn_Open") self.horizontalLayout.addWidget(self.btn_Open) + self.lbl_Step = QtWidgets.QLabel(parent=self.wdg_Main) + self.lbl_Step.setMaximumSize(QtCore.QSize(120, 16777215)) + self.lbl_Step.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) + self.lbl_Step.setObjectName("lbl_Step") + self.horizontalLayout.addWidget(self.lbl_Step) + self.cmb_Step = QtWidgets.QComboBox(parent=self.wdg_Main) + self.cmb_Step.setMaximumSize(QtCore.QSize(60, 16777215)) + self.cmb_Step.setObjectName("cmb_Step") + self.horizontalLayout.addWidget(self.cmb_Step) self.btn_Renumber = QtWidgets.QPushButton(parent=self.wdg_Main) self.btn_Renumber.setMinimumSize(QtCore.QSize(100, 0)) self.btn_Renumber.setFocusPolicy(QtCore.Qt.FocusPolicy.ClickFocus) @@ -259,5 +268,6 @@ class Ui_MainWindow(object): self.chk_IgnMeta.setText(_translate("MainWindow", "Ignore Metadata")) self.chk_Preview.setText(_translate("MainWindow", "Show Preview")) self.btn_Open.setText(_translate("MainWindow", "Open Folder")) + self.lbl_Step.setText(_translate("MainWindow", "Numbering Steps")) self.btn_Renumber.setText(_translate("MainWindow", "Renumber Photo and Movies")) self.btn_Exit.setText(_translate("MainWindow", "Exit")) diff --git a/PICT_numtool_ui.ui b/PICT_numtool_ui.ui index 27877e7..5090e61 100755 --- a/PICT_numtool_ui.ui +++ b/PICT_numtool_ui.ui @@ -429,7 +429,7 @@ - 150 + 110 20 @@ -451,6 +451,32 @@ + + + + + 120 + 16777215 + + + + Numbering Steps + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 60 + 16777215 + + + + diff --git a/PICT_numtool_ui_ui.py b/PICT_numtool_ui_ui.py index cb5675a..27199c7 100644 --- a/PICT_numtool_ui_ui.py +++ b/PICT_numtool_ui_ui.py @@ -1,4 +1,4 @@ -# Form implementation generated from reading ui file '/Users/Tiziano/Library/CloudStorage/OneDrive-Personal/Documents/Develop/Develop-Personal/PICT_poetry_devel/PICT_numtool_ui.ui' +# Form implementation generated from reading ui file '/Users/Tiziano/Library/CloudStorage/OneDrive-Personal/Documents/Develop/Develop-Personal/PICT_numtool_NG/PICT_numtool_ui.ui' # # Created by: PyQt6 UI code generator 6.7.1 # @@ -201,13 +201,22 @@ class Ui_MainWindow(object): sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.chk_Preview.sizePolicy().hasHeightForWidth()) self.chk_Preview.setSizePolicy(sizePolicy) - self.chk_Preview.setMinimumSize(QtCore.QSize(150, 20)) + self.chk_Preview.setMinimumSize(QtCore.QSize(110, 20)) self.chk_Preview.setObjectName("chk_Preview") self.horizontalLayout.addWidget(self.chk_Preview) self.btn_Open = QtWidgets.QPushButton(parent=self.wdg_Main) self.btn_Open.setMinimumSize(QtCore.QSize(100, 30)) self.btn_Open.setObjectName("btn_Open") self.horizontalLayout.addWidget(self.btn_Open) + self.lbl_Step = QtWidgets.QLabel(parent=self.wdg_Main) + self.lbl_Step.setMaximumSize(QtCore.QSize(120, 16777215)) + self.lbl_Step.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) + self.lbl_Step.setObjectName("lbl_Step") + self.horizontalLayout.addWidget(self.lbl_Step) + self.cmb_Step = QtWidgets.QComboBox(parent=self.wdg_Main) + self.cmb_Step.setMaximumSize(QtCore.QSize(60, 16777215)) + self.cmb_Step.setObjectName("cmb_Step") + self.horizontalLayout.addWidget(self.cmb_Step) self.btn_Renumber = QtWidgets.QPushButton(parent=self.wdg_Main) self.btn_Renumber.setMinimumSize(QtCore.QSize(100, 0)) self.btn_Renumber.setFocusPolicy(QtCore.Qt.FocusPolicy.ClickFocus) @@ -259,5 +268,6 @@ class Ui_MainWindow(object): self.chk_IgnMeta.setText(_translate("MainWindow", "Ignore Metadata")) self.chk_Preview.setText(_translate("MainWindow", "Show Preview")) self.btn_Open.setText(_translate("MainWindow", "Open Folder")) + self.lbl_Step.setText(_translate("MainWindow", "Numbering Steps")) self.btn_Renumber.setText(_translate("MainWindow", "Renumber Photo and Movies")) self.btn_Exit.setText(_translate("MainWindow", "Exit")) diff --git a/__pycache__/PICT_numtool_ui.cpython-313.pyc b/__pycache__/PICT_numtool_ui.cpython-313.pyc new file mode 100644 index 0000000..e781be8 Binary files /dev/null and b/__pycache__/PICT_numtool_ui.cpython-313.pyc differ diff --git a/go b/go new file mode 100755 index 0000000..51f0e46 --- /dev/null +++ b/go @@ -0,0 +1,2 @@ +python3 ./PICT_numtool.py +