tictacsync 0.1a14__py3-none-any.whl → 1.4.4b0__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 tictacsync might be problematic. Click here for more details.
- tictacsync/device_scanner.py +362 -169
- tictacsync/entry.py +240 -135
- tictacsync/mamconf.py +157 -0
- tictacsync/mamdav.py +642 -0
- tictacsync/mamreap.py +481 -0
- tictacsync/mamsync.py +343 -0
- tictacsync/multi2polywav.py +21 -14
- tictacsync/timeline.py +1126 -442
- tictacsync/yaltc.py +895 -1067
- tictacsync-1.4.4b0.dist-info/METADATA +118 -0
- tictacsync-1.4.4b0.dist-info/RECORD +16 -0
- tictacsync-1.4.4b0.dist-info/entry_points.txt +7 -0
- tictacsync/LTCcheck.py +0 -394
- tictacsync-0.1a14.dist-info/METADATA +0 -96
- tictacsync-0.1a14.dist-info/RECORD +0 -13
- tictacsync-0.1a14.dist-info/entry_points.txt +0 -4
- {tictacsync-0.1a14.dist-info → tictacsync-1.4.4b0.dist-info}/LICENSE +0 -0
- {tictacsync-0.1a14.dist-info → tictacsync-1.4.4b0.dist-info}/WHEEL +0 -0
- {tictacsync-0.1a14.dist-info → tictacsync-1.4.4b0.dist-info}/top_level.txt +0 -0
tictacsync/device_scanner.py
CHANGED
|
@@ -16,7 +16,10 @@ M4V SVI 3GP 3G2 MXF ROQ NSV FLV F4V F4P F4A F4B 3GP AA AAC AAX ACT AIFF ALAC
|
|
|
16
16
|
AMR APE AU AWB DSS DVF FLAC GSM IKLAX IVS M4A M4B M4P MMF MP3 MPC MSV NMF
|
|
17
17
|
OGG OGA MOGG OPUS RA RM RAW RF64 SLN TTA VOC VOX WAV WMA WV WEBM 8SVX CDA MOV AVI BWF""".split()
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
audio_ext = 'aiff wav mp3'.split()
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
import ffmpeg, os, sys, shutil
|
|
20
23
|
from os import listdir
|
|
21
24
|
from os.path import isfile, join, isdir
|
|
22
25
|
from collections import namedtuple
|
|
@@ -25,7 +28,7 @@ from pprint import pformat
|
|
|
25
28
|
# from collections import defaultdict
|
|
26
29
|
from loguru import logger
|
|
27
30
|
# import pathlib, os.path
|
|
28
|
-
import sox, tempfile
|
|
31
|
+
import sox, tempfile, platformdirs, filecmp
|
|
29
32
|
# from functools import reduce
|
|
30
33
|
from rich import print
|
|
31
34
|
from itertools import groupby
|
|
@@ -33,9 +36,17 @@ from itertools import groupby
|
|
|
33
36
|
# import distance
|
|
34
37
|
try:
|
|
35
38
|
from . import multi2polywav
|
|
39
|
+
from . import mamsync
|
|
40
|
+
from . import mamconf
|
|
41
|
+
from . import yaltc
|
|
36
42
|
except:
|
|
37
43
|
import multi2polywav
|
|
44
|
+
import mamsync
|
|
45
|
+
import mamconf
|
|
46
|
+
import yaltc
|
|
38
47
|
|
|
48
|
+
MCCDIR = 'SyncedMulticamClips'
|
|
49
|
+
SYNCEDFOLDER = 'SyncedMedia'
|
|
39
50
|
|
|
40
51
|
# utility for accessing pathnames
|
|
41
52
|
def _pathname(tempfile_or_path):
|
|
@@ -53,40 +64,89 @@ def print_grby(grby):
|
|
|
53
64
|
print('\ngrouped by %s:'%key)
|
|
54
65
|
for e in keylist:
|
|
55
66
|
print(' ', e)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
@dataclass
|
|
68
|
+
class Tracks:
|
|
69
|
+
# track numbers start at 1 for first track (as needed by sox,1 Based Index)
|
|
70
|
+
ttc: int # track number of TicTacCode signal
|
|
71
|
+
unused: list # of unused tracks
|
|
72
|
+
stereomics: list # of stereo mics track tuples (Lchan#, Rchan#)
|
|
73
|
+
mix: list # of mixed tracks, if a pair, order is L than R
|
|
74
|
+
others: list #of all other tags: (tag, track#) tuples
|
|
75
|
+
rawtrx: list # list of strings read from file
|
|
76
|
+
error_msg: str # 'None' if none
|
|
77
|
+
lag_values: list # list of lags in ms, entry is None if not specified.
|
|
78
|
+
# UTC_timestamp: str # to the nearest minute ISO 8601 date and time e.g.: "2007-04-05T14:30Z"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class Device:
|
|
83
|
+
UID: int
|
|
84
|
+
folder: Path # media's parent folder
|
|
85
|
+
name: str
|
|
86
|
+
dev_type: str # CAM or REC
|
|
87
|
+
n_chan: int
|
|
88
|
+
ttc: int # zero based index?
|
|
89
|
+
tracks: Tracks
|
|
90
|
+
sampling_freq: float # fps if cam
|
|
91
|
+
def __hash__(self):
|
|
92
|
+
return self.UID
|
|
93
|
+
def __eq__(self, other):
|
|
94
|
+
return self.UID == other
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class Media:
|
|
98
|
+
"""A custom data type that represents data for a media file.
|
|
99
|
+
"""
|
|
100
|
+
path: Path
|
|
101
|
+
device: Device
|
|
102
|
+
|
|
103
|
+
def media_at_path(input_structure, p):
|
|
104
|
+
# return Media object for mediafile using ffprobe
|
|
105
|
+
dev_UID, dt, sf = get_device_ffprobe_UID(p)
|
|
106
|
+
dev_name = None
|
|
107
|
+
logger.debug('ffprobe dev_UID:%s dt:%s sf:%s'%(dev_UID, dt,sf))
|
|
108
|
+
if input_structure == 'ordered':
|
|
109
|
+
dev_name = p.parent.name
|
|
110
|
+
if dev_UID is None:
|
|
111
|
+
dev_UID = hash(dev_name)
|
|
112
|
+
if dt == 'CAM':
|
|
113
|
+
streams = ffmpeg.probe(p)['streams']
|
|
114
|
+
audio_streams = [
|
|
115
|
+
stream
|
|
116
|
+
for stream
|
|
117
|
+
in streams
|
|
118
|
+
if stream['codec_type']=='audio'
|
|
119
|
+
]
|
|
120
|
+
if len(audio_streams) > 1:
|
|
121
|
+
print('\nfor [gold1]%s[/gold1], ffprobe gave multiple audio streams, quitting.'%p)
|
|
67
122
|
quit()
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
123
|
+
# raise Exception('ffprobe gave multiple audio streams?')
|
|
124
|
+
if len(audio_streams) == 0:
|
|
125
|
+
print('ffprobe gave no audio stream for [gold1]%s[/gold1], quitting.'%p)
|
|
126
|
+
quit()
|
|
127
|
+
# raise Exception('ffprobe gave no audio stream for %s, quitting'%p)
|
|
128
|
+
audio_str = audio_streams[0]
|
|
129
|
+
n = audio_str['channels']
|
|
130
|
+
# pprint(ffmpeg.probe(p))
|
|
131
|
+
else:
|
|
132
|
+
n = sox.file_info.channels(_pathname(p)) # eg 2
|
|
133
|
+
logger.debug('for file %s dev_UID established %s'%(p.name, dev_UID))
|
|
134
|
+
device = Device(UID=dev_UID, folder=p.parent, name=dev_name, dev_type=dt,
|
|
135
|
+
n_chan=n, ttc=None, sampling_freq=sf, tracks=None)
|
|
136
|
+
logger.debug('for path: %s, device:%s'%(p,device))
|
|
137
|
+
return Media(p, device)
|
|
78
138
|
|
|
79
139
|
def get_device_ffprobe_UID(file):
|
|
80
140
|
"""
|
|
81
141
|
Tries to find an unique hash integer identifying the device that produced
|
|
82
142
|
the file based on the string inside ffprobe metadata without any
|
|
83
|
-
reference to date
|
|
143
|
+
reference to date, location, length or time. Find out with ffprobe the type
|
|
84
144
|
of device: CAM or REC for videocamera or audio recorder.
|
|
85
145
|
|
|
86
146
|
Device UIDs are used later in Montage._get_concatenated_audiofile_for()
|
|
87
147
|
for grouping each audio or video clip along its own timeline track.
|
|
88
148
|
|
|
89
|
-
Returns a tuple: (UID, CAM|REC)
|
|
149
|
+
Returns a tuple: (UID, CAM|REC, sampling_freq)
|
|
90
150
|
|
|
91
151
|
If an ffmpeg.Error occurs, returns (None, None)
|
|
92
152
|
if no UID is found, but device type is identified, returns (None, CAM|REC)
|
|
@@ -99,63 +159,66 @@ def get_device_ffprobe_UID(file):
|
|
|
99
159
|
except ffmpeg.Error as e:
|
|
100
160
|
print('ffmpeg.probe error')
|
|
101
161
|
print(e.stderr, file)
|
|
102
|
-
return None, None
|
|
162
|
+
return None, None #-----------------------------------------------------
|
|
103
163
|
# fall back to folder name
|
|
164
|
+
logger.debug('ffprobe %s'%probe)
|
|
104
165
|
streams = probe['streams']
|
|
166
|
+
video_streams = [st for st in streams if st['codec_type'] == 'video']
|
|
167
|
+
audio_streams = [st for st in streams if st['codec_type'] == 'audio']
|
|
168
|
+
if len(video_streams) > 1:
|
|
169
|
+
print('\nmore than one video stream for %s... quitting'%file)
|
|
170
|
+
quit()
|
|
171
|
+
if len(audio_streams) != 1:
|
|
172
|
+
print('\nnbr of audio stream for %s not 1 ... quitting'%file)
|
|
173
|
+
quit()
|
|
105
174
|
codecs = [stream['codec_type'] for stream in streams]
|
|
106
|
-
|
|
175
|
+
# cameras have two streams: video AND audio
|
|
176
|
+
device_type = 'CAM' if len(video_streams) == 1 else 'REC'
|
|
177
|
+
if device_type == 'CAM':
|
|
178
|
+
sampling_freq = eval(video_streams[0]['r_frame_rate'])
|
|
179
|
+
else:
|
|
180
|
+
sampling_freq = float(audio_streams[0]['sample_rate'])
|
|
107
181
|
format_dict = probe['format'] # all files should have this
|
|
108
182
|
if 'tags' in format_dict:
|
|
109
183
|
probe_string = pformat(format_dict['tags'])
|
|
110
184
|
probe_lines = [l for l in probe_string.split('\n')
|
|
111
185
|
if '_time' not in l
|
|
112
186
|
and 'time_' not in l
|
|
187
|
+
and 'location' not in l
|
|
113
188
|
and 'date' not in l ]
|
|
114
189
|
# this removes any metadata related to the file
|
|
115
190
|
# but keeps metadata related to the device
|
|
191
|
+
logger.debug('probe_lines %s'%probe_lines)
|
|
116
192
|
UID = hash(''.join(probe_lines))
|
|
117
193
|
else:
|
|
118
194
|
UID = None
|
|
195
|
+
if UID == 0: # empty probe_lines from Audacity ?!?
|
|
196
|
+
UID = None
|
|
119
197
|
logger.debug('ffprobe_UID is: %s'%UID)
|
|
120
|
-
return UID, device_type
|
|
121
|
-
|
|
198
|
+
return UID, device_type, sampling_freq
|
|
122
199
|
|
|
123
200
|
class Scanner:
|
|
124
201
|
"""
|
|
125
202
|
Class that encapsulates scanning of the directory given as CLI argument.
|
|
126
|
-
Depending on the
|
|
127
|
-
structure (or not). Build a list of media files found and a
|
|
128
|
-
indentify uniquely the device used to record each media file.
|
|
203
|
+
Depending on the input_structure detected (loose|ordered), enforce
|
|
204
|
+
some directory structure (or not). Build a list of media files found and a
|
|
205
|
+
try to indentify uniquely the device used to record each media file.
|
|
129
206
|
|
|
130
207
|
Attributes:
|
|
131
208
|
|
|
132
209
|
input_structure: string
|
|
133
|
-
|
|
134
|
-
Any of
|
|
210
|
+
Any of:
|
|
135
211
|
'loose'
|
|
136
212
|
all files audio + video are in top folder
|
|
137
|
-
'
|
|
213
|
+
'ordered'
|
|
138
214
|
eg for multicam on Davinci Resolve
|
|
215
|
+
input_structure is set in scan_media_and_build_devices_UID()
|
|
139
216
|
|
|
140
217
|
top_directory : string
|
|
218
|
+
String of path where to start searching for media files.
|
|
141
219
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
top_dir_has_multicam : bool
|
|
145
|
-
|
|
146
|
-
If top dir is folder structures AND more than on cam
|
|
147
|
-
|
|
148
|
-
devices_names : dict of str
|
|
149
|
-
|
|
150
|
-
more evocative names for each device, keys are same as
|
|
151
|
-
self.devices_UID_count
|
|
152
|
-
|
|
153
|
-
found_media_files: list of dicts
|
|
154
|
-
{
|
|
155
|
-
'path' : as is ,
|
|
156
|
-
'sample length' : as is
|
|
157
|
-
'dev' : Device namedtuple
|
|
158
|
-
}
|
|
220
|
+
found_media_files: list of dataclass Media instances encapsulating
|
|
221
|
+
the pathlibPath and the device (of Device dataclass).
|
|
159
222
|
"""
|
|
160
223
|
|
|
161
224
|
def __init__(
|
|
@@ -171,202 +234,332 @@ class Scanner:
|
|
|
171
234
|
self.found_media_files = []
|
|
172
235
|
self.stay_silent = stay_silent
|
|
173
236
|
|
|
174
|
-
|
|
175
237
|
def get_devices_number(self):
|
|
176
238
|
# how many devices have been found
|
|
177
|
-
return len(set([m
|
|
239
|
+
return len(set([m.device.UID for m in self.found_media_files]))
|
|
240
|
+
|
|
241
|
+
def get_devices(self):
|
|
242
|
+
return set([m.device for m in self.found_media_files])
|
|
243
|
+
|
|
244
|
+
def get_media_for_device(self, dev):
|
|
245
|
+
return [m for m in self.found_media_files if m.device == dev]
|
|
246
|
+
|
|
247
|
+
def CAM_numbers(self):
|
|
248
|
+
devices = [m.device for m in self.found_media_files]
|
|
249
|
+
CAMs = [d for d in devices if d.dev_type == 'CAM']
|
|
250
|
+
return len(set(CAMs))
|
|
178
251
|
|
|
179
|
-
def scan_media_and_build_devices_UID(self,
|
|
252
|
+
def scan_media_and_build_devices_UID(self, synced_root = None):
|
|
180
253
|
"""
|
|
181
|
-
Scans
|
|
254
|
+
Scans Scanner.top_directory recursively for files with known audio-video
|
|
182
255
|
extensions. For each file found, a device fingerprint is obtained from
|
|
183
256
|
their ffprobe result to ID the device used.
|
|
184
257
|
|
|
185
|
-
|
|
186
258
|
Also looked for are multifile recordings: files with the exact same
|
|
187
259
|
length. When done, calls
|
|
188
260
|
|
|
189
261
|
Returns nothing
|
|
190
262
|
|
|
191
|
-
Populates Scanner.found_media_files, a list of
|
|
192
|
-
{
|
|
193
|
-
'path' : as is ,
|
|
194
|
-
'sample length' : as is
|
|
195
|
-
'dev' : Device namedtuple
|
|
196
|
-
}
|
|
263
|
+
Populates Scanner.found_media_files, a list of Media objects
|
|
197
264
|
|
|
198
|
-
Sets input_structure = 'loose'|'
|
|
265
|
+
Sets Scanner.input_structure = 'loose'|'ordered'
|
|
199
266
|
|
|
200
267
|
"""
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
268
|
+
logger.debug(f'on entry synced_root: {synced_root}')
|
|
269
|
+
if synced_root != None: # mam mode
|
|
270
|
+
p = Path(platformdirs.user_data_dir('mamsync', 'plutz'))/mamconf.LOG_FILE
|
|
271
|
+
with open(p, 'r') as fh:
|
|
272
|
+
done = set(fh.read().split()) # sets of strings of abs path
|
|
273
|
+
logger.debug(f'done clips: {pformat(done)}')
|
|
274
|
+
files = Path(self.top_directory).rglob('*')
|
|
275
|
+
clip_paths = []
|
|
276
|
+
some_done = False
|
|
277
|
+
for raw_path in files:
|
|
278
|
+
if raw_path.suffix[1:] in av_file_extensions:
|
|
279
|
+
if SYNCEDFOLDER not in raw_path.parts: # SyncedMedia
|
|
280
|
+
if MCCDIR not in raw_path.parts: # SyncedMulticamClips
|
|
281
|
+
if '_ISO' not in [part[-4:] for part in raw_path.parts]: # exclude ISO wav files
|
|
282
|
+
if synced_root != None and str(raw_path) in done:
|
|
283
|
+
logger.debug(f'{raw_path} done')
|
|
284
|
+
some_done = True
|
|
285
|
+
continue
|
|
286
|
+
else:
|
|
287
|
+
clip_paths.append(raw_path)
|
|
288
|
+
if some_done:
|
|
289
|
+
print('Somme media files were already synced...')
|
|
290
|
+
logger.debug('found media files %s'%clip_paths)
|
|
291
|
+
if len(clip_paths) == 0:
|
|
292
|
+
print('No media found, bye.')
|
|
293
|
+
sys.exit(0)
|
|
294
|
+
# self.found_media_files = []
|
|
295
|
+
# self.input_structure = 'loose'
|
|
296
|
+
# return
|
|
297
|
+
parents = [p.parent for p in clip_paths]
|
|
298
|
+
logger.debug('found parents %s'%pformat(parents))
|
|
299
|
+
# True if all elements are identical
|
|
300
|
+
AV_files_have_same_parent = parents.count(parents[0]) == len(parents)
|
|
301
|
+
logger.debug('AV_files_have_same_parent %s'%AV_files_have_same_parent)
|
|
302
|
+
if AV_files_have_same_parent:
|
|
303
|
+
# all media (video + audio) are in a same folder, so this is loose
|
|
214
304
|
self.input_structure = 'loose'
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
print(' %s, quitting.\n'%self.top_directory)
|
|
218
|
-
quit()
|
|
219
|
-
# quit()
|
|
220
|
-
files = Path(self.top_directory).rglob('*.*')
|
|
221
|
-
paths = [
|
|
222
|
-
p
|
|
223
|
-
for p in files
|
|
224
|
-
if p.suffix[1:] in av_file_extensions
|
|
225
|
-
and 'SyncedMedia' not in p.parts
|
|
226
|
-
]
|
|
227
|
-
for p in paths:
|
|
228
|
-
new_media = media_dict_from_path(p) # dev UID set here
|
|
229
|
-
self.found_media_files.append(new_media)
|
|
230
|
-
logger.debug('Scanner.found_media_files = %s'%self.found_media_files)
|
|
231
|
-
if self.input_structure == 'folder_is_device':
|
|
232
|
-
self._enforce_folder_is_device()
|
|
233
|
-
self._use_folder_as_device_name()
|
|
305
|
+
# for now (TO DO?) 'loose' == no multi-cam
|
|
306
|
+
# self.top_dir_has_multicam = False
|
|
234
307
|
else:
|
|
235
|
-
|
|
308
|
+
# check later if inside each folder, media have same device
|
|
309
|
+
# for now, we'll guess structure is 'ordered'
|
|
310
|
+
self.input_structure = 'ordered'
|
|
311
|
+
for p in clip_paths:
|
|
312
|
+
new_media = media_at_path(self.input_structure, p) # dev UID set here
|
|
313
|
+
self.found_media_files.append(new_media)
|
|
314
|
+
# for non UIDed try building UID from filenam
|
|
315
|
+
def _try_name_from_files(medias):
|
|
316
|
+
# return common first strings in filename
|
|
317
|
+
def _all_identical(a_list):
|
|
318
|
+
return a_list.count(a_list[0]) == len(a_list)
|
|
319
|
+
names = [m.path.name for m in medias]
|
|
320
|
+
transposed_names = list(map(list, zip(*names)))
|
|
321
|
+
same = list(map(_all_identical, transposed_names))
|
|
322
|
+
try:
|
|
323
|
+
first_diff = same.index(False)
|
|
324
|
+
except:
|
|
325
|
+
return names[0].split('.')[0]
|
|
326
|
+
return names[0][:first_diff]
|
|
327
|
+
no_device_UID_medias = [m for m in self.found_media_files
|
|
328
|
+
if not m.device.UID]
|
|
329
|
+
logger.debug('those media have no device UID %s'%no_device_UID_medias)
|
|
330
|
+
if no_device_UID_medias:
|
|
331
|
+
# will guess a device name from media filenames
|
|
332
|
+
logger.debug('no_device_UID_medias %s'%no_device_UID_medias)
|
|
333
|
+
start_string = _try_name_from_files(no_device_UID_medias)
|
|
334
|
+
if len(start_string) < 2:
|
|
335
|
+
print('\nError, cant identify the device for those files:')
|
|
336
|
+
[print('%s, '%m.path.name, end='') for m in no_device_UID_medias]
|
|
337
|
+
print('\n')
|
|
338
|
+
sys.exit(1)
|
|
339
|
+
one_device = no_device_UID_medias[0].device
|
|
340
|
+
one_device.name = start_string
|
|
341
|
+
if not one_device.UID:
|
|
342
|
+
one_device.UID = hash(start_string)
|
|
343
|
+
print('\nWarning, guessing a device ID for those files:')
|
|
344
|
+
[print('[gold1]%s[/gold1], '%m.path.name, end='') for m
|
|
345
|
+
in no_device_UID_medias]
|
|
346
|
+
print('UID: [gold1]%s[/gold1]'%start_string)
|
|
347
|
+
for m in no_device_UID_medias:
|
|
348
|
+
m.device = one_device
|
|
349
|
+
logger.debug('new device added %s'%self.found_media_files)
|
|
350
|
+
logger.debug('Scanner.found_media_files = %s'%pformat(self.found_media_files))
|
|
351
|
+
if self.input_structure == 'ordered':
|
|
352
|
+
self._confirm_folders_have_same_device()
|
|
353
|
+
# [TODO] move this where Recordings have been TCed for any tracks timestamps
|
|
354
|
+
# <begin>
|
|
355
|
+
# devices = set([m.device for m in self.found_media_files])
|
|
356
|
+
# audio_devices = [d for d in devices if d.dev_type == 'REC']
|
|
357
|
+
# for recorder in audio_devices:
|
|
358
|
+
# # process tracks.txt for audio recorders
|
|
359
|
+
# recorder.tracks = self._get_tracks_from_file(recorder)
|
|
360
|
+
# # logging only:
|
|
361
|
+
# if recorder.tracks:
|
|
362
|
+
# if not all([lv == None for lv in recorder.tracks.lag_values]):
|
|
363
|
+
# logger.debug('%s has lag_values %s'%(
|
|
364
|
+
# recorder.name, recorder.tracks.lag_values))
|
|
365
|
+
# </end>
|
|
366
|
+
# check if device is in fact two parents up (and parent = ROLLxx):
|
|
367
|
+
# Group media by folder 2up and verify all media for each
|
|
368
|
+
# group have same device.
|
|
369
|
+
folder2up = lambda m: m.path.parent.parent
|
|
370
|
+
# logger.debug('folder2up: %s'%pformat([folder2up(m) for m
|
|
371
|
+
# in self.found_media_files]))
|
|
372
|
+
medias = sorted(self.found_media_files, key=folder2up)
|
|
373
|
+
# build lists for multiple reference of iterators
|
|
374
|
+
media_grouped_by_folder2up = [ (k, list(iterator)) for k, iterator
|
|
375
|
+
in groupby(medias, folder2up)]
|
|
376
|
+
logger.debug('media_grouped_by_folder2up: %s'%pformat(
|
|
377
|
+
media_grouped_by_folder2up))
|
|
378
|
+
folder_and_UIDs = [(f, [m.device.UID for m in medias])
|
|
379
|
+
for f, medias in media_grouped_by_folder2up]
|
|
380
|
+
logger.debug('devices: %s'%pformat(folder_and_UIDs))
|
|
381
|
+
def _multiple_and_same(a_list):
|
|
382
|
+
same = a_list.count(a_list[0]) == len(a_list)
|
|
383
|
+
return len(a_list) > 1 and same
|
|
384
|
+
folders_with_same_dev = [(f.name, UIDs[0]) for f, UIDs
|
|
385
|
+
in folder_and_UIDs
|
|
386
|
+
if _multiple_and_same(UIDs)]
|
|
387
|
+
logger.debug('folders_with_same_dev: %s'%pformat(folders_with_same_dev))
|
|
388
|
+
for name, UID in folders_with_same_dev:
|
|
389
|
+
for m in self.found_media_files:
|
|
390
|
+
if m.device.UID == UID:
|
|
391
|
+
m.device.name = name
|
|
392
|
+
no_name_devices = [m.device for m in self.found_media_files
|
|
393
|
+
if not m.device.name]
|
|
394
|
+
# possible if self.input_structure == 'loose'
|
|
395
|
+
def _try_name_from_metadata(media): # unused for now
|
|
396
|
+
# search model and make from fprobe
|
|
397
|
+
file = Path(media.path)
|
|
398
|
+
logger.debug('trying to find maker model for %s'%file)
|
|
399
|
+
try:
|
|
400
|
+
probe = ffmpeg.probe(file)
|
|
401
|
+
except ffmpeg.Error as e:
|
|
402
|
+
print('ffmpeg.probe error')
|
|
403
|
+
print(e.stderr, file)
|
|
404
|
+
return None, None #-----------------------------------------------------
|
|
405
|
+
# fall back to folder name
|
|
406
|
+
logger.debug('ffprobe %s'%pformat(probe))
|
|
407
|
+
# [TO BE COMPLETED]
|
|
408
|
+
# could reside in ['format','tags','com.apple.quicktime.model'],
|
|
409
|
+
# or ['format','tags','model'],
|
|
410
|
+
# or ['streams'][0]['tags']['vendor_id']) :-(
|
|
411
|
+
for anon_dev in no_name_devices:
|
|
412
|
+
medias = self.get_media_for_device(anon_dev)
|
|
413
|
+
guess_name = _try_name_from_files(medias)
|
|
414
|
+
# print('dev %s has no name, guessing %s'%(anon_dev, guess_name))
|
|
415
|
+
logger.debug('dev %s has no name, guessing %s'%(anon_dev, guess_name))
|
|
416
|
+
anon_dev.name = guess_name
|
|
236
417
|
pprint_found_media_files = pformat(self.found_media_files)
|
|
237
418
|
logger.debug('scanner.found_media_files = %s'%pprint_found_media_files)
|
|
419
|
+
logger.debug('all devices %s'%[m.device for m in self.found_media_files])
|
|
420
|
+
dev_is_REC = [m.device.dev_type == 'REC' for m in self.found_media_files]
|
|
421
|
+
if not any(dev_is_REC): # no audio recordings!
|
|
422
|
+
print('\rNo audio recording found, nothing to sync, bye.')
|
|
423
|
+
sys.exit(0)
|
|
238
424
|
|
|
239
|
-
def
|
|
425
|
+
def _confirm_folders_have_same_device(self):
|
|
240
426
|
"""
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
Returns nothing
|
|
245
|
-
"""
|
|
246
|
-
for m in self.found_media_files:
|
|
247
|
-
folder_name = m['path'].parent.name
|
|
248
|
-
# known_folder_name = [media['dev'].UID for media
|
|
249
|
-
# in self.found_media_files]
|
|
250
|
-
# if folder_name not in known_folder_name:
|
|
251
|
-
# print('l238', m['dev'])
|
|
252
|
-
m['dev'] = m['dev']._replace(name=folder_name)
|
|
253
|
-
# print('l240', m['dev'])
|
|
254
|
-
# m['dev'].UID = folder_name
|
|
255
|
-
# else:
|
|
256
|
-
# print('already existing folder name: [gold1]%s[/gold1] please change it and rerun'%m['path'].parent)
|
|
257
|
-
# quit()
|
|
258
|
-
logger.debug(self.found_media_files)
|
|
259
|
-
|
|
260
|
-
def _enforce_folder_is_device(self):
|
|
261
|
-
"""
|
|
262
|
-
|
|
263
|
-
Checks for files in self.found_media_files for structure as following.
|
|
427
|
+
Since input_structure == 'ordered',
|
|
428
|
+
checks for files in self.found_media_files for structure as following.
|
|
264
429
|
|
|
265
430
|
Warns user and quit program for:
|
|
266
431
|
A- folders with mix of video and audio
|
|
267
432
|
B- folders with mix of uniquely identified devices and unUIDied ones
|
|
268
|
-
C- folders with mixed audio
|
|
433
|
+
C- folders with mixed audio an video files
|
|
269
434
|
|
|
270
435
|
Warns user but proceeds for:
|
|
271
436
|
D- folder with only unUIDied files (overlaps will be check later)
|
|
272
437
|
|
|
438
|
+
Changes self.input_structure to 'loose' if a folder contains files
|
|
439
|
+
from different devices.
|
|
440
|
+
|
|
273
441
|
Proceeds silently if
|
|
274
442
|
E- all files in the folder are from the same device
|
|
275
443
|
|
|
276
444
|
Returns nothing
|
|
277
445
|
"""
|
|
278
|
-
def
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
446
|
+
def _exit_on_folder_name_clash():
|
|
447
|
+
# Check media parent folders are unique
|
|
448
|
+
# returns media_grouped_by_folder
|
|
449
|
+
def _list_duplicates(seq):
|
|
450
|
+
seen = set()
|
|
451
|
+
seen_add = seen.add
|
|
452
|
+
# adds all elements it doesn't know yet to seen and all other to seen_twice
|
|
453
|
+
seen_twice = set( x for x in seq if x in seen or seen_add(x) )
|
|
454
|
+
# turn the set into a list (as requested)
|
|
455
|
+
return list( seen_twice )
|
|
456
|
+
folder_key = lambda m: m.path.parent
|
|
457
|
+
medias = sorted(self.found_media_files, key=folder_key)
|
|
458
|
+
# build lists for multiple reference of iterators
|
|
459
|
+
media_grouped_by_folder = [ (k, list(iterator)) for k, iterator
|
|
460
|
+
in groupby(medias, folder_key)]
|
|
461
|
+
logger.debug('media_grouped_by_folder %s'%pformat(
|
|
462
|
+
media_grouped_by_folder))
|
|
463
|
+
complete_path_folders = [e[0] for e in media_grouped_by_folder]
|
|
464
|
+
name_of_folders = [p.name for p in complete_path_folders]
|
|
465
|
+
logger.debug('complete_path_folders with media files %s'%
|
|
466
|
+
complete_path_folders)
|
|
467
|
+
logger.debug('name_of_folders with media files %s'%name_of_folders)
|
|
468
|
+
# unique_folder_names = set(name_of_folders) [TODO] is this useful ?
|
|
469
|
+
# repeated_folders = _list_duplicates(name_of_folders)
|
|
470
|
+
# logger.debug('repeated_folders %s'%repeated_folders)
|
|
471
|
+
# if repeated_folders:
|
|
472
|
+
# print('There are conflicts for some repeated folder names:')
|
|
473
|
+
# for f in [str(p) for p in repeated_folders]:
|
|
474
|
+
# print(' [gold1]%s[/gold1]'%f)
|
|
475
|
+
# print('Here are the complete paths:')
|
|
476
|
+
# for f in [str(p) for p in complete_path_folders]:
|
|
477
|
+
# print(' [gold1]%s[/gold1]'%f)
|
|
478
|
+
# print('please rename and rerun. Quitting..')
|
|
479
|
+
# sys.exit(1) ####################################################
|
|
480
|
+
return media_grouped_by_folder
|
|
481
|
+
media_grouped_by_folder = _exit_on_folder_name_clash()
|
|
308
482
|
n_CAM_folder = 0
|
|
309
483
|
for folder, list_of_medias_in_folder in media_grouped_by_folder:
|
|
310
|
-
# list_of_medias_in_folder = list(media_files_same_folder_iterator)
|
|
311
484
|
# check all medias are either video or audio recordings in folder
|
|
312
485
|
# if not, warn user and quit.
|
|
313
|
-
dev_types = set([m
|
|
314
|
-
logger.debug('dev_types %s'%dev_types)
|
|
486
|
+
dev_types = set([m.device.dev_type for m in list_of_medias_in_folder])
|
|
487
|
+
logger.debug('dev_types for folder%s: %s'%(folder,dev_types))
|
|
315
488
|
if dev_types == {'CAM'}:
|
|
316
489
|
n_CAM_folder += 1
|
|
317
490
|
if len(dev_types) != 1:
|
|
318
491
|
print('\nProblem while scanning for media files. In [gold1]%s[/gold1]:'%folder)
|
|
319
492
|
print('There is a mix of video and audio files:')
|
|
320
|
-
[print('[gold1]%s[/gold1]'%m
|
|
493
|
+
[print('[gold1]%s[/gold1]'%m.path.name, end =', ')
|
|
321
494
|
for m in list_of_medias_in_folder]
|
|
322
495
|
print('\nplease move them in exclusive folders and rerun.\n')
|
|
323
|
-
|
|
496
|
+
sys.exit(1) ######################################################
|
|
324
497
|
unidentified = [m for m in list_of_medias_in_folder
|
|
325
|
-
if m
|
|
498
|
+
if m.device.UID == None]
|
|
326
499
|
UIDed = [m for m in list_of_medias_in_folder
|
|
327
|
-
if m
|
|
500
|
+
if m.device.UID != None]
|
|
328
501
|
logger.debug('devices in folder %s:'%folder)
|
|
329
|
-
logger.debug(' media with unknown devices %s'%unidentified)
|
|
330
|
-
logger.debug(' media with UIDed devices %s'%UIDed)
|
|
502
|
+
logger.debug(' media with unknown devices %s'%pformat(unidentified))
|
|
503
|
+
logger.debug(' media with UIDed devices %s'%pformat(UIDed))
|
|
331
504
|
if len(unidentified) != 0 and len(UIDed) != 0:
|
|
332
505
|
print('\nProblem while grouping files in [gold1]%s[/gold1]:'%folder)
|
|
333
506
|
print('There is a mix of unidentifiable and identified devices.')
|
|
334
507
|
print('Is this file:')
|
|
335
508
|
for m in unidentified:
|
|
336
|
-
print(' [gold1]%s[/gold1]'%m
|
|
509
|
+
print(' [gold1]%s[/gold1]'%m.path.name)
|
|
337
510
|
answer = input("In the right folder?")
|
|
338
511
|
if answer.upper() in ["Y", "YES"]:
|
|
339
512
|
continue
|
|
340
513
|
elif answer.upper() in ["N", "NO"]:
|
|
341
514
|
# Do action you need
|
|
342
515
|
print('please move the following files in a folder named appropriately:\n')
|
|
343
|
-
|
|
516
|
+
sys.exit(1) ################################################
|
|
344
517
|
# if, in a folder, there's a mix of different identified devices,
|
|
345
518
|
# Warn user and quit.
|
|
346
|
-
|
|
519
|
+
UIDs = [m.device.UID for m in UIDed]
|
|
520
|
+
all_same_device = UIDs.count(UIDs[0]) == len(UIDs)
|
|
521
|
+
logger.debug('UIDs in %s: %s. all_same_device %s'%(folder,
|
|
522
|
+
pformat(UIDs), all_same_device))
|
|
523
|
+
if not all_same_device:
|
|
524
|
+
self.input_structure = 'loose'
|
|
525
|
+
# self.top_dir_has_multicam = False
|
|
526
|
+
logger.debug('changed input_structure to loose')
|
|
527
|
+
# device name should be generated (it isn't the folder name...)
|
|
528
|
+
distinct_UIDS = set(UIDs)
|
|
529
|
+
n_UIDs = len(distinct_UIDS)
|
|
530
|
+
logger.debug('There are %i UIDs: %s'%(n_UIDs, distinct_UIDS))
|
|
531
|
+
# Buid CAM01, CAM02 or REC01, REC02.
|
|
532
|
+
# Get dev type from first media in list
|
|
533
|
+
devT = UIDed[0].device.dev_type # 'CAM' or 'REC'
|
|
534
|
+
generic_names = [devT + str(i).zfill(2) for i in range(n_UIDs)]
|
|
535
|
+
devUIDs_names = dict(zip(distinct_UIDS, generic_names))
|
|
536
|
+
logger.debug('devUIDs_names %s'%pformat(devUIDs_names))
|
|
537
|
+
# rename
|
|
538
|
+
for m in UIDed:
|
|
539
|
+
m.device.name = devUIDs_names[m.device.UID]
|
|
540
|
+
logger.debug('new name %s'%m.device.name)
|
|
347
541
|
if len(dev_types) != 1:
|
|
348
542
|
print('\nProblem while scanning for media files. In [gold1]%s[/gold1]:'%folder)
|
|
349
543
|
print('There is a mix of files from different devices:')
|
|
350
|
-
[print('[gold1]%s[/gold1]'%m
|
|
544
|
+
[print('[gold1]%s[/gold1]'%m.path.name, end =', ')
|
|
351
545
|
for m in list_of_medias_in_folder]
|
|
352
546
|
print('\nplease move them in exclusive folders and rerun.\n')
|
|
353
|
-
|
|
547
|
+
sys.exit(1) ####################################################
|
|
354
548
|
if len(unidentified) == len(list_of_medias_in_folder):
|
|
355
549
|
# all unidentified
|
|
356
550
|
if len(unidentified) > 1:
|
|
357
551
|
print('Assuming those files are from the same device:')
|
|
358
|
-
[print('[gold1]%s[/gold1]'%m
|
|
552
|
+
[print('[gold1]%s[/gold1]'%m.path.name, end =', ')
|
|
359
553
|
for m in unidentified]
|
|
360
554
|
print('\nIf not, there\'s a risk of error: put them in exclusive folders and rerun.')
|
|
361
555
|
# if we are here, the check is done: either
|
|
362
556
|
# all files in folder are from unidentified device or
|
|
363
557
|
# all files in folder are from the same identified device
|
|
364
558
|
logger.debug('n_CAM_folder %i'%n_CAM_folder)
|
|
365
|
-
self.top_dir_has_multicam = n_CAM_folder > 1
|
|
366
|
-
logger.debug('top_dir_has_multicam: %s'%self.top_dir_has_multicam)
|
|
367
559
|
return
|
|
368
560
|
|
|
369
561
|
|
|
370
562
|
|
|
371
563
|
|
|
372
564
|
|
|
565
|
+
|