-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathsequencelib.py
237 lines (209 loc) · 8.45 KB
/
sequencelib.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
"""
sequencelib by henry foster, henry@toadstorm.com
A library of tools for collating and manipulating sequences of files.
"""
import os
import re
import decimal
import copy
SEQUENCE_REGEX = '^(\D+[._])(\d+\.?\d*)([._]?\w*)\.(\w{2,5})$'
# capture groups: (prefix) (number) (optional suffix) (file extension)
def drange(x, y, jump):
"""
like range(), but with Decimal objects instead.
:param x: the start of the range.
:param y: the end of the range.
:param jump: the step.
:return: a Generator that returns each value in the range, one at a time.
"""
while x < y:
yield float(x)
x += decimal.Decimal(jump)
class File(object):
"""
A generic class to contain a file path and a number (the sequence order)
"""
def __init__(self, file):
self.number = 0
self.path = file
match = re.match(SEQUENCE_REGEX, file)
if match:
if match.groups()[1]:
num = match.groups()[1].lstrip('0')
self.number = decimal.Decimal(num)
class Sequence(object):
"""
A class containing a list of File objects, and the prefix/suffix/extension
for matching with other sequences or files.
"""
def __init__(self, files=None):
self._files = list() # all File objects in this sequence
self.prefix = '' # the prefix (before the frame number)
self.suffix = '' # the suffix (after the frame number)
self.extension = '' # the file extension
self.padding = 0 # the framepadding
self.directory = '' # the base directory this sequence lives in
if files:
# if the user provided a single file instead of a list of files, just convert it
if isinstance(files, str):
files = [files]
self.directory = os.path.dirname(files[0])
# parse the first file in the list to get prefix, suffix, extension, padding
match = re.match(SEQUENCE_REGEX, os.path.basename(files[0]))
if match:
if match.groups()[0]:
self.prefix = match.groups()[0]
if match.groups()[1]:
# check to see if there's padding in there.
zeroes = len(match.groups()[1]) - len(match.groups()[1].lstrip('0'))
self.padding = zeroes
if match.groups()[2]:
self.suffix = match.groups()[2]
if match.groups()[3]:
self.extension = match.groups()[3]
# now convert each file into a File object and add it to our internal _files list
for f in files:
file_obj = File(f)
self._files.append(file_obj)
# sort by frame number
self._files = sorted(self._files, key=lambda x: x.number)
def append(self, filepath):
"""
add a file (by path) to this Sequence.
:param filepath: the full path on disk to the file.
:return: None
"""
file_obj = File(filepath)
# screen the list of files first so we can't have duplicates.
if self._files:
for file in self._files:
if file.path == filepath:
return
self._files.append(file_obj)
self._files = sorted(self._files, key=lambda x: x.number)
def remove(self, filepath):
"""
remove a file (by path) from this Sequence.
:param filepath: the path to remove, or optionally, the File object to remove.
:return: None
"""
if isinstance(filepath, File):
filepath = File.path
if self._files:
for file in self._files:
if file.path == filepath:
self._files.remove(file)
break
def files(self):
"""
return a friendly list of file paths as strings (rather than File objects as in self._files)
:return: a list of file paths as strings, in order.
"""
if self._files:
return [f.path for f in self._files]
return None
def file_match(self, file):
"""
check to see if the given file matches this sequence.
:param file: the file to test.
:return: True if the file belongs in this sequence.
"""
match = re.match(SEQUENCE_REGEX, file)
if match:
if match.groups():
if match.groups()[0] == self.prefix and match.groups()[2] == self.suffix and match.groups()[3] == self.extension:
return True
return False
def find_missing_frames(self, step=1, start=None, end=None):
"""
Ensure that the sequence is contiguous.
:param step: the step between frames (defaults to 1)
:param start: the start of the sequence. default is whatever the first detected frame is.
:param end: the end of the sequence. default is whatever the last detected frame is.
:return: a list of missing filenames, if any exist.
"""
step = decimal.Decimal(step)
if start is None:
start = self._files[0].number
if end is None:
end = self._files[-1].number
# copy our internal _files list to a temporary duplicate so we can remove from it as we test for files
test_files = copy.deepcopy(self._files)
missing_frames = list()
# for each potential frame in our list, verify that a File with an identical number exists in the Sequence
for frame in drange(start, end+step, step):
found = False
for file in test_files:
if file.number == frame:
found = True
test_files.remove(file)
break
if not found:
missing_frames.append(decimal.Decimal(frame))
if missing_frames:
# create full file paths for these if possible and then return them so they're human-readable.
missing_files = list()
for frame in missing_frames:
file = self.prefix + str(frame).zfill(self.padding) + self.suffix + '.' + self.extension
filepath = os.path.join(self.directory, file).replace('\\', '/')
missing_files.append(filepath)
return missing_files
return None
def debug(self):
"""
print a bunch of crap about the sequence.
:return: None
"""
print('Sequence has {} files.'.format(len(self._files)))
print('Prefix: {}'.format(self.prefix))
print('Suffix: {}'.format(self.suffix))
print('Extension: {}'.format(self.extension))
print('All files: {}'.format(self.files()))
print('Missing files: {}'.format(self.find_missing_frames()))
def is_file_valid(file):
"""
test if a file could actually be a sequence.
:param file: the file path to test.
:return: True if the file could be part of a sequence.
"""
match = re.match(SEQUENCE_REGEX, file)
if match:
if match.groups()[1]:
return True
return False
def find_sequences(path, extensions=None):
"""
given a path on disk, find all possible file sequences.
:param path: the path to search.
:param extensions: if provided, a list of file extensions to mask by.
:return: a list of Sequence objects.
"""
sequences = list()
all_files = os.listdir(path)
if not all_files:
return None
all_files = [f for f in all_files if not os.path.isdir(os.path.join(path,f))]
if not all_files:
return None
if extensions:
if isinstance(extensions, str):
extensions = [extensions]
extensions = [f.strip('.') for f in extensions]
all_files = [f for f in os.listdir(path) if os.path.splitext(f)[-1].strip('.') in extensions]
if not all_files:
return None
# collate found files into sequences if they fit the regex,
for file in all_files:
found_match = False
if sequences:
for seq in sequences:
if seq.file_match(file):
found_match = True
seq.append(os.path.join(path, file).replace('\\', '/'))
break
if not found_match:
# generate a new sequence if this is a sequenceable file.
if is_file_valid(file):
new_seq = Sequence(os.path.join(path, file).replace('\\', '/'))
sequences.append(new_seq)
return sequences