Initial commit.

This commit is contained in:
Olivier 'reivilibre' 2021-04-08 11:54:35 +01:00
commit 22d2d14aec
6 changed files with 392 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/.idea
/.venv

0
phototrie/__init__.py Normal file
View File

65
phototrie/batchrename.py Normal file
View File

@ -0,0 +1,65 @@
import os
import re
import sys
import argparse
from typing import List
def cli():
parser = argparse.ArgumentParser(
description="Renames files in batch by regex.",
)
parser.add_argument(
"pattern",
type=str,
help="regex pattern to be replaced (on the basename)",
)
parser.add_argument(
"replacement",
type=str,
help="replacement pattern",
)
parser.add_argument(
"files",
type=str,
nargs="*",
help="list of files to rename",
)
parser.add_argument(
"--apply",
"-y",
action="store_true",
help="actually apply the replacement (dry-run if omitted)",
)
args = parser.parse_args(sys.argv[1:])
pattern = re.compile(args.pattern)
for filename in sys.argv[3:]:
base = os.path.basename(filename)
dirn = os.path.dirname(filename)
new_base = pattern.sub(args.replacement, base)
if base == new_base:
print(f"no match: {filename}", file=sys.stderr)
continue
target_filename = dirn + "/" + new_base
if os.path.exists(target_filename):
print(f"target exists: {target_filename}", file=sys.stderr)
continue
if args.apply:
os.rename(filename, target_filename)
else:
print(f"{filename}{target_filename}", file=sys.stderr)
if __name__ == "__main__":
cli()

96
phototrie/datename.py Normal file
View File

@ -0,0 +1,96 @@
import argparse
import os
import sys
import exifread
import re
DATETIME_KEY = "Image DateTime"
DATETIME_PATTERN = re.compile(r"^(\d{4}):(\d{2}):(\d{2}) (\d{2}):(\d{2}):(\d{2})$")
def get_new_name(filename, date_matches):
dirn = os.path.dirname(filename)
# base = os.path.basename(filename)
extension = os.path.splitext(filename)[1]
year, month, date, hour, second, minute = date_matches.groups()
suffix = 0
while True:
if suffix == 0:
target = (
f"{dirn}/{year}-{month}-{date}_{hour}:{second}:{minute}.{extension}"
)
else:
target = f"{dirn}/{year}-{month}-{date}_{hour}:{second}:{minute}_{suffix}.{extension}"
if not os.path.exists(target):
return target
suffix += 1
def cli():
parser = argparse.ArgumentParser(
description="Renames files in batch by regex.",
)
parser.add_argument(
"files",
type=str,
nargs="*",
help="list of files to rename",
)
parser.add_argument(
"--apply",
"-y",
action="store_true",
help="actually apply the replacement (dry-run if omitted)",
)
args = parser.parse_args(sys.argv[1:])
for filename in args.files:
f = open(filename, "rb")
tags = exifread.process_file(f)
if DATETIME_KEY not in tags:
print(f"No datetime tag found for: {filename}", file=sys.stderr)
continue
match = DATETIME_PATTERN.match(tags[DATETIME_KEY])
if match is None:
print(
f"Invalid datetime tag ({tags[DATETIME_KEY]!r}) found for: {filename}",
file=sys.stderr,
)
# also search for raw CR2
unextended = os.path.splitext(filename)[0]
found_raw = None
if os.path.exists(unextended + ".CR2"):
found_raw = unextended + ".CR2"
else:
unextended_basename = os.path.basename(unextended)
try_next = os.path.dirname(filename) + "/CR2/" + unextended_basename
if os.path.exists(try_next):
found_raw = try_next
# for k, v in tags.items():
# print(f"{k} = {v}")
# print(tags[DATETIME_KEY])
# print(found_raw)
if args.apply:
os.rename(filename, get_new_name(filename, match))
if found_raw is not None:
os.rename(found_raw, get_new_name(found_raw, match))
else:
print(filename, "", get_new_name(filename, match))
if found_raw is not None:
print(found_raw, "", get_new_name(found_raw, match))
if __name__ == "__main__":
cli()

104
phototrie/phototrie.py Normal file
View File

