skilleter-thingy 0.0.25__tar.gz → 0.0.27__tar.gz

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 (71) hide show
  1. {skilleter_thingy-0.0.25/skilleter_thingy.egg-info → skilleter_thingy-0.0.27}/PKG-INFO +1 -2
  2. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/pyproject.toml +1 -2
  3. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/git_review.py +11 -5
  4. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/readable.py +2 -2
  5. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27/skilleter_thingy.egg-info}/PKG-INFO +1 -2
  6. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy.egg-info/SOURCES.txt +0 -1
  7. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy.egg-info/requires.txt +0 -1
  8. skilleter_thingy-0.0.25/skilleter_thingy/imagedupe.py +0 -281
  9. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/LICENSE +0 -0
  10. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/README.md +0 -0
  11. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/setup.cfg +0 -0
  12. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/__init__.py +0 -0
  13. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/addpath.py +0 -0
  14. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/borger.py +0 -0
  15. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/colour.py +0 -0
  16. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/console_colours.py +0 -0
  17. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/dc_curses.py +0 -0
  18. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/dc_defaults.py +0 -0
  19. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/dc_util.py +0 -0
  20. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/dircolors.py +0 -0
  21. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/diskspacecheck.py +0 -0
  22. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/docker.py +0 -0
  23. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/docker_purge.py +0 -0
  24. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/ffind.py +0 -0
  25. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/files.py +0 -0
  26. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/ggit.py +0 -0
  27. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/ggrep.py +0 -0
  28. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/git.py +0 -0
  29. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/git2.py +0 -0
  30. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/git_br.py +0 -0
  31. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/git_ca.py +0 -0
  32. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/git_cleanup.py +0 -0
  33. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/git_co.py +0 -0
  34. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/git_common.py +0 -0
  35. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/git_hold.py +0 -0
  36. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/git_mr.py +0 -0
  37. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/git_parent.py +0 -0
  38. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/git_update.py +0 -0
  39. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/git_wt.py +0 -0
  40. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/gitcmp_helper.py +0 -0
  41. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/gitlab.py +0 -0
  42. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/gitprompt.py +0 -0
  43. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/gl.py +0 -0
  44. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/gphotosync.py +0 -0
  45. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/linecount.py +0 -0
  46. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/logger.py +0 -0
  47. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/moviemover.py +0 -0
  48. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/path.py +0 -0
  49. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/photodupe.py +0 -0
  50. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/phototidier.py +0 -0
  51. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/popup.py +0 -0
  52. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/process.py +0 -0
  53. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/py_audit.py +0 -0
  54. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/remdir.py +0 -0
  55. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/rmdupe.py +0 -0
  56. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/rpylint.py +0 -0
  57. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/run.py +0 -0
  58. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/splitpics.py +0 -0
  59. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/strreplace.py +0 -0
  60. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/sysmon.py +0 -0
  61. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/tfm.py +0 -0
  62. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/tfm_pane.py +0 -0
  63. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/tfparse.py +0 -0
  64. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/tidy.py +0 -0
  65. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/trimpath.py +0 -0
  66. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/window_rename.py +0 -0
  67. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/xchmod.py +0 -0
  68. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy/yamlcheck.py +0 -0
  69. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy.egg-info/dependency_links.txt +0 -0
  70. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy.egg-info/entry_points.txt +0 -0
  71. {skilleter_thingy-0.0.25 → skilleter_thingy-0.0.27}/skilleter_thingy.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: skilleter_thingy
3
- Version: 0.0.25
3
+ Version: 0.0.27
4
4
  Summary: A collection of useful utilities, mainly aimed at making Git more friendly
5
5
  Author-email: John Skilleter <john@skilleter.org.uk>
6
6
  Project-URL: Home, https://skilleter.org.uk
@@ -18,7 +18,6 @@ Requires-Dist: pyaml
18
18
  Requires-Dist: pygit2
19
19
  Requires-Dist: python-dateutil
20
20
  Requires-Dist: requests
21
- Requires-Dist: numpy
22
21
 
23
22
  # Thingy
24
23
 
@@ -7,7 +7,7 @@ name = "skilleter_thingy"
7
7
 
8
8
  # Version must be incremented to install updated Thingy
9
9
 
10
- version = "0.0.25"
10
+ version = "0.0.27"
11
11
 
