addDateTaken ist pythonisiert
This commit is contained in:
parent
6d78c328c6
commit
077f490687
3 changed files with 391 additions and 2 deletions
|
@ -1,7 +1,7 @@
|
||||||
# Felix' Internetauftritt
|
# Felix' Internetauftritt
|
||||||
|
|
||||||
* [Tricks und Anleitungen, gesammelt, von Felix; hauptsächlich für Linux](Tricks.md)
|
* [Tricks und Anleitungen, gesammelt, von Felix; hauptsächlich für Linux](Tricks.md)
|
||||||
* [ein Shell-Skript zum umbenennen von Bildern, wobei das Datum als Dateiname genutzt wird](addDateTaken.sh)
|
* [ein Python-Skript zum umbenennen von Bildern, wobei das Datum als Dateiname genutzt wird](addDateTaken.py)
|
||||||
* [ein Python-Skript zum Heraussuchen von den Beschreibungen von OSM-Notes aus .osn-Dateien, siehe auch "Tricks und Anleitungen"](extract-hinweise.py)
|
* [ein Python-Skript zum Heraussuchen von den Beschreibungen von OSM-Notes aus .osn-Dateien, siehe auch "Tricks und Anleitungen"](extract-hinweise.py)
|
||||||
* [ein Shell-Skript zum Finden und auflisten aller symlinks in einem Ordner inklusive verlinktem Ort](listLinks.sh)
|
* [ein Shell-Skript zum Finden und auflisten aller symlinks in einem Ordner inklusive verlinktem Ort](listLinks.sh)
|
||||||
* [ein Shell-Skript zum Automatischen Umbenennen von Dateien (entfernen von nervigen Sonderzeichen)](removeBadSymbols.sh)
|
* [ein Shell-Skript zum Automatischen Umbenennen von Dateien (entfernen von nervigen Sonderzeichen)](removeBadSymbols.sh)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
[Für deutsche Version hier klicken.](README-de.md)
|
[Für deutsche Version hier klicken.](README-de.md)
|
||||||
|
|
||||||
* [Tricks and tutorials, collected by Felix, mainly for Linux (german)](Tricks.md)
|
* [Tricks and tutorials, collected by Felix, mainly for Linux (german)](Tricks.md)
|
||||||
* [`.sh`-script for renaming pictures. Use date for filename.](addDateTaken.sh)
|
* [python-script for renaming pictures. Use date for filename.](addDateTaken.py)
|
||||||
* [Python script for getting descriptions of OSM-notes from `.osn`-files. Also see "Tricks and tutorials"](extract-hinweise.py)
|
* [Python script for getting descriptions of OSM-notes from `.osn`-files. Also see "Tricks and tutorials"](extract-hinweise.py)
|
||||||
* [`.sh`-script for finding and listing all symlinks inkluding the linked paths](listLinks.sh)
|
* [`.sh`-script for finding and listing all symlinks inkluding the linked paths](listLinks.sh)
|
||||||
* [python script for automatic renaming of files for removal of annoying symbols](removeBadSymbols.py)
|
* [python script for automatic renaming of files for removal of annoying symbols](removeBadSymbols.py)
|
||||||
|
|
389
addDateTaken.py
Executable file
389
addDateTaken.py
Executable file
|
@ -0,0 +1,389 @@
|
||||||
|
#! /usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""Rename image files with dates.
|
||||||
|
|
||||||
|
# options
|
||||||
|
# -h --help display help
|
||||||
|
# -a --noask --no-interactive do not prompt the user to confirm renaming
|
||||||
|
# -l --onlylog just logs what would have been done but not doing anything
|
||||||
|
# -o [file] --outputfile [file] specifies the output (log) file (standard is da.log)
|
||||||
|
# -v [file] --voutputfile [file] specifies the verbose output log file (standard dav.log)
|
||||||
|
# -q --quiet do not log
|
||||||
|
"""
|
||||||
|
|
||||||
|
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
|
||||||
|
import logging
|
||||||
|
# import re as regex
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import exifread
|
||||||
|
import subprocess
|
||||||
|
import re
|
||||||
|
|
||||||
|
LOGGING_LEVELS = {"debug": logging.DEBUG,
|
||||||
|
"info": logging.INFO,
|
||||||
|
"warning": logging.WARNING,
|
||||||
|
"error": logging.ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
REPLACER = "_"
|
||||||
|
INVALID_NAMES = ["", ".", ".."]
|
||||||
|
CAMERA_REGEXES = [
|
||||||
|
r"SAM_\d{4}",
|
||||||
|
r"SVN_\d{5}",
|
||||||
|
r"DSC[_\dFIN]\d{4}",
|
||||||
|
r"IMG_\d{8}_\d{6}(_\d{3})?",
|
||||||
|
r"R?IMGP?[_\d]{4,5}",
|
||||||
|
r"\d{3}-\d{4}_IMG",
|
||||||
|
r"(IMG|VID|PXL)_20\d{6}_\d{6,9}"
|
||||||
|
+ r"(.PANO|.NIGHT|.LS|.MP|.PORTRAIT|.MOTION-\d\d.ORIGINAL)?",
|
||||||
|
# here one could extract the date and compare it with
|
||||||
|
# the exif data and issue a warning if different
|
||||||
|
r"P[ABC\d]\d{6}",
|
||||||
|
r"PICT\d{4}",
|
||||||
|
r"SDC\d{5}",
|
||||||
|
r"\d{13}",
|
||||||
|
r"\d{14}_0M.{6}--\.\d",
|
||||||
|
r"0.{7}--\.\d",
|
||||||
|
r"0CLx.{6}--\.\d",
|
||||||
|
r"\d{8}_\d{6}(_HDR)?(~2)?",
|
||||||
|
r"Foto\d{4}",
|
||||||
|
r"\d{8,9}_\d{6}",
|
||||||
|
r"[\dA-Z]{8}-([\dA-Z]{4,5}-){3}[\dA-Z]{12}",
|
||||||
|
r"D7C_\d{4}",
|
||||||
|
r"_MG_\d{4}",
|
||||||
|
r"\d{5}" # only with MTS at the end but this cannot be detected
|
||||||
|
# by the regex, can it?
|
||||||
|
]
|
||||||
|
CAMERA_REGEXES = [re.compile(regex) for regex in CAMERA_REGEXES]
|
||||||
|
DATE_REGEX = re.compile(r'20\d{2}_\d{2}_\d{2}')
|
||||||
|
DATE_IN_FILENAME_REGEXES = [
|
||||||
|
r"signal-(?P<year>20\d{2})-(?P<month>[01]\d)-(?P<day>[0123]\d)-(?P<hour>"
|
||||||
|
+ r"\d{2})(?P<min>\d{2})(?P<sec>\d{2})(?P<extra>.*)",
|
||||||
|
# telegram:
|
||||||
|
r"photo_(?P<year>20\d{2})-(?P<month>[01]\d)-(?P<day>[0123]\d)_(?P<hour>"
|
||||||
|
+ r"\d{2})-(?P<min>\d{2})-(?P<sec>\d{2})(?P<extra>.*)",
|
||||||
|
# Camera from my Fairphone 3 with DivestOS:
|
||||||
|
r"(?P<year>20\d{2})_(?P<month>[01]\d)_(?P<day>[0123]\d)_"
|
||||||
|
+ r"(?P<hour>\d{2})(?P<min>\d{2})(?P<sec>\d{2})_"
|
||||||
|
+ r"\d{4}-(\d{2}-){5}\d{3}(?P<extra>.*)"
|
||||||
|
]
|
||||||
|
DATE_IN_FILENAME_REGEXES = [re.compile(regex) for regex in
|
||||||
|
DATE_IN_FILENAME_REGEXES]
|
||||||
|
LOGGER = logging.getLogger('addDates')
|
||||||
|
# not used:
|
||||||
|
EXIF_TAGS = {'.jpg': ("EXIF DateTimeOriginal", ["Image DateTime"]),
|
||||||
|
'.JPG': ("EXIF DateTimeOriginal", ["Image DateTime"]),
|
||||||
|
'.JPEG': ("EXIF DateTimeOriginal", ["Image DateTime"]),
|
||||||
|
'.jpeg': ("EXIF DateTimeOriginal", ["Image DateTime"]),
|
||||||
|
'.mov': ("EXIF DateTimeOriginal", ["Image DateTime"]),
|
||||||
|
'.MOV': ("EXIF DateTimeOriginal", ["Image DateTime"]),
|
||||||
|
'.mp4': ("EXIF DateTimeOriginal", ["Image DateTime"]),
|
||||||
|
'.mts': ("EXIF DateTimeOriginal", ["Image DateTime"]),
|
||||||
|
'.MTS': ("EXIF DateTimeOriginal", ["Image DateTime"])}
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
"""Parse command line arguments.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with the command line arguments.
|
||||||
|
|
||||||
|
"""
|
||||||
|
parser = ArgumentParser(
|
||||||
|
description="""Using the date the picture or movie was taken for file name.
|
||||||
|
|
||||||
|
Which files are renamed is logged in a log file.
|
||||||
|
(See below in --log.)""",
|
||||||
|
formatter_class=ArgumentDefaultsHelpFormatter
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-a", "--noask", "--no-interactive",
|
||||||
|
dest="ask",
|
||||||
|
help=("Specify if renaming should not be confirmed by the user" +
|
||||||
|
" but just done."),
|
||||||
|
action="store_false"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-l", "--onlylog",
|
||||||
|
dest="rename",
|
||||||
|
action="store_false",
|
||||||
|
help="Dry run."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--log", "--logfile",
|
||||||
|
dest="log",
|
||||||
|
metavar="L",
|
||||||
|
help="""the file where to write the logging output.
|
||||||
|
This file is overwritten if existing.
|
||||||
|
If not specified assume 'rename.log'""",
|
||||||
|
action="store",
|
||||||
|
default=os.path.join("..", "rename.log")
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--loglevel", "--level",
|
||||||
|
dest="loglevel",
|
||||||
|
help="""Specify the log level.
|
||||||
|
'error' includes OS errors at renaming files.
|
||||||
|
'warning' additionally includes no-renames because of weird resulting file name.
|
||||||
|
'info' includes no-renames because of existing files and renames and working directories.
|
||||||
|
'debug' includes good files.""",
|
||||||
|
default="debug",
|
||||||
|
choices=LOGGING_LEVELS.keys()
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-q", "--quiet",
|
||||||
|
dest="quiet",
|
||||||
|
help="Set log level to warning, so usually do not create a log file.",
|
||||||
|
action="store_true"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
nargs="?",
|
||||||
|
dest="top",
|
||||||
|
metavar="directory",
|
||||||
|
help="""The directory in which to rename files.""",
|
||||||
|
action="store",
|
||||||
|
default=".")
|
||||||
|
args = parser.parse_args()
|
||||||
|
if args.quiet:
|
||||||
|
args.loglevel = "warning"
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def remove_camerastring(name: str):
|
||||||
|
"""Remove all bad symbols from name."""
|
||||||
|
for regex in CAMERA_REGEXES:
|
||||||
|
match = re.match(regex, name)
|
||||||
|
if match:
|
||||||
|
LOGGER.debug("Pattern {pattern} is found and left {rest}".format(
|
||||||
|
pattern=regex.pattern, rest=name[match.end():]))
|
||||||
|
return name[match.end():]
|
||||||
|
if re.match(DATE_REGEX, name):
|
||||||
|
LOGGER.debug("File '{}' already has the date.".format(name))
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
LOGGER.warning(
|
||||||
|
"File '{}' does not have date nor is from a recognized camera.".format(name))
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def get_date_taken(directory, file):
|
||||||
|
"""Get the date the file was taken at in nice format.
|
||||||
|
|
||||||
|
Format is YYYY_MM_DD_HHMMSS.
|
||||||
|
"""
|
||||||
|
path = os.path.join(directory, file)
|
||||||
|
try:
|
||||||
|
image = open(path, 'rb') # r=read-only, b=binary
|
||||||
|
except FileNotFoundError:
|
||||||
|
LOGGER.warning("File '{}' not found.".format(os.path.join(directory, file)))
|
||||||
|
return None
|
||||||
|
except Exception as error:
|
||||||
|
LOGGER.warning(
|
||||||
|
"Error while opening file '{}' Please improve accurary of script with this error:\n{}".format(
|
||||||
|
path, error))
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
with image:
|
||||||
|
exif = exifread.process_file(image, details=False)
|
||||||
|
if len(exif) == 0:
|
||||||
|
# no exif data readable by python, probably MOV file
|
||||||
|
exifresult = subprocess.run(
|
||||||
|
['exiftool', '-dateFormat', '%Y_%m_%d_%H%M%S',
|
||||||
|
'-DateTimeOriginal', path],
|
||||||
|
# -DateTimeOriginal works for MTS files from my camera
|
||||||
|
# and one mov file I found on my computer
|
||||||
|
# while -CreateDate works only with the mov file
|
||||||
|
# If in the future there are files with only
|
||||||
|
# -CreateDate or -DateTimeOriginal is wrong, a more
|
||||||
|
# sophisticated method is needed.
|
||||||
|
# '-CreateDate', path],
|
||||||
|
capture_output=True)
|
||||||
|
output = exifresult.stdout.decode('utf-8', 'backslashreplace').strip()
|
||||||
|
LOGGER.debug("exiftool result for '{}': '{}'".format(path, output))
|
||||||
|
try:
|
||||||
|
date = output.split(":")[1].strip()
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
LOGGER.info("File '{}' has no exif data.".format(path))
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
if date == "0000": # no actual data
|
||||||
|
LOGGER.info(("File '{}' has no meaningful "
|
||||||
|
+ "creation date.").format(path))
|
||||||
|
return None
|
||||||
|
return date
|
||||||
|
# Debug: print("\n".join("'{}'\t'{}'".format(k, v) for k, v in exif.items()))
|
||||||
|
try:
|
||||||
|
date_as_str = str(exif["EXIF DateTimeOriginal"])
|
||||||
|
except KeyError:
|
||||||
|
try:
|
||||||
|
date_as_str = str(exif["Image DateTime"])
|
||||||
|
except KeyError:
|
||||||
|
LOGGER.warning("Image '{}' has no creation date.".format(path))
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
LOGGER.warning("Image '{}' has no 'EXIF DateTimeOriginal'. So take 'Image DateTime'.".format(
|
||||||
|
path
|
||||||
|
))
|
||||||
|
# no creation date
|
||||||
|
# now either returned or date_as_str exists
|
||||||
|
logging.debug("That's the date for '{}': '{}'".format(path, date_as_str))
|
||||||
|
if date_as_str == "0000:00:00 00:00:00":
|
||||||
|
LOGGER.info("Image '{}' has no meaningful creation date.".format(path))
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
date, time = date_as_str.split(" ")
|
||||||
|
except ValueError:
|
||||||
|
LOGGER.error("File '{}' has invalid datetime format: '{}'".format(
|
||||||
|
path, date_as_str
|
||||||
|
))
|
||||||
|
return None
|
||||||
|
except AttributeError as attrerror:
|
||||||
|
LOGGER.error("File '{}' has a problem: with the following exif data: '{}'\n'{}'".format(
|
||||||
|
path,
|
||||||
|
"\n".join("'{}'\t'{}'".format(k, v) for k, v in exif.items()),
|
||||||
|
attrerror
|
||||||
|
))
|
||||||
|
raise
|
||||||
|
try:
|
||||||
|
return "_".join(date.split(":")) + "_" + "".join(time.split(":"))
|
||||||
|
except ValueError:
|
||||||
|
LOGGER.error("File '{}' has invalid date or time format: '{}'".format(
|
||||||
|
path, date_as_str
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def get_date_and_extra_from_filename(file):
|
||||||
|
"""Return date if it's coded into filename.
|
||||||
|
|
||||||
|
This makes sense if there is no suitable exif data.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None if no date is found. Otherwise:
|
||||||
|
date in YYYY_MM_DD_hhmmss
|
||||||
|
shortened: the file name without the date part
|
||||||
|
"""
|
||||||
|
template = r"\g<year>_\g<month>_\g<day>_\g<hour>\g<min>\g<sec>"
|
||||||
|
for regex in DATE_IN_FILENAME_REGEXES:
|
||||||
|
match = re.match(regex, file)
|
||||||
|
if match:
|
||||||
|
date = match.expand(template)
|
||||||
|
LOGGER.debug("Found datetime in filename {file}: {date}".format(
|
||||||
|
file=file, date=date))
|
||||||
|
return (date, match.group("extra"))
|
||||||
|
LOGGER.info("No date found in filename {file}".format(file=file))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def rename_file(directory, file, args):
|
||||||
|
"""Rename file directory/file if renaming is OK."""
|
||||||
|
date = get_date_taken(directory, file)
|
||||||
|
LOGGER.debug('got date {} for file {}'.format(date, file))
|
||||||
|
if date is None:
|
||||||
|
# logging already happened
|
||||||
|
date_shortened = get_date_and_extra_from_filename(file)
|
||||||
|
if date_shortened is None:
|
||||||
|
# logging already happened
|
||||||
|
return
|
||||||
|
date, shortened = date_shortened
|
||||||
|
else:
|
||||||
|
shortened = remove_camerastring(file)
|
||||||
|
if shortened is None:
|
||||||
|
# date is already there, logging has happened
|
||||||
|
return
|
||||||
|
new_name = (
|
||||||
|
date
|
||||||
|
+ ("" if shortened.startswith("_") or shortened.startswith(".") else "_")
|
||||||
|
+ shortened.replace('.JPEG', '.jpg').replace('.jpeg', '.jpg').replace(
|
||||||
|
'.JPG', '.jpg').replace('.MOV', '.mov').replace(".MTS", ".mts")
|
||||||
|
)
|
||||||
|
if new_name == file:
|
||||||
|
LOGGER.debug("'{}' is OK.".format(file))
|
||||||
|
return
|
||||||
|
path = os.path.join(directory, file)
|
||||||
|
new_path = os.path.join(directory, new_name)
|
||||||
|
if os.path.lexists(new_path):
|
||||||
|
LOGGER.info("'{}' is not renamed to '{}' because this already exists.".format(
|
||||||
|
file, new_name
|
||||||
|
))
|
||||||
|
return
|
||||||
|
if new_name in INVALID_NAMES:
|
||||||
|
LOGGER.warning("'{}' is not renamed because it would invalid: '{}'.".format(
|
||||||
|
path, new_name))
|
||||||
|
return
|
||||||
|
if args.ask:
|
||||||
|
rename = input("Rename '{}' to '{}'? (Enter for yes)".format(
|
||||||
|
path, new_name
|
||||||
|
))
|
||||||
|
rename = not rename.lower().startswith("n")
|
||||||
|
else:
|
||||||
|
rename = True
|
||||||
|
if rename:
|
||||||
|
if args.rename: # == not onlylog
|
||||||
|
try:
|
||||||
|
os.rename(path, new_path)
|
||||||
|
except FileNotFoundError as error:
|
||||||
|
LOGGER.error("Could not move '{}' to '{}' due to FileNotFoundError: {}".format(
|
||||||
|
path, new_name, error
|
||||||
|
))
|
||||||
|
LOGGER.info("Rename '{}' to '{}'".format(file, new_name))
|
||||||
|
else:
|
||||||
|
LOGGER.info("Did not rename '{}' to '{}' due to user choice.".format(
|
||||||
|
file, new_name
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def if_ignore_dir(dirname):
|
||||||
|
"""Return if this directory name should be ignored.
|
||||||
|
|
||||||
|
This means that the directory content is ignored as well."""
|
||||||
|
return (
|
||||||
|
dirname.startswith('.') or
|
||||||
|
dirname == '__pycache__' or (
|
||||||
|
dirname.startswith('__') and ( # special python file
|
||||||
|
dirname.endswith('__.py') or
|
||||||
|
dirname.endswith('__.html') or
|
||||||
|
dirname.endswith('__.txt')
|
||||||
|
)) or dirname.endswith('.class')) # java class file
|
||||||
|
|
||||||
|
def if_ignore_file(filename):
|
||||||
|
"""Return if this file should be ignored."""
|
||||||
|
return (
|
||||||
|
filename.startswith('.') or not (
|
||||||
|
filename.endswith('.jpeg') or
|
||||||
|
filename.endswith('.jpg') or
|
||||||
|
filename.endswith('.JPEG') or
|
||||||
|
filename.endswith('.JPG') or
|
||||||
|
filename.endswith('.png') or
|
||||||
|
filename.endswith('.PNG') or
|
||||||
|
filename.endswith('.mov') or
|
||||||
|
filename.endswith('.MOV') or
|
||||||
|
filename.endswith('.mp4') or
|
||||||
|
filename.endswith('.mkv') or
|
||||||
|
filename.endswith('.mts') or
|
||||||
|
filename.endswith('.MTS')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if __name__ == "__main__":
|
||||||
|
args = parse_args()
|
||||||
|
logging.basicConfig(
|
||||||
|
filename=args.log,
|
||||||
|
)
|
||||||
|
LOGGER.setLevel(LOGGING_LEVELS[args.loglevel])
|
||||||
|
for dirpath, dirnames, filenames in os.walk(args.top, topdown=True):
|
||||||
|
print('.', end='')
|
||||||
|
LOGGER.info("Go to directory '{}'".format(os.path.abspath(dirpath)))
|
||||||
|
for dir in dirnames:
|
||||||
|
if if_ignore_dir(dir):
|
||||||
|
LOGGER.debug("Ignore dir '{}'".format(dir))
|
||||||
|
dirnames.remove(dir)
|
||||||
|
for file in filenames:
|
||||||
|
if if_ignore_file(file):
|
||||||
|
LOGGER.debug("Ignore file '{}'".format(file))
|
||||||
|
else:
|
||||||
|
rename_file(dirpath, file, args)
|
||||||
|
|
||||||
|
# remove empty log file:
|
||||||
|
logging.shutdown()
|
||||||
|
if os.stat(args.log).st_size == 0:
|
||||||
|
os.remove(args.log)
|
Loading…
Reference in a new issue