@ -0,0 +1,104 @@
import os
import shutil
from tempfile import mkdtemp
from tkinter import *
from typing import List, Tuple, Optional
from PIL import Image, ImageTk
NAMES = {"b": "bad", "g": "good", "p": "pristine"}
def prepare_processable(search_in: str):
tmp_dir = mkdtemp("")
to_process = []
extension = "." + sys.argv[1]
for file in sorted(os.listdir(search_in)):
if file.endswith(extension):
full_path = search_in + "/" + file
thumb_path = tmp_dir + "/" + file + ".jpg"
image = Image.open(file)
image.thumbnail((512, 512))
image.save(thumb_path)
# also search for raw CR2
unextended = os.path.splitext(file)[0]
found_raw = None
if os.path.exists(unextended + ".CR2"):
found_raw = unextended + ".CR2"
else:
unextended_basename = os.path.basename(unextended)
try_next = os.path.dirname(file) + "/CR2/" + unextended_basename
if os.path.exists(try_next):
found_raw = try_next
to_process.append((full_path, thumb_path, found_raw))
return to_process, tmp_dir
class App:
def __init__(
self, to_process: List[Tuple[str, str, Optional[str]]], sort_dest: str
):
self.sort_dest = sort_dest
self.top = Tk()
self.processing_now = None
self.to_process = to_process
image = self.load_next_image()
self.display = Label(self.top, image=image)
self.display.image = image # prevents GC
self.display.grid()
self.top.bind("<b>", self.callback)
self.top.bind("<g>", self.callback)
self.top.bind("<p>", self.callback)
def callback(self, e):
name = NAMES[e.char]
print(self.processing_now[0], name)
shutil.move(self.processing_now[0], self.sort_dest + "/" + name)
if self.processing_now[2] is not None:
# also move the raw
shutil.move(self.processing_now[2], self.sort_dest + "/" + name)
os.remove(self.processing_now[1])
try:
next_img = self.load_next_image()
except IndexError:
self.top.quit()
return
self.display.configure(image=next_img)
self.display.image = next_img # prevents GC
def load_next_image(self):
next = self.to_process.pop(0)
self.processing_now = next
_full_path, thumb_path, _raw_path = next
img = Image.open(thumb_path)
photo_img = ImageTk.PhotoImage(img)
return photo_img
def cli():
print("<phototrie.py> [file extension to process e.g. CR2 or jpg.")
print("keybinds: B 'bad', 'G' good, 'P' pristine")
to_process, tmp_dir = prepare_processable(".")
dirs = "./bad", "./good", "./pristine"
for directory in dirs:
if not os.path.isdir(directory):
os.mkdir(directory)
app = App(to_process, ".")
app.top.mainloop()
os.rmdir(tmp_dir)
if __name__ == "__main__":
cli()

125
setup.py Normal file
View File

@ -0,0 +1,125 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import io
import os
import sys
from shutil import rmtree
from setuptools import find_packages, setup, Command
# Package meta-data.
NAME = "phototrie"
DESCRIPTION = "Tools for sorting photos"
URL = "https://bics.ga/reivilibre/phototrie"
EMAIL = "reivi@librepush.net"
AUTHOR = "Olivier 'reivilibre'"
REQUIRES_PYTHON = ">=3.7.0"
VERSION = "0.1.0"
# What packages are required for this module to be executed?
REQUIRED = [
"ExifRead",
"Pillow",
]
# What packages are optional?
EXTRAS = {
}
# The rest you shouldn't have to touch too much :)
# ------------------------------------------------
# Except, perhaps the License and Trove Classifiers!
# If you do change the License, remember to change the Trove Classifier for that!
here = os.path.abspath(os.path.dirname(__file__))
# Import the README and use it as the long-description.
# Note: this will only work if 'README.md' is present in your MANIFEST.in file!
try:
with io.open(os.path.join(here, "README.md"), encoding="utf-8") as f:
long_description = "\n" + f.read()
except FileNotFoundError:
long_description = DESCRIPTION
# Load the package's __version__.py module as a dictionary.
about = {}
if not VERSION:
project_slug = NAME.lower().replace("-", "_").replace(" ", "_")
with open(os.path.join(here, project_slug, "__version__.py")) as f:
exec(f.read(), about)
else:
about["__version__"] = VERSION
class UploadCommand(Command):
"""Support setup.py upload."""
description = "Build and publish the package."
user_options = []
@staticmethod
def status(s):
"""Prints things in bold."""
print("\033[1m{0}\033[0m".format(s))
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
try:
self.status("Removing previous builds…")
rmtree(os.path.join(here, "dist"))
except OSError:
pass
self.status("Building Source and Wheel (universal) distribution…")
os.system("{0} setup.py sdist bdist_wheel --universal".format(sys.executable))
self.status("Uploading the package to PyPI via Twine…")
os.system("twine upload dist/*")
self.status("Pushing git tags…")
os.system("git tag v{0}".format(about["__version__"]))
os.system("git push --tags")
sys.exit()
# Where the magic happens:
setup(
name=NAME,
version=about["__version__"],
description=DESCRIPTION,
long_description=long_description,
long_description_content_type="text/markdown",
author=AUTHOR,
author_email=EMAIL,
python_requires=REQUIRES_PYTHON,
url=URL,
packages=find_packages(exclude=["tests", "*.tests", "*.tests.*", "tests.*"]),
# If your package is a single module, use this instead of 'packages':
# py_modules=['mypackage'],
entry_points={
"console_scripts": [
"phototrie=phototrie.phototrie:cli",
"batchrename=phototrie.batchrename:cli",
"datename=phototrie.datename:cli",
],
},
install_requires=REQUIRED,
extras_require=EXTRAS,
include_package_data=True,
# TODO license='GPL3',
classifiers=[
# Trove classifiers
# Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers
"Programming Language :: Python",
"Programming Language :: Python :: 3",
],
)