skilleter-thingy 0.0.40__py3-none-any.whl → 0.0.41__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of skilleter-thingy might be problematic. Click here for more details.

Files changed (68) hide show
  1. skilleter_thingy/__init__.py +6 -0
  2. skilleter_thingy/addpath.py +107 -0
  3. skilleter_thingy/borger.py +269 -0
  4. skilleter_thingy/console_colours.py +63 -0
  5. skilleter_thingy/diskspacecheck.py +67 -0
  6. skilleter_thingy/docker_purge.py +113 -0
  7. skilleter_thingy/ffind.py +536 -0
  8. skilleter_thingy/ggit.py +90 -0
  9. skilleter_thingy/ggrep.py +154 -0
  10. skilleter_thingy/git_br.py +180 -0
  11. skilleter_thingy/git_ca.py +142 -0
  12. skilleter_thingy/git_cleanup.py +287 -0
  13. skilleter_thingy/git_co.py +220 -0
  14. skilleter_thingy/git_common.py +61 -0
  15. skilleter_thingy/git_hold.py +154 -0
  16. skilleter_thingy/git_mr.py +92 -0
  17. skilleter_thingy/git_parent.py +77 -0
  18. skilleter_thingy/git_review.py +1428 -0
  19. skilleter_thingy/git_update.py +385 -0
  20. skilleter_thingy/git_wt.py +96 -0
  21. skilleter_thingy/gitcmp_helper.py +322 -0
  22. skilleter_thingy/gitprompt.py +274 -0
  23. skilleter_thingy/gl.py +174 -0
  24. skilleter_thingy/gphotosync.py +610 -0
  25. skilleter_thingy/linecount.py +155 -0
  26. skilleter_thingy/moviemover.py +133 -0
  27. skilleter_thingy/photodupe.py +136 -0
  28. skilleter_thingy/phototidier.py +248 -0
  29. skilleter_thingy/py_audit.py +131 -0
  30. skilleter_thingy/readable.py +270 -0
  31. skilleter_thingy/remdir.py +126 -0
  32. skilleter_thingy/rmdupe.py +550 -0
  33. skilleter_thingy/rpylint.py +91 -0
  34. skilleter_thingy/splitpics.py +99 -0
  35. skilleter_thingy/strreplace.py +82 -0
  36. skilleter_thingy/sysmon.py +435 -0
  37. skilleter_thingy/tfm.py +920 -0
  38. skilleter_thingy/tfparse.py +101 -0
  39. skilleter_thingy/thingy/__init__.py +6 -0
  40. skilleter_thingy/thingy/colour.py +213 -0
  41. skilleter_thingy/thingy/dc_curses.py +278 -0
  42. skilleter_thingy/thingy/dc_defaults.py +221 -0
  43. skilleter_thingy/thingy/dc_util.py +50 -0
  44. skilleter_thingy/thingy/dircolors.py +308 -0
  45. skilleter_thingy/thingy/docker.py +95 -0
  46. skilleter_thingy/thingy/files.py +142 -0
  47. skilleter_thingy/thingy/git.py +1371 -0
  48. skilleter_thingy/thingy/git2.py +1307 -0
  49. skilleter_thingy/thingy/gitlab.py +193 -0
  50. skilleter_thingy/thingy/logger.py +112 -0
  51. skilleter_thingy/thingy/path.py +156 -0
  52. skilleter_thingy/thingy/popup.py +87 -0
  53. skilleter_thingy/thingy/process.py +112 -0
  54. skilleter_thingy/thingy/run.py +334 -0
  55. skilleter_thingy/thingy/tfm_pane.py +595 -0
  56. skilleter_thingy/thingy/tidy.py +160 -0
  57. skilleter_thingy/trimpath.py +84 -0
  58. skilleter_thingy/window_rename.py +92 -0
  59. skilleter_thingy/xchmod.py +125 -0
  60. skilleter_thingy/yamlcheck.py +89 -0
  61. {skilleter_thingy-0.0.40.dist-info → skilleter_thingy-0.0.41.dist-info}/METADATA +1 -1
  62. skilleter_thingy-0.0.41.dist-info/RECORD +66 -0
  63. skilleter_thingy-0.0.41.dist-info/top_level.txt +1 -0
  64. skilleter_thingy-0.0.40.dist-info/RECORD +0 -6
  65. skilleter_thingy-0.0.40.dist-info/top_level.txt +0 -1
  66. {skilleter_thingy-0.0.40.dist-info → skilleter_thingy-0.0.41.dist-info}/LICENSE +0 -0
  67. {skilleter_thingy-0.0.40.dist-info → skilleter_thingy-0.0.41.dist-info}/WHEEL +0 -0
  68. {skilleter_thingy-0.0.40.dist-info → skilleter_thingy-0.0.41.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,155 @@
1
+ #! /usr/bin/env python3
2
+
3
+ ################################################################################
4
+ """ Count lines of code by filetype
5
+ """
6
+ ################################################################################
7
+
8
+ import os
9
+ import sys
10
+ import argparse
11
+
12
+ import filetype
13
+
14
+ import thingy.files as files
15
+
16
+ ################################################################################
17
+
18
+ def guess_filetype(filepath):
19
+ """ Guess the type of a file """
20
+
21
+ binary = False
22
+
23
+ # Check for the filetype (usually detects binary files)
24
+
25
+ ftype = filetype.guess(filepath)
26
+
27
+ # If we have a type, store the extension and, as it is binary
28
+ # set the number of lines to 0
29
+ # Otherwise, work out the filetype and count the lines
30
+
31
+ if ftype:
32
+ ext = ftype.extension
33
+ binary = True
34
+ else:
35
+ filename = os.path.split(filepath)[1]
36
+
37
+ if '.' in filename:
38
+ ext = filename.split('.')[-1]
39
+ else:
40
+ if filename.startswith('Jenkins'):
41
+ ext = 'Jenkins'
42
+ elif filename.startswith('Docker'):
43
+ ext = 'Docker'
44
+ else:
45
+ ext = filename
46
+
47
+ return ext, binary
48
+
49
+ ################################################################################
50
+
51
+ def determine_filetype(filepath):
52
+ """ Determine the type of a file """
53
+
54
+ file_type = files.file_type(filepath)
55
+
56
+ if file_type.startswith('a /usr/bin/env '):
57
+ file_type = file_type[len('a /usr/bin/env '):]
58
+ elif file_type.startswith('symbolic link to '):
59
+ file_type = 'Symbolic link'
60
+
61
+ if file_type[0].islower():
62
+ file_type = file_type.capitalize()
63
+
64
+ ext = file_type.split(',')[0]
65
+ binary = 'text' not in file_type
66
+
67
+ if file_type.startswith('ASCII text'):
68
+ return guess_filetype(filepath)
69
+
70
+ return ext, binary
71
+
72
+ ################################################################################
73
+
74
+ def main():
75
+ """ Report Summary of files by name or extension """
76
+
77
+ parser = argparse.ArgumentParser(description='Summarise number of files, lines of text and total size of files in a directory tree')
78
+ parser.add_argument('-e-', '--ext', action='store_true', help='Identify file type using the file extension (faster but less accurrate)')
79
+
80
+ args = parser.parse_args()
81
+
82
+ filetypes = {}
83
+
84
+ # Wander down the tree
85
+
86
+ for dirpath, dirnames, filenames in os.walk('.'):
87
+ # Skip .git directories
88
+
89
+ if '.git' in dirnames:
90
+ dirnames.remove('.git')
91
+
92
+ for filename in filenames:
93
+ # Get the file path and size
94
+
95
+ filepath = os.path.join(dirpath, filename)
96
+ size = os.stat(filepath).st_size
97
+
98
+ if args.ext:
99
+ ext, binary = guess_filetype(filepath)
100
+ else:
101
+ ext, binary = determine_filetype(filepath)
102
+
103
+ if binary:
104
+ lines = 0
105
+ else:
106
+ with open(filepath, 'rb') as infile:
107
+ lines = len(infile.readlines())
108
+
109
+ # Update the summary
110
+
111
+ if ext in filetypes:
112
+ filetypes[ext]['files'] += 1
113
+ filetypes[ext]['size'] += size
114
+ filetypes[ext]['lines'] += lines
115
+ else:
116
+ filetypes[ext] = {'files': 1, 'size': size, 'lines': lines}
117
+
118
+ # Work out the maximum size of each field of data
119
+
120
+ total_files = 0
121
+ total_lines = 0
122
+ total_size = 0
123
+
124
+ for ext in sorted(filetypes.keys()):
125
+ total_files += filetypes[ext]['files']
126
+ total_lines += filetypes[ext]['lines']
127
+ total_size += filetypes[ext]['size']
128
+
129
+ size = files.format_size(filetypes[ext]['size'])
130
+ print(f"{ext}: {filetypes[ext]['files']:,} files, {filetypes[ext]['lines']:,} lines, {size}")
131
+
132
+ size = files.format_size(total_size)
133
+
134
+ print()
135
+ print(f'Total files: {total_files:,}')
136
+ print(f'Total lines: {total_lines:,}')
137
+ print(f'Total size: {size}')
138
+
139
+ ################################################################################
140
+
141
+ def linecount():
142
+ """Entry point"""
143
+
144
+ try:
145
+ main()
146
+
147
+ except KeyboardInterrupt:
148
+ sys.exit(1)
149
+ except BrokenPipeError:
150
+ sys.exit(2)
151
+
152
+ ################################################################################
153
+
154
+ if __name__ == '__main__':
155
+ linecount()
@@ -0,0 +1,133 @@
1
+ #! /usr/bin/env python3
2
+
3
+ """ Search for files matching a wildcard in a directory tree and move them to an
4
+ equivalent location in a different tree """
5
+
6
+ import argparse
7
+ import os
8
+ import sys
9
+ import glob
10
+ import pathlib
11
+ import shutil
12
+ import filecmp
13
+
14
+ ################################################################################
15
+
16
+ def error(msg, status=1):
17
+ """ Exit with an error message """
18
+
19
+ print(msg)
20
+
21
+ sys.exit(status)
22
+
23
+ ################################################################################
24
+
25
+ def parse_command_line():
26
+ """ Handle command line arguments """
27
+
28
+ parser = argparse.ArgumentParser(description='File relocation - move files by wildcard from one directory tree to another')
29
+
30
+ parser.add_argument('-s', '--source', type=str, required=True, help='Source directory')
31
+ parser.add_argument('-d', '--destination', type=str, required=True, help='Destination directory')
32
+ parser.add_argument('-D', '--dry-run', action='store_true', help='Report what files would be moved, without actually moving them')
33
+ parser.add_argument('files', nargs='*', help='List of wildcard matches')
34
+
35
+ args = parser.parse_args()
36
+
37
+ if not args.files:
38
+ print('You must specify at least one wildcard/regex parameter')
39
+
40
+ if not os.path.isdir(args.source):
41
+ error(f'{args.source} is not a directory')
42
+
43
+ if not os.path.isdir(args.destination):
44
+ error(f'{args.destination} is not a directory')
45
+
46
+ args.source_path = pathlib.Path(os.path.realpath(args.source))
47
+ args.destination_path = pathlib.Path(os.path.realpath(args.destination))
48
+
49
+ if args.source_path == args.destination_path:
50
+ error('Source and destination paths cannot be the same')
51
+
52
+ if args.source_path in args.destination_path.parents:
53
+ error('The destination directory cannot be within the source path')
54
+
55
+ if args.destination_path in args.source_path.parents:
56
+ error('The source directory cannot be within the destination path')
57
+
58
+ return args
59
+
60
+ ################################################################################
61
+
62
+ def main():
63
+ """ Entry point """
64
+
65
+ args = parse_command_line()
66
+
67
+ # Process each wildcard
68
+
69
+ for wild in args.files:
70
+ # Find matching files in the source tree
71
+
72
+ for source_file in args.source_path.glob(f'**/{wild}'):
73
+ # Ignore anything that isn't a file
74
+
75
+ if source_file.is_file():
76
+ # Determine where to put it
77
+
78
+ dest_file = args.destination_path / source_file.relative_to(args.source_path)
79
+
80
+ if dest_file.exists():
81
+
82
+ if filecmp.cmp(source_file, dest_file, shallow=False):
83
+ print(f'Destination file {dest_file} already exists and is identical, so deleting source')
84
+ if not args.dry_run:
85
+ os.unlink(source_file)
86
+ else:
87
+ print(f'Destination file {dest_file} already exists and is DIFFERENT')
88
+ else:
89
+ # If the destination directory doesn't exist, then create it
90
+
91
+ if not dest_file.parent.is_dir():
92
+ print(f'Creating directory {dest_file.parent}')
93
+
94
+ if not args.dry_run:
95
+ dest_file.parent.mkdir(parents=True, exist_ok=True)
96
+
97
+ # Move the file
98
+
99
+ print(f'Moving {source_file.name} to {dest_file.parent}')
100
+
101
+ if not args.dry_run:
102
+ try:
103
+ shutil.move(source_file, dest_file)
104
+ except PermissionError:
105
+ print(f'WARNING: Permissions error moving {source_file}')
106
+
107
+ # Delete the source directory if it is not empty
108
+
109
+ source_dir = os.path.dirname(source_file)
110
+
111
+ if not glob.glob(source_dir, recursive=True):
112
+ print('Deleting directory "{source_dir}"')
113
+ if not args.dry_run:
114
+ os.path.unlink(source_dir)
115
+
116
+ ################################################################################
117
+
118
+ def moviemover():
119
+ """Entry point"""
120
+
121
+ try:
122
+ main()
123
+
124
+ except KeyboardInterrupt:
125
+ sys.exit(1)
126
+
127
+ except BrokenPipeError:
128
+ sys.exit(2)
129
+
130
+ ################################################################################
131
+
132
+ if __name__ == '__main__':
133
+ moviemover()
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env python3
2
+ """Hash photos to find closely-similar images and report them"""
3
+
4
+ import sys
5
+ import os
6
+ import sys
7
+ import pickle
8
+ import argparse
9
+
10
+ import PIL
11
+
12
+ from collections import defaultdict
13
+
14
+ from PIL import Image
15
+ import imagehash
16
+
17
+ ################################################################################
18
+
19
+ def read_image_hashes(directories):
20
+ """Read all the specfied directories and hash every picture therein"""
21
+
22
+ hashes = defaultdict(list)
23
+
24
+ # Walk each directory tree
25
+
26
+ for directory in directories:
27
+ print(f'Scanning directory tree {directory}')
28
+
29
+ for root, _, files in os.walk(directory):
30
+ print(f'Scanning directory {root}')
31
+
32
+ for file in files:
33
+ filepath = os.path.join(root, file)
34
+
35
+ fileext = os.path.splitext(file)[1]
36
+
37
+ if fileext.lower() not in ('.jbf', '.ini', '.xml', '.ffs_db'):
38
+ # Calculate the hash and store path, dimensions and file size under the hash entry in the hashes table
39
+
40
+ try:
41
+ with Image.open(filepath) as image:
42
+ hash_value = imagehash.average_hash(image, hash_size=12)
43
+
44
+ size = os.stat(filepath).st_size
45
+ hashes[hash_value].append({'path': filepath, 'width': image.width, 'height': image.height, 'size': size})
46
+
47
+ except PIL.UnidentifiedImageError:
48
+ sys.stderr.write(f'ERROR: Unrecognized format {filepath} (size={size})\n')
49
+
50
+ except OSError:
51
+ sys.stderr.write(f'ERROR: Unable to read {filepath} (size={size})\n')
52
+
53
+ # Return the hash table
54
+
55
+ return hashes
56
+
57
+ ################################################################################
58
+
59
+ def main():
60
+ """Read the hashes and report duplicates in a vaguely civilised way"""
61
+
62
+ parser = argparse.ArgumentParser(description='Search for similar images')
63
+ parser.add_argument('directories', nargs='*', action='store', help='Directories to search')
64
+
65
+ args = parser.parse_args()
66
+
67
+ if not args.directories:
68
+ print('You must be specify at least one directory')
69
+ sys.exit(1)
70
+
71
+ try:
72
+ print('Loading cached data')
73
+
74
+ with open('photodupe.pickle', 'rb') as pickles:
75
+ hashes = pickle.load(pickles)
76
+ except (FileNotFoundError, EOFError):
77
+ print('Scanning directories')
78
+
79
+ hashes = read_image_hashes(args.directories)
80
+
81
+ # Sort the list of hashes so that we can easily find close matches
82
+
83
+ print('Sorting hashes')
84
+
85
+ hash_values = sorted([str(hashval) for hashval in hashes])
86
+
87
+ for hash_value in hash_values:
88
+ if len(hashes[hash_value]) > 1:
89
+ print(hash_value)
90
+ max_len = 0
91
+ min_size = None
92
+
93
+ for entry in hashes[hash_value]:
94
+ max_len = max(max_len, len(entry['path']))
95
+
96
+ if min_size is None:
97
+ min_size = entry['size']
98
+ else:
99
+ min_size = min(min_size, entry['size'])
100
+
101
+ if min_size >= 1024 * 1024:
102
+ size_suffix = 'MiB'
103
+ size_div = 1024*1024
104
+
105
+ elif min_size > 1024:
106
+ size_suffix = 'KiB'
107
+ size_div = 1024
108
+ else:
109
+ size_div = 1
110
+ size_suffix = ''
111
+
112
+ for entry in hashes[hash_value]:
113
+ size = entry['size'] // size_div
114
+ print(f' {entry["path"]:{max_len}} {size:>4} {size_suffix} ({entry["width"]}x{entry["height"]})')
115
+
116
+ with open('photodupe.pickle', 'wb') as pickles:
117
+ pickle.dump(hashes, pickles)
118
+
119
+ ################################################################################
120
+
121
+ def photodupe():
122
+ """Entry point"""
123
+
124
+ try:
125
+ main()
126
+
127
+ except KeyboardInterrupt:
128
+ sys.exit(1)
129
+
130
+ except BrokenPipeError:
131
+ sys.exit(2)
132
+
133
+ ################################################################################
134
+
135
+ if __name__ == '__main__':
136
+ photodupe()
@@ -0,0 +1,248 @@
1
+ #! /usr/bin/env python3
2
+
3
+ """ Perform various tidying operations on a directory full of photos:
4
+ 1. Remove leading '$' and '_' from filenames
5
+ 2. Move files in hidden directories up 1 level
6
+ 3. If the EXIF data in a photo indicates that it was taken on date that
7
+ doesn't match the name of the directory it is stored in (in YYYY-MM-DD format)
8
+ then it is moved to the correct directory, creating it if necessary.
9
+
10
+ All move/rename operations are carried out safely with the file being moved having
11
+ a numeric suffix added to the name if it conflicts with an existing file.
12
+
13
+ TODO: Ignore .stversions files
14
+
15
+ """
16
+
17
+ ################################################################################
18
+
19
+ import argparse
20
+ import os
21
+ import sys
22
+ import pathlib
23
+ import re
24
+
25
+ from PIL import UnidentifiedImageError
26
+ from PIL import Image
27
+ from PIL.ExifTags import TAGS
28
+
29
+ import thingy.colour as colour
30
+
31
+ ################################################################################
32
+
33
+ FILE_TYPES = ('.jpg', '.jpeg')
34
+
35
+ DATE_RE = re.compile(r'[0-9]{4}-[0-9]{2}-[0-9]{2}')
36
+
37
+ NUMBER_RE = re.compile(r'(.*) +\([0-9]+\).*')
38
+
39
+ ################################################################################
40
+
41
+ def error(msg, status=1):
42
+ """ Exit with an error message """
43
+
44
+ print(msg)
45
+
46
+ sys.exit(status)
47
+
48
+ ################################################################################
49
+
50
+ def parse_command_line():
51
+ """ Handle command line arguments """
52
+
53
+ parser = argparse.ArgumentParser(description='Re-organise photos into (hopefully) the correct folders.')
54
+
55
+ parser.add_argument('-D', '--dry-run', action='store_true', help='Report what files would be moved, without actually moving them')
56
+ parser.add_argument('path', nargs=1, help='Path to the picture storage directory')
57
+
58
+ args = parser.parse_args()
59
+
60
+ if not os.path.isdir(args.path[0]):
61
+ error(f'{args.path} is not a directory')
62
+
63
+ args.path = pathlib.Path(os.path.realpath(args.path[0]))
64
+
65
+ return args
66
+
67
+ ################################################################################
68
+
69
+ def safe_rename(args, source_file, new_name):
70
+ """ Rename a file, adding a numeric suffix to avoid overwriting anything """
71
+
72
+ # If the destination file exists, add a numeric suffix to the new name
73
+ # until we find one that doesn't
74
+
75
+ index = 1
76
+ new_name_stem = new_name.stem
77
+
78
+ while new_name.exists():
79
+ new_name = new_name.with_name(f'{new_name_stem}-{index}{new_name.suffix}')
80
+ index += 1
81
+
82
+ colour.write(f'Rename [BLUE:{source_file}] to [BLUE:{new_name}]')
83
+
84
+ # Panic if the destination parent directory exists, but isn't actually a directory
85
+
86
+ if new_name.parent.exists and not new_name.parent.is_dir:
87
+ colour.write('[RED:WARNING]: Destination [BLUE:{new_name.parent}] exists, but is not a directory - [BLUE:{source_file}] will not be renamed')
88
+ return source_file
89
+
90
+ # Rename and return the new namem, creating the directory for it to go in, if necessary
91
+
92
+ if not args.dry_run:
93
+ new_name.parent.mkdir(parents=True, exist_ok=True)
94
+
95
+ source_file.rename(new_name)
96
+
97
+ return new_name
98
+
99
+ ################################################################################
100
+
101
+ def get_exif_date(source_file):
102
+ """ Try an extract the daste when the photo was taken from the EXIF data
103
+ and return it in YYYY/YYYY-MM-DD format as the subdirectory where
104
+ the photo should be located """
105
+
106
+ # Get the EXIF data
107
+
108
+ try:
109
+ photo = Image.open(source_file)
110
+ except (OSError, UnidentifiedImageError):
111
+ colour.write(f'[RED:ERROR]: [BLUE:{source_file}] does not appear to be a valid image - ignoring EXIF data')
112
+ return None
113
+
114
+ exif = photo.getexif()
115
+
116
+ # Search for the original date/time tag
117
+
118
+ for tag_id in exif:
119
+ tag = TAGS.get(tag_id, tag_id)
120
+
121
+ if tag == 'DateTimeOriginal':
122
+ data = exif.get(tag_id)
123
+ if isinstance(data, bytes):
124
+ data = data.decode()
125
+
126
+ # Ignore dummy value
127
+
128
+ if data.startswith('0000:00:00'):
129
+ return None
130
+
131
+ # Convert to YYYY-MM-DD format, removing the time
132
+
133
+ date = f'{int(data[0:4]):04}-{int(data[5:7]):02}-{int(data[8:10]):02}'
134
+
135
+ return date
136
+
137
+ # No date tag found
138
+
139
+ return None
140
+
141
+ ################################################################################
142
+
143
+ def fix_file(args, source_file):
144
+ """ Fix a file by moving or renaming it to fix naming or directory issues """
145
+
146
+ # Get the image date from the EXIF data
147
+
148
+ image_date = get_exif_date(source_file)
149
+
150
+ # If the file starts with $, ~, _ or ., rename it to remove it
151
+
152
+ while source_file.name[0] in ('$', '~', '_', '.'):
153
+ new_name = source_file.with_name(source_file.name[1:])
154
+
155
+ source_file = safe_rename(args, source_file, new_name)
156
+
157
+ # If filename contains '~' then truncate it
158
+
159
+ if '~' in source_file.name:
160
+ new_name = source_file.with_name(source_file.name.split('~')[0] + source_file.suffix)
161
+
162
+ source_file = safe_rename(args, source_file, new_name)
163
+
164
+ # If the directory name starts with . or $ move the file up 1 level
165
+
166
+ while source_file.parts[-2][0] in ('$', '.'):
167
+ new_name = source_file.parent.parent / source_file.name
168
+
169
+ source_file = safe_rename(args, source_file, new_name)
170
+
171
+ # If the filename has a number in parentheses, then remove it
172
+
173
+ num_match = NUMBER_RE.fullmatch(source_file.stem)
174
+ if num_match:
175
+ new_name = source_file.parent / (num_match.group(1) + source_file.suffix)
176
+
177
+ source_file = safe_rename(args, source_file, new_name)
178
+
179
+ # See if the date in the EXIF data matches the directory name prefix
180
+ # and move it to the correct location if it doesn't
181
+
182
+ if image_date:
183
+ image_year = image_date.split('-')[0]
184
+
185
+ image_path = args.path / image_year / image_date
186
+
187
+ # If the file isn't already in a directory with the correct year and date
188
+ # move it to one that it
189
+
190
+ if not str(source_file.parent).startswith(str(image_path)):
191
+ # If the source directory has a description after the date, append that
192
+ # to the destination directory
193
+ # Otherwise, if the source directory doesn't have a date, append the whole
194
+ # directory name.
195
+
196
+ source_parent_dir = source_file.parts[-2]
197
+
198
+ if DATE_RE.match(source_parent_dir):
199
+ if len(source_parent_dir) > 10:
200
+ image_path = args.path / image_year / f'{image_date}{source_parent_dir[10:]}'
201
+ else:
202
+ image_path = args.path / image_year / f'{image_date} - {source_parent_dir}'
203
+
204
+ source_file = safe_rename(args, source_file, image_path / source_file.name)
205
+
206
+ ################################################################################
207
+
208
+ def main():
209
+ """ Entry point """
210
+
211
+ args = parse_command_line()
212
+
213
+ # Disable the maximum image size in PIL
214
+
215
+ Image.MAX_IMAGE_PIXELS = None
216
+
217
+ # Find matching files in the source tree
218
+
219
+ print(f'Searching {args.path} with extension matching {", ".join(FILE_TYPES)}')
220
+
221
+ all_matches = args.path.glob('**/*')
222
+
223
+ matches = [file for file in all_matches if file.suffix.lower() in FILE_TYPES and file.is_file()]
224
+
225
+ print(f'Found {len(matches)} matching files')
226
+
227
+ for source_file in matches:
228
+ if '.stversions' not in source_file.parts:
229
+ fix_file(args, source_file)
230
+
231
+ ################################################################################
232
+
233
+ def phototidier():
234
+ """Entry point"""
235
+
236
+ try:
237
+ main()
238
+
239
+ except KeyboardInterrupt:
240
+ sys.exit(1)
241
+
242
+ except BrokenPipeError:
243
+ sys.exit(2)
244
+
245
+ ################################################################################
246
+
247
+ if __name__ == '__main__':
248
+ phototidier()