12
12
  authors = [
13
13
  {name="John Skilleter", email="john@skilleter.org.uk"},
@@ -34,7 +34,6 @@ dependencies = [
34
34
  "pygit2",
35
35
  "python-dateutil",
36
36
  "requests",
37
- "numpy",
38
37
  ]
39
38
 
40
39
  [project.urls]
@@ -514,6 +514,12 @@ class GitReview():
514
514
  def show_file_list(self):
515
515
  """ Draw the current page of the file list """
516
516
 
517
+ def format_change(prefix, value):
518
+ """If value is 0 just return it as a string, otherwise apply the prefix and
519
+ return it (e.g. '+' or '-')"""
520
+
521
+ return f'{prefix}{value}' if value else '0'
522
+
517
523
  for ypos in range(0, self.file_list_h):
518
524
 
519
525
  normal_colour = curses.color_pair(COLOUR_NORMAL)
@@ -535,13 +541,13 @@ class GitReview():
535
541
  # Diff stats, with or without non-whitespace changes
536
542
 
537
543
  if self.show_none_whitespace_stats:
538
- added = f'+{current_file["non-ws added"]}'
539
- deleted = f'-{current_file["non-ws deleted"]}'
544
+ added = format_change('+', current_file["non-ws added"])
545
+ deleted = format_change('-', current_file["non-ws deleted"])
540
546
  else:
541
- added = f'+{current_file["added"]}'
542
- deleted = f'-{current_file["deleted"]}'
547
+ added = format_change('+', current_file["added"])
548
+ deleted = format_change('-', current_file["deleted"])
543
549
 
544
- status = f'{current_file["status"]} {added:>4}/{deleted:>4}'
550
+ status = f'{current_file["status"]} {deleted:>4}/{added:>4}'
545
551
 
546
552
  abspath = os.path.join(self.working_tree_dir, filename)
547
553
 
@@ -29,8 +29,8 @@ from skilleter_thingy import files
29
29
  TF_OBJECTS_CHANGED_END = 'Terraform detected the following changes made outside of Terraform since the last "terraform apply":'
30
30
  TF_IGNORE_MSG = 'Unless you have made equivalent changes to your configuration, or ignored the relevant attributes using ignore_changes, the following plan may include actions to undo or respond to these changes.'
31
31
 
32
- TF_REFRESHING_AND_READING = re.compile(r'.*: (?:Refreshing state\.\.\.|Reading\.\.\.|Read complete after ).*')
33
- TF_FINDING_AND_INSTALLING = re.compile(r'- (?:Finding .*[.]{3}|Installing .*[.]{3}|Installed .*)')
32
+ TF_REFRESHING_AND_READING = re.compile(r'.*: (?:Refreshing state\.\.\.|Reading\.\.\.|Read complete after |Preparing import\.\.\.).*')
33
+ TF_FINDING_AND_INSTALLING = re.compile(r'- (?:Finding .*[.]{3}|Installing .*[.]{3}|Installed .*)')
34
34
 
35
35
  TF_HAS_CHANGED = re.compile(r' # .* has changed')
36
36
  TF_READ_DURING_APPLY = re.compile(r' +# [-a-z_.0-9\[\]]+ will be read during apply')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: skilleter_thingy
3
- Version: 0.0.25
3
+ Version: 0.0.27
4
4
  Summary: A collection of useful utilities, mainly aimed at making Git more friendly
5
5
  Author-email: John Skilleter <john@skilleter.org.uk>
6
6
  Project-URL: Home, https://skilleter.org.uk
@@ -18,7 +18,6 @@ Requires-Dist: pyaml
18
18
  Requires-Dist: pygit2
19
19
  Requires-Dist: python-dateutil
20
20
  Requires-Dist: requests
21
- Requires-Dist: numpy
22
21
 
23
22
  # Thingy
24
23
 
@@ -35,7 +35,6 @@ skilleter_thingy/gitlab.py
35
35
  skilleter_thingy/gitprompt.py
36
36
  skilleter_thingy/gl.py
37
37
  skilleter_thingy/gphotosync.py
38
- skilleter_thingy/imagedupe.py
39
38
  skilleter_thingy/linecount.py
40
39
  skilleter_thingy/logger.py
41
40
  skilleter_thingy/moviemover.py
@@ -6,4 +6,3 @@ pyaml
6
6
  pygit2
7
7
  python-dateutil
8
8
  requests
9
- numpy
@@ -1,281 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Hash photos to find closely-similar images and report them"""
3
-
4
- # TODO: Configurable hash type and stored in pickle data
5
- # TODO: Option to pickle duplicates
6
- # TODO: Check for files that are no longer present after scanning and remove them from the data
7
- # TODO: Display duplicates - borrow code from imagedup
8
- # TODO: Note that if a==b, we get reports for a==b and b==a so we need to do some filtering and decide on what's likely to be an original and what is a duplicate
9
- # TODO: Maybe store file date, size and image dimensions in 'updated' for better comparisons
10
- # TODO: Pickle data named for directories
11
- # TODO: Use database instead of pickling - very slow to load big pickles - would facilitate scanning/checking specific directories
12
- # TODO: If directories specified, the only scan and compare within those directories (but keep everything in pickle file (yes, it will get biggerer, and biggererer)
13
-
14
- import threading
15
- import queue
16
- import sys
17
- import os
18
- import pickle
19
- import argparse
20
- from pathlib import PurePath
21
- from typing import List, Union
22
-
23
- from imagededup.methods import PHash
24
-
25
- import matplotlib.gridspec as gridspec
26
- import matplotlib.pyplot as plt
27
- from matplotlib import figure
28
-
29
- import numpy as np
30
- from PIL import Image
31
-
32
- ################################################################################
33
-
34
- PICKLE_FILE = 'imagedupe.pickle'
35
-
36
- IMAGE_EXT_LIST = ('.jpeg', '.jpg', '.png', '.bmp', '.mpo', '.ppm', '.tif', '.tiff', '.gif', '.svg', '.pgm', '.pbm', '.webp')
37
-
38
- PICKLE_VERSION = 1
39
-
40
- NUM_THREADS = 24
41
-
42
- ################################################################################
43
-
44
- def queue_files_to_hash(file_queue, directories):
45
- """Read all the specfied directories queue every file therein"""
46
-
47
- # Walk each directory tree
48
-
49
- for directory in directories:
50
- print(f'Scanning directory tree {directory}')
51
-
52
- for root, _, files in os.walk(directory):
53
- print(f'Scanning directory {root}')
54
-
55
- for file in files:
56
- file_queue.put(os.path.join(root, file))
57
-
58
- ################################################################################
59
-
60
- def hasher_thread(method, file_queue, hash_queue, hashes, updated):
61
- """Thread - reads a file from the queue and, if it is an unhashed or updated image
62
- calculate the hash and post it and the modification time on the updated queue"""
63
-
64
- while not file_queue.empty():
65
- filepath = file_queue.get()
66
-
67
- fileext = os.path.splitext(filepath)[1]
68
- mod_time = os.path.getmtime(filepath)
69
-
70
- # If the file type is an image and the file hasn't been hashed or the modification time has changed
71
- # then save the hash and the modification time
72
-
73
- if fileext.lower() in IMAGE_EXT_LIST and (filepath not in hashes or mod_time != updated[filepath]):
74
- # Calculate the hash and store path, dimensions and file size under the hash entry in the hashes table
75
-
76
- print(f'Calculating hash for {filepath}')
77
- encoding = method.encode_image(image_file=filepath)
78
-
79
- if encoding:
80
- hash_queue.put({'filepath': filepath, 'encoding': encoding, 'updated': mod_time})
81
- else:
82
- print(f'Invalid image {filepath}')
83
-
84
- file_queue.task_done()
85
-
86
- ################################################################################
87
-
88
- def hash_directories(directories, method, hashes, updated):
89
- """Scan for new files and calculate their hashes"""
90
-
91
- # Create the I/O queues
92
-
93
- file_queue = queue.Queue()
94
- hash_queue = queue.Queue()
95
-
96
- # Queue the list of files to hash
97
-
98
- queue_files_to_hash(file_queue, directories)
99
-
100
- # Start the threads hashing away
101
-
102
- thread_list = []
103
- for _ in range(NUM_THREADS):
104
- thread = threading.Thread(target=hasher_thread, daemon=True, args=(method, file_queue, hash_queue, hashes, updated))
105
- thread.start()
106
- thread_list.append(thread)
107
-
108
- # Wait for the threads to stop working
109
-
110
- print('Waiting for threads to terminate')
111
-
112
- for thread in thread_list:
113
- thread.join()
114
-
115
- # Process the results
116
-
117
- while not hash_queue.empty():
118
- entry = hash_queue.get()
119
-
120
- filepath = entry['filepath']
121
- hashes[filepath] = entry['encoding']
122
- updated[filepath] = entry['updated']
123
-
124
- ################################################################################
125
-
126
- def plot_images(
127
- orig: str,
128
- image_list: List,
129
- scores: bool = False,
130
- outfile: str = None,
131
- ) -> None:
132
- """
133
- Plotting function for plot_duplicates() defined below.
134
-
135
- Args:
136
- orig: filename for which duplicates are to be plotted.
137
- image_list: List of duplicate filenames, could also be with scores (filename, score).
138
- scores: Whether only filenames are present in the image_list or scores as well.
139
- outfile: Name of the file to save the plot.
140
- """
141
-
142
- def formatter(val: Union[int, np.float32]):
143
- """
144
- For printing floats only upto 3rd precision. Ints are unchanged.
145
- """
146
- if isinstance(val, np.float32):
147
- return f'{val:.3f}'
148
-
149
- return val
150
-
151
- n_ims = len(image_list)
152
- ncols = 4 # fixed for a consistent layout
153
- nrows = int(np.ceil(n_ims / ncols)) + 1
154
- fig = figure.Figure(figsize=(10, 14))
155
-
156
- gs = gridspec.GridSpec(nrows=nrows, ncols=ncols)
157
- ax = plt.subplot(
158
- gs[0, 1:3]
159
- ) # Always plot the original image in the middle of top row
160
- ax.imshow(Image.open(orig))
161
- ax.set_title(f'Original Image: {format(orig)}')
162
- ax.axis('off')
163
-
164
- for i in range(0, n_ims):
165
- row_num = (i // ncols) + 1
166
- col_num = i % ncols
167
-
168
- ax = plt.subplot(gs[row_num, col_num])
169
- if scores:
170
- ax.imshow(Image.open(image_list[i][0]))
171
- val = formatter(image_list[i][1])
172
- title = ' '.join([image_list[i][0], f'({val})'])
173
- else:
174
- ax.imshow(Image.open(image_list[i]))
175
- title = image_list[i]
176
-
177
- ax.set_title(title, fontsize=6)
178
- ax.axis('off')
179
- gs.tight_layout(fig)
180
-
181
- if outfile:
182
- plt.savefig(outfile)
183
-
184
- plt.show()
185
- plt.close()
186
-
187
- ################################################################################
188
-
189
- def main():
190
- """Read the hashes and report duplicates in a vaguely civilised way"""
191
-
192
- # Hashing and comparison method
193
-
194
- method = PHash()
195
-
196
- # Handle command line arguments
197
-
198
- parser = argparse.ArgumentParser(description='Search for similar images')
199
- parser.add_argument('--no-scan', action='store_true', help='Use pickled scan data without updating it')
200
- parser.add_argument('--no-compare', action='store_true', help='Use pickled comparison data without updating it')
201
- parser.add_argument('--show', action='store_true', help='Show duplicate images')
202
- parser.add_argument('directories', nargs='*', action='store', help='Directories to search')
203
-
204
- args = parser.parse_args()
205
-
206
- breakpoint()
207
-
208
- if not args.no_scan and not args.directories:
209
- print('You must be specify at least one directory in order to perform a scan')
210
- sys.exit(1)
211
-
212
- # We pickle the current set of files, hashes and comparisons
213
-
214
- try:
215
- print('Loading cached data')
216
-
217
- with open(PICKLE_FILE, 'rb') as pickles:
218
- data = pickle.load(pickles)
219
-
220
- if data['version'] != PICKLE_VERSION:
221
- print(f'WARNING: Current version is {PICKLE_VERSION} but saved data is from version {data["version"]}. Interesting things could happen....')
222
-
223
- hashes = data['hashes']
224
- updated = data['updated']
225
- duplicates = data.get('duplicates', None)
226
-
227
- except (FileNotFoundError, EOFError):
228
- if args.no_scan:
229
- print('ERROR: Cannot use no-scan option as no cached scan data is available')
230
- sys.exit(1)
231
-
232
- hashes = {}
233
- updated = {}
234
- duplicates = {}
235
-
236
- if args.no_compare and not duplicates:
237
- print('ERROR: Cannot use no-compare option as no cached comparison data is available')
238
- sys.exit(1)
239
-
240
- if not args.no_scan:
241
- # Scan for new values and calculate hashes
242
-
243
- hash_directories(method, args.directories, hashes, updated)
244
-
245
- if not args.no_compare:
246
- # Look for duplicates
247
-
248
- duplicates = method.find_duplicates(encoding_map=hashes)
249
-
250
- # Pickle the updated results
251
-
252
- with open(PICKLE_FILE, 'wb') as pickles:
253
- dupe_data = {'hashes': hashes, 'updated': updated, 'version': PICKLE_VERSION, 'duplicates': duplicates}
254
- pickle.dump(dupe_data, pickles)
255
-
256
- # Report them
257
-
258
- for entry in duplicates:
259
- if duplicates[entry]:
260
- print(f'{entry}: {duplicates[entry]}')
261
- if args.show:
262
- plot_duplicates(entry, duplicates[entry])
263
-
264
- ################################################################################
265
-
266
- def photodupe():
267
- """Entry point"""
268
-
269
- try:
270
- main()
271
-
272
- except KeyboardInterrupt:
273
- sys.exit(1)
274
-
275
- except BrokenPipeError:
276
- sys.exit(2)
277
-
278
- ################################################################################
279
-
280
- if __name__ == '__main__':
281
- photodupe()