#!/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 ========================================================================================================= Usefull link: http://www.freeformatter.com/json-formatter.html#ad-output ''' from collections import OrderedDict import sys import os import shutil import time import logging import threading from datetime import datetime import tempfile import json ''' PyQT4 imports''' from PICT_numtool_ui import Ui_MainWindow from PyQt6.QtWidgets import QTreeView, QLabel, QTreeWidgetItem, QMainWindow, QApplication, QFileDialog, QAbstractItemView as QabsW from PyQt6 import QtGui #from PyQt5.QtCore import * from PyQt6 import QtCore ''' Module version ''' mod_ver = "v1.1" mod_dver = "2022:07:31" ''' Logger Variables ''' LOG_FORMAT = '%(asctime)s|%(levelname)-7s|%(funcName)-15s|%(lineno)-5d: %(message)-50s' LOG_TIME_FORMAT = '%m-%d %H:%M:%S' PHOTO_filter = 'Images (*.jpg *.jpeg *.png *.tiff *.heic)' MOVIE_filter = 'Movies (*.mov *.avi *.mp4 *.wmk)' FILE_TREE_CNTL = [('Filename.', 300, QTreeView), ('FullNAme', 600, QtGui.QTextItem), ('Preview', 100, 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) 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) 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("Workdir >%s<" % 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() ''' add an item to Qtree including resizing of pictures ''' self.add_Qtree_item(fname) basename = os.path.basename(str(fname)) ''' fill dictionary ''' self.WorkData.update({ basename : { 'itemNum' : str(count), 'fileExt' : self.get_extension(str(fname)), 'tmpName' : self.TmpDir + '/PICTtmp-' + next(tempfile._get_candidate_names()), 'rename' : True }, }) count += 1 self.pb.end() 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) # WorkDataKL = self.WorkData.keys() # WorkDataKL.sort() WorkData_s = OrderedDict(self.WorkData) #print(json.dumps(WorkData_s, indent=2)) #WorkData_s = {k: self.WorkData[k] for k in sorted(self.WorkData)} #for fname in WorkData_s: # LOGGER.debug("fname: >%s<" % fname) ''' for all files move to temp to avoid overlapping''' for fname in WorkData_s: if WorkData_s[fname]['rename']: self.pb.incr() try: tmpfile = self.WorkData[fname]['tmpName'] origfile = self.WorkDir + '/' + fname except: LOGGER.error('Parsing file attribites [%s] ... Skipping ...' % (fname)) else: try: shutil.move(origfile, tmpfile) #os.rename(origfile, tmpfile) except: LOGGER.error('Renaming file [%s] to: %s ... Skipping ...' % (origfile, tmpfile)) else: # LOGGER.debug('Origfile [%s] ==> TmpFile [%s]' % (origfile, tmpfile)) pass ''' for all files move to current directory with new name''' count = 1 for fname in WorkData_s: if WorkData_s[fname]['rename']: self.pb.incr() try: tmpfile = self.WorkData[fname]['tmpName'] extension = self.WorkData[fname]['fileExt'] count = int(self.WorkData[fname]['itemNum']) except: LOGGER.error('Parsing file attributes [%s] ... Skipping ...' % (fname)) 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: LOGGER.error('Renaming file [%s] to: %s ... Skipping ...' % (tmpfile , targetfile)) else: #LOGGER.debug('TmpFile [%s] ==> TargetFIle [%s]' % (tmpfile, targetfile)) LOGGER.info('Renanme [%s] ==> [%s]' % (fname, 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: LOGGER.error('Renanme of directory to [%s] failed ...Skipping...' % (basedir)) ''' 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) # WorkDataKL = self.WorkData.keys() # WorkDataKL.sort() # WorkData_s = {k: self.WorkData[k] for k in sorted(self.WorkData)} WorkData_s = OrderedDict(self.WorkData) 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) pict = QPixmap.fromImage(qim) except: LOGGER.error('Unable to process image [%s] ... Skipping ...' % (fname)) 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__': global LOGGER ''' 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(logging.DEBUG) fh.setFormatter(formatter) LOGGER.addHandler(fh) ''' Console logging ''' cl = logging.StreamHandler(sys.stdout) cl.setLevel(logging.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_())