diff options
Diffstat (limited to 'genlib.py')
-rw-r--r-- | genlib.py | 177 |
1 files changed, 177 insertions, 0 deletions
diff --git a/genlib.py b/genlib.py new file mode 100644 index 0000000..898542d --- /dev/null +++ b/genlib.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +# Startdate: 2021-01-29 15:35 +# Dependencies: +# devuan-req: python3-exifread +# Reference: +# https://stackoverflow.com/questions/1192978/python-get-relative-path-of-all-files-and-subfolders-in-a-directory +# https://stackoverflow.com/questions/3207219/how-do-i-list-all-files-of-a-directory +# https://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python +from sys import stderr +from exifread import process_file +from os import walk, path, stat, symlink +from pathlib import Path +from datetime import date +from shutil import copy2, move + +try: + if debuglevel is None: + debuglevel = 0 +except: + debuglevel = 0 + +# Functions +def eprint(*args, **kwargs): + print(*args, file=stderr, **kwargs) + +def list_files(indir,relative=False,debuglevel=debuglevel, excludes=None): + x = 0 + f = [] + if debuglevel >= 9: + eprint(f"Listing all files underneath {indir}") + if len(excludes) > 0: + eprint("While excluding path matches:",excludes) + for (dirpath, dirnames, filenames) in walk(indir): + for file in filenames: + x += 1 + fullpath=path.join(dirpath,file) + relpath=path.join(path.relpath(dirpath,indir),file) + use = True + use_pattern = "" + for e in excludes: + # simple match, no regex + if e in relpath: + use = False + use_pattern = e + break # short-circuit + if use: + if debuglevel >= 9: eprint(x,fullpath,relpath) + f.append(relpath if relative else fullpath) + else: + if debuglevel >= 9: eprint(x,fullpath,f"ignored per {use_pattern}") + return f + +def limit(listf=None,filters=["image","video"]): + # available types are "image", "video" + newlist = [] + for f in listf: + _, ext = path.splitext(f) + ext = ext[1:].lower() + if ext in ["jpg","jpeg","png","svg","tif","tiff","gif"] and "image" in filters: + newlist.append(f) + elif ext in ["mp4","webm","avi"] and "video" in filters: + newlist.append(f) + return newlist + +def get_file_YM(tf,debuglevel=debuglevel,zero_pad=False): + Y = 0 ; M = 0 + #eprint(f"Debuglevel {debuglevel}") + try: + # read exif data + with open(tf,'rb') as f: + tags = process_file(f, details=False, stop_tag="Image DateTime") + if "Image DateTime" in tags: + #if debuglevel >= 5: eprint(tf,tags["Image DateTime"]) + Y = str(tags["Image DateTime"]).split(' ')[0].split(':') + if Y == "['']" or Y == ['']: + Y = ['0','0'] + M = Y[1] ; Y = Y[0]; + # Any other image timestamps could be used here + #eprint(tags) + except KeyboardInterrupt: + return -1, -1 + except: + if debuglevel >= 1: eprint(f"Unable to extract any exif data for {tf}") + if int(Y) == 0 or int(M) == 0: + # need to just use timestamp + try: + ts = date.fromtimestamp(stat(tf).st_mtime) + except KeyboardInterrupt: + return -1, -1 + except: + # no timestamp available from filesystem? + if debuglevel >= 1: eprint(f"No timestamp available for {tf}") + if int(Y) == 0: Y = ts.year + if int(M) == 0: M = ts.month + if zero_pad: + M = str(M).zfill(2) + else: + M = str(int(M)) + if debuglevel >= 3: + eprint(f"{tf} {Y}/{M}") + return Y, M + +def make_YM_forest(outdir, flist, action = "symlink", dryrun=True, debuglevel=debuglevel, zero_pad=False, relative_symlinks = False): + """ + For each file in flist, [action] it to outdir, sorting into YYYY/MM subdirs based on exif metadata or timestamp. + Action may be one of ['symlink', 'copy', 'move']. + If relative_symlink = True, then make relative symlinks if possible. + """ + result = 0 + stop = False + + # validate input + if action not in ['symlink', 'copy', 'move']: + eprint("make_YM_forest action may be one of ['symlink', 'copy', 'move'].") + return -1 + + # Learn all directories to make + # and also cache the files with Y/M value + fdict = {} + destdirs = [] + for f in flist: + try: + Y, M = get_file_YM(f,debuglevel=debuglevel,zero_pad=zero_pad) + if Y == -1 and M == -1: + #eprint("Stopping due to keyboard variant 2.") + stop = True + break + except KeyboardInterrupt: + #eprint("Stopping due to keyboard variant 1.") + stop = True + break + if zero_pad: + M = str(M).zfill(2) + else: + M = str(int(M)) + YM = f"{Y}/{M}" + if YM not in destdirs: destdirs.append(YM) + fdict[f] = YM + # finish the for f in flist + + # short-circuit if keyboard action happened in one of the nested functions + if stop: + return -1 + + # Make directories + for d in destdirs: + dd = path.join(outdir,d) + if debuglevel >= 3: + eprint(f"Make dir: {dd}") + if not dryrun: + Path(dd).mkdir(parents=True, exist_ok=True) + + # Make symlinks + for f in fdict: + ff = f + basename = path.basename(ff) + destfile = path.join(outdir,fdict[f],basename) + if action == "symlink" and relative_symlinks: + ff = path.relpath(ff,destfile) + if debuglevel >= 2: + if action == "copy": + eprint(f"cp -p {ff} {destfile}") + elif action == "symlink": + eprint(f"ln -s {ff} {destfile}") + elif action == "move": + eprint(f"mv {ff} {destfile}") + + if not dryrun: + #if False: + if action == "copy": + copy2(ff,destfile) + elif action == "symlink": + symlink(ff,destfile) + elif action == "move": + move(ff,destfile) + + return result |