765 lines
32 KiB
Python
Executable File
765 lines
32 KiB
Python
Executable File
#!/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"<class '{self.__class__.__name__}'> 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: <class 'str'>, - 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: <class 'str'>, - 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())
|