tictacsync 0.3a4__py3-none-any.whl → 0.5a0__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.
- tictacsync/device_scanner.py +20 -26
- tictacsync/entry.py +7 -10
- tictacsync/multi2polywav.py +2 -1
- tictacsync/remergemix.py +205 -92
- tictacsync/timeline.py +353 -196
- tictacsync/yaltc.py +179 -171
- {tictacsync-0.3a4.dist-info → tictacsync-0.5a0.dist-info}/METADATA +15 -9
- tictacsync-0.5a0.dist-info/RECORD +15 -0
- tictacsync-0.3a4.dist-info/RECORD +0 -15
- {tictacsync-0.3a4.dist-info → tictacsync-0.5a0.dist-info}/LICENSE +0 -0
- {tictacsync-0.3a4.dist-info → tictacsync-0.5a0.dist-info}/WHEEL +0 -0
- {tictacsync-0.3a4.dist-info → tictacsync-0.5a0.dist-info}/entry_points.txt +0 -0
- {tictacsync-0.3a4.dist-info → tictacsync-0.5a0.dist-info}/top_level.txt +0 -0
tictacsync/device_scanner.py
CHANGED
|
@@ -77,6 +77,7 @@ class Device:
|
|
|
77
77
|
name: str
|
|
78
78
|
dev_type: str # CAM or REC
|
|
79
79
|
n_chan: int
|
|
80
|
+
ttc: int
|
|
80
81
|
tracks: Tracks
|
|
81
82
|
def __hash__(self):
|
|
82
83
|
return self.UID
|
|
@@ -89,10 +90,12 @@ class Media:
|
|
|
89
90
|
"""
|
|
90
91
|
path: Path
|
|
91
92
|
device: Device
|
|
93
|
+
|
|
92
94
|
def media_at_path(input_structure, p):
|
|
93
95
|
# return Media object for mediafile using ffprobe
|
|
94
96
|
dev_UID, dt = get_device_ffprobe_UID(p)
|
|
95
|
-
dev_name = None
|
|
97
|
+
dev_name = None
|
|
98
|
+
logger.debug('ffprobe dev_UID:%s dt:%s'%(dev_UID, dt))
|
|
96
99
|
if input_structure == 'folder_is_device':
|
|
97
100
|
dev_name = p.parent.name
|
|
98
101
|
if dev_UID is None:
|
|
@@ -113,9 +116,10 @@ def media_at_path(input_structure, p):
|
|
|
113
116
|
else:
|
|
114
117
|
n = sox.file_info.channels(_pathname(p)) # eg 2
|
|
115
118
|
logger.debug('for file %s dev_UID established %s'%(p.name, dev_UID))
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
device = Device(UID=dev_UID, folder=p.parent, name=dev_name, dev_type=dt,
|
|
120
|
+
n_chan=n, ttc=None, tracks=None)
|
|
121
|
+
logger.debug('for path: %s, device:%s'%(p,device))
|
|
122
|
+
return Media(p, device)
|
|
119
123
|
|
|
120
124
|
def get_device_ffprobe_UID(file):
|
|
121
125
|
"""
|
|
@@ -140,7 +144,7 @@ def get_device_ffprobe_UID(file):
|
|
|
140
144
|
except ffmpeg.Error as e:
|
|
141
145
|
print('ffmpeg.probe error')
|
|
142
146
|
print(e.stderr, file)
|
|
143
|
-
return None, None
|
|
147
|
+
return None, None #-----------------------------------------------------
|
|
144
148
|
# fall back to folder name
|
|
145
149
|
streams = probe['streams']
|
|
146
150
|
codecs = [stream['codec_type'] for stream in streams]
|
|
@@ -154,9 +158,12 @@ def get_device_ffprobe_UID(file):
|
|
|
154
158
|
and 'date' not in l ]
|
|
155
159
|
# this removes any metadata related to the file
|
|
156
160
|
# but keeps metadata related to the device
|
|
161
|
+
logger.debug('probe_lines %s'%probe_lines)
|
|
157
162
|
UID = hash(''.join(probe_lines))
|
|
158
163
|
else:
|
|
159
164
|
UID = None
|
|
165
|
+
if UID == 0: # empty probe_lines from Audacity ?!?
|
|
166
|
+
UID = None
|
|
160
167
|
logger.debug('ffprobe_UID is: %s'%UID)
|
|
161
168
|
return UID, device_type
|
|
162
169
|
|
|
@@ -256,9 +263,6 @@ class Scanner:
|
|
|
256
263
|
for p in paths:
|
|
257
264
|
new_media = media_at_path(self.input_structure, p) # dev UID set here
|
|
258
265
|
self.found_media_files.append(new_media)
|
|
259
|
-
# files from devices without UID or name
|
|
260
|
-
# def _list_all_the_same(l):
|
|
261
|
-
# return all(e == l[0] for e in l)
|
|
262
266
|
def _try_name(medias):
|
|
263
267
|
# return common first strings in filename
|
|
264
268
|
names = [m.path.name for m in medias]
|
|
@@ -273,7 +277,6 @@ class Scanner:
|
|
|
273
277
|
if not m.device.UID]
|
|
274
278
|
if no_device_UID_media:
|
|
275
279
|
logger.debug('no_device_UID_media %s'%no_device_UID_media)
|
|
276
|
-
# print(no_device_UID_media)
|
|
277
280
|
start_string = _try_name(no_device_UID_media)
|
|
278
281
|
if len(start_string) < 2:
|
|
279
282
|
print('\nError, cant identify the device for those files:')
|
|
@@ -285,7 +288,8 @@ class Scanner:
|
|
|
285
288
|
if not one_device.UID:
|
|
286
289
|
one_device.UID = hash(start_string)
|
|
287
290
|
print('\nWarning, guessing a device ID for those files:')
|
|
288
|
-
[print('[gold1]%s[/gold1], '%m.path.name, end='') for m
|
|
291
|
+
[print('[gold1]%s[/gold1], '%m.path.name, end='') for m
|
|
292
|
+
in no_device_UID_media]
|
|
289
293
|
print('UID: [gold1]%s[/gold1]'%start_string)
|
|
290
294
|
for m in no_device_UID_media:
|
|
291
295
|
m.device = one_device
|
|
@@ -342,32 +346,22 @@ class Scanner:
|
|
|
342
346
|
ntracks += len(tracks.others)
|
|
343
347
|
ntracks += 1 # for ttc track
|
|
344
348
|
logger.debug(' n chan: %i n tracks file: %i'%(nchan, ntracks))
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
349
|
+
if ntracks != nchan:
|
|
350
|
+
print('\nError parsing %s content'%tracks_file)
|
|
351
|
+
print('incoherent number of tracks, %i vs %i quitting\n'%
|
|
352
|
+
(nchan, ntracks))
|
|
353
|
+
sys.exit(1)
|
|
350
354
|
err_msg = tracks.error_msg
|
|
351
355
|
if err_msg != None:
|
|
352
356
|
print('Error, quitting: in file %s, %s'%(tracks_file, err_msg))
|
|
353
357
|
raise Exception
|
|
354
358
|
else:
|
|
359
|
+
logger.debug('tracks object%s'%tracks)
|
|
355
360
|
return tracks
|
|
356
361
|
else:
|
|
357
362
|
logger.debug('no tracks.txt file found')
|
|
358
363
|
return None
|
|
359
364
|
|
|
360
|
-
# def _use_folder_as_device_name(self):
|
|
361
|
-
# """
|
|
362
|
-
# For each media in self.found_media_files replace existing Device.name by
|
|
363
|
-
# folder name.
|
|
364
|
-
|
|
365
|
-
# Returns nothing
|
|
366
|
-
# """
|
|
367
|
-
# for m in self.found_media_files:
|
|
368
|
-
# m.device.name = m.path.parent.name
|
|
369
|
-
# logger.debug(self.found_media_files)
|
|
370
|
-
|
|
371
365
|
def _check_folders_have_same_device(self):
|
|
372
366
|
"""
|
|
373
367
|
Since input_structure == 'folder_is_device,
|
tictacsync/entry.py
CHANGED
|
@@ -126,10 +126,10 @@ def main():
|
|
|
126
126
|
# logger.add(sys.stdout, filter="__main__")
|
|
127
127
|
# logger.add(sys.stdout, filter="device_scanner")
|
|
128
128
|
# logger.add(sys.stdout, filter="yaltc") _extract_sound_to_merge
|
|
129
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "media_at_path")
|
|
129
130
|
# logger.add(sys.stdout, filter=lambda r: r["function"] == "scan_media_and_build_devices_UID")
|
|
130
|
-
# logger.add(sys.stdout, filter=lambda r: r["function"] == "
|
|
131
|
-
# logger.add(sys.stdout, filter=lambda r: r["function"] == "
|
|
132
|
-
# logger.add(sys.stdout, filter=lambda r: r["function"] == "_sox_mix")
|
|
131
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "_get_device_mix")
|
|
132
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "_sox_mix_files")
|
|
133
133
|
top_dir = args.directory[0]
|
|
134
134
|
if os.path.isfile(top_dir):
|
|
135
135
|
file = top_dir
|
|
@@ -234,7 +234,7 @@ def main():
|
|
|
234
234
|
print('\nNothing to sync, exiting.\n')
|
|
235
235
|
sys.exit(1)
|
|
236
236
|
matcher = timeline.Matcher(recordings_with_time)
|
|
237
|
-
matcher.
|
|
237
|
+
matcher.scan_audio_for_each_videoclip()
|
|
238
238
|
if not matcher.video_mergers:
|
|
239
239
|
if not args.terse:
|
|
240
240
|
print('\nNothing to sync, bye.\n')
|
|
@@ -263,11 +263,11 @@ def main():
|
|
|
263
263
|
print('\nWrote output in folder [gold1]%s[/gold1]'%(
|
|
264
264
|
a_stitcher.synced_clip_dir))
|
|
265
265
|
for stitcher in matcher.video_mergers:
|
|
266
|
-
print('[gold1]%s[/gold1]'%stitcher.
|
|
266
|
+
print('[gold1]%s[/gold1]'%stitcher.videoclip.AVpath.name, end='')
|
|
267
267
|
for audio in stitcher.get_matched_audio_recs():
|
|
268
268
|
print(' + [gold1]%s[/gold1]'%audio.AVpath.name, end='')
|
|
269
|
-
new_file = stitcher.
|
|
270
|
-
print(' became [gold1]%s[/gold1]'%stitcher.
|
|
269
|
+
new_file = stitcher.videoclip.final_synced_file.parts
|
|
270
|
+
print(' became [gold1]%s[/gold1]'%stitcher.videoclip.final_synced_file.name)
|
|
271
271
|
# matcher._build_otio_tracks_for_cam()
|
|
272
272
|
matcher.shrink_gaps_between_takes()
|
|
273
273
|
sys.exit(0)
|
|
@@ -277,6 +277,3 @@ if __name__ == '__main__':
|
|
|
277
277
|
|
|
278
278
|
|
|
279
279
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
tictacsync/multi2polywav.py
CHANGED
|
@@ -61,7 +61,7 @@ def build_poly_name(multifiles):
|
|
|
61
61
|
"""
|
|
62
62
|
Returns string of polywav filename, constructed from similitudes between
|
|
63
63
|
multifile names. Ex:
|
|
64
|
-
|
|
64
|
+
4CH002I.wav and 4CH002M.wav returns 4CH002X.wav
|
|
65
65
|
"""
|
|
66
66
|
s1 = str(multifiles[0].stem)
|
|
67
67
|
s2 = str(multifiles[1].stem)
|
|
@@ -96,6 +96,7 @@ def build_poly(multifiles):
|
|
|
96
96
|
# multifiles is list of Path
|
|
97
97
|
# change extensions to mfw (multifile wav)
|
|
98
98
|
dir_multi = multifiles[0].parent
|
|
99
|
+
multifiles.reverse()
|
|
99
100
|
poly_name_b = build_poly_name(multifiles) # base only
|
|
100
101
|
poly_name = str(dir_multi/Path(poly_name_b))
|
|
101
102
|
filenames = [str(p) for p in multifiles]
|
tictacsync/remergemix.py
CHANGED
|
@@ -3,13 +3,35 @@ from loguru import logger
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
import sox, tempfile, os, ffmpeg
|
|
5
5
|
from rich import print
|
|
6
|
-
import shutil
|
|
6
|
+
import shutil, sys, re
|
|
7
|
+
from pprint import pformat
|
|
8
|
+
from itertools import groupby
|
|
7
9
|
|
|
10
|
+
try:
|
|
11
|
+
from . import timeline
|
|
12
|
+
except:
|
|
13
|
+
import timeline
|
|
8
14
|
|
|
15
|
+
DEL_TEMP = False
|
|
16
|
+
|
|
17
|
+
logger.level("DEBUG", color="<yellow>")
|
|
9
18
|
logger.remove()
|
|
10
|
-
|
|
19
|
+
|
|
20
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "_get_ISO_dirs")
|
|
21
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "_join_audio2video")
|
|
22
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "main")
|
|
23
|
+
# logger.add(sys.stdout, filter="__main__")
|
|
24
|
+
|
|
25
|
+
OUT_DIR = 'SyncedMedia'
|
|
26
|
+
SEC_DELAY_CHANGED_ISO = 10 #sec, ISO_DIR changed if diff time is bigger
|
|
27
|
+
|
|
28
|
+
video_extensions = \
|
|
29
|
+
"""webm mkv flv flv vob ogv ogg drc gif gifv mng avi mov
|
|
30
|
+
qt wmv yuv rm rmvb viv asf mp4 m4p m4v mpg mp2 mpeg mpe
|
|
31
|
+
mpv mpg mpeg m2v m4v svi 3gp 3g2 mxf roq nsv""".split() # from wikipedia
|
|
11
32
|
|
|
12
33
|
def _pathname(tempfile_or_path) -> str:
|
|
34
|
+
# utility for obtaining a str from different filesystem objects
|
|
13
35
|
if isinstance(tempfile_or_path, str):
|
|
14
36
|
return tempfile_or_path
|
|
15
37
|
if isinstance(tempfile_or_path, Path):
|
|
@@ -19,125 +41,216 @@ def _pathname(tempfile_or_path) -> str:
|
|
|
19
41
|
else:
|
|
20
42
|
raise Exception('%s should be Path or tempfile...'%tempfile_or_path)
|
|
21
43
|
|
|
22
|
-
def
|
|
44
|
+
def _keep_VIDEO_only(video_path):
|
|
45
|
+
# return file handle to a temp video file formed from the video_path
|
|
46
|
+
# stripped of its sound
|
|
47
|
+
in1 = ffmpeg.input(_pathname(video_path))
|
|
48
|
+
video_extension = video_path.suffix
|
|
49
|
+
silenced_opts = ["-loglevel", "quiet", "-nostats", "-hide_banner"]
|
|
50
|
+
file_handle = tempfile.NamedTemporaryFile(suffix=video_extension,
|
|
51
|
+
delete=DEL_TEMP)
|
|
52
|
+
out1 = in1.output(file_handle.name, map='0:v', vcodec='copy')
|
|
53
|
+
ffmpeg.run([out1.global_args(*silenced_opts)], overwrite_output=True)
|
|
54
|
+
return file_handle
|
|
55
|
+
|
|
56
|
+
def _join_audio2video(audio_path: Path, video: Path):
|
|
57
|
+
"""
|
|
58
|
+
Replace audio in video (argument) by the audio contained in
|
|
59
|
+
audio_path (argument) returns nothing
|
|
60
|
+
"""
|
|
23
61
|
video_ext = video.name.split('.')[1]
|
|
24
|
-
|
|
62
|
+
vid_only_handle = _keep_VIDEO_only(video)
|
|
63
|
+
a_n = _pathname(audio_path)
|
|
64
|
+
v_n = _pathname(vid_only_handle)
|
|
65
|
+
out_n = _pathname(video)
|
|
66
|
+
# building args for debug purpose only:
|
|
67
|
+
ffmpeg_args = (
|
|
25
68
|
ffmpeg
|
|
26
|
-
.input(
|
|
27
|
-
|
|
69
|
+
.input(v_n)
|
|
70
|
+
.output(out_n, shortest=None, vcodec='copy')
|
|
71
|
+
.global_args('-i', a_n, "-hide_banner")
|
|
72
|
+
.overwrite_output()
|
|
73
|
+
.get_args()
|
|
74
|
+
)
|
|
75
|
+
logger.debug('ffmpeg args: %s'%' '.join(ffmpeg_args))
|
|
76
|
+
try: # for real now
|
|
77
|
+
_, out = (
|
|
28
78
|
ffmpeg
|
|
29
|
-
.input(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
try:
|
|
33
|
-
(ffmpeg
|
|
34
|
-
.filter((audio_left, audio_right), 'join', inputs=2,
|
|
35
|
-
channel_layout='stereo')
|
|
36
|
-
.output(input_video.video, _pathname(out_file),
|
|
37
|
-
shortest=None, vcodec='copy', loglevel="quiet")
|
|
79
|
+
.input(v_n)
|
|
80
|
+
.output(out_n, shortest=None, vcodec='copy')
|
|
81
|
+
.global_args('-i', a_n, "-hide_banner")
|
|
38
82
|
.overwrite_output()
|
|
39
|
-
.run()
|
|
83
|
+
.run(capture_stderr=True)
|
|
84
|
+
)
|
|
85
|
+
logger.debug('ffmpeg output')
|
|
86
|
+
for l in out.decode("utf-8").split('\n'):
|
|
87
|
+
logger.debug(l)
|
|
40
88
|
except ffmpeg.Error as e:
|
|
89
|
+
print('ffmpeg.run error merging: \n\t %s + %s = %s\n'%(
|
|
90
|
+
audio_path,
|
|
91
|
+
video_path,
|
|
92
|
+
synced_clip_file
|
|
93
|
+
))
|
|
41
94
|
print(e)
|
|
42
95
|
print(e.stderr.decode('UTF-8'))
|
|
43
96
|
sys.exit(1)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def sync_cam(dir):
|
|
48
|
-
# dir is a CAM dir, contents are clips and ISO folders
|
|
49
|
-
ISOs = list(dir.glob('*.ISO'))
|
|
50
|
-
for iso in ISOs:
|
|
51
|
-
# iso is a folder
|
|
52
|
-
statResult = iso.stat()
|
|
53
|
-
mtime = statResult.st_mtime
|
|
54
|
-
# print('%s: %s'%(iso.name, biggest_mtime_in_dir(iso)))
|
|
55
|
-
iso_mod_time = biggest_mtime_in_dir(iso)
|
|
56
|
-
clip = clip_from_iso(iso)
|
|
57
|
-
clip_mod_time = clip.stat().st_mtime
|
|
58
|
-
iso_edited = iso_mod_time > clip_mod_time
|
|
59
|
-
# print('clip %s should be resync: %s'%(clip.name, iso_edited))
|
|
60
|
-
# print(clip)
|
|
61
|
-
if iso_edited:
|
|
62
|
-
print('Resyncing [gold1]%s[/gold1]'%clip.name)
|
|
63
|
-
LR_channels = list(valid_audio_files(iso))
|
|
64
|
-
_join(LR_channels, clip, 'test.MOV')
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def valid_audio_files(isofolder: Path) -> list:
|
|
97
|
+
|
|
98
|
+
def _changed(dir) -> bool:
|
|
68
99
|
"""
|
|
69
|
-
Returns
|
|
70
|
-
|
|
71
|
-
case B - more than two files, two are mixL mixR
|
|
72
|
-
case C - one file only -> sys.exit(1)
|
|
73
|
-
case D - more than two files, no mixL mixR -> sys.exit(1)
|
|
100
|
+
Returns True if any content of dir (arg) is more recent than dir itself by a
|
|
101
|
+
delay of SEC_DELAY_CHANGED_ISO. Uses modification times.
|
|
74
102
|
"""
|
|
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
|
-
|
|
103
|
+
logger.debug(f'checking {dir.name} for change')
|
|
104
|
+
ISO_modification_time = biggest_mtime_in_dir(dir)
|
|
105
|
+
clip = clip_from_iso(dir)
|
|
106
|
+
clip_mod_time = clip.stat().st_mtime
|
|
107
|
+
# difference of modification time in secs
|
|
108
|
+
ISO_more_recent_by = ISO_modification_time - clip_mod_time
|
|
109
|
+
logger.debug('ISO_more_recent_by: %s'%(ISO_more_recent_by))
|
|
110
|
+
iso_edited = ISO_more_recent_by > SEC_DELAY_CHANGED_ISO
|
|
111
|
+
logger.debug(f'_changed: {iso_edited}')
|
|
112
|
+
return iso_edited
|
|
113
|
+
|
|
114
|
+
def sox_mix(ISOdir):
|
|
115
|
+
"""
|
|
116
|
+
Mixes all wav files present in ISOdir (so excludes ttc file and nullified
|
|
117
|
+
ones).
|
|
118
|
+
Returns a mono or stereo tempfile
|
|
119
|
+
"""
|
|
120
|
+
logger.debug(f'mixing {ISOdir}')
|
|
121
|
+
def is_stereo_mic(p):
|
|
122
|
+
re_result = re.match(r'mic([lrLR])*', p.name)
|
|
123
|
+
# logger.debug(f're_result {re_result}')
|
|
124
|
+
return re_result is not None
|
|
125
|
+
stereo_mics = [p for p in ISOdir.iterdir() if is_stereo_mic(p)]
|
|
126
|
+
monofiles = [p for p in ISOdir.iterdir() if p not in stereo_mics]
|
|
127
|
+
# removing ttc files
|
|
128
|
+
def notTTC(p):
|
|
129
|
+
return p.name[:3] != 'ttc'
|
|
130
|
+
monofiles = [p for p in monofiles if notTTC(p)]
|
|
131
|
+
if stereo_mics == []: # mono
|
|
132
|
+
return timeline._sox_mix_files(monofiles) #-----------------------------
|
|
133
|
+
logger.debug(f'stereo_mics: {stereo_mics}')
|
|
134
|
+
def mic(p):
|
|
135
|
+
return re.search(r'(mic\d*)([lrLR])*', p.name).groups()
|
|
136
|
+
mics = [mic(p) for p in stereo_mics]
|
|
137
|
+
p_and_mic = list(zip(stereo_mics, mics))
|
|
138
|
+
logger.debug(f'p_and_mic: {p_and_mic}')
|
|
139
|
+
same_mic_key = lambda pair: pair[1][0]
|
|
140
|
+
p_and_mic = sorted(p_and_mic, key=same_mic_key)
|
|
141
|
+
grouped_by_mic = [ (k, list(iterator)) for k, iterator
|
|
142
|
+
in groupby(p_and_mic, same_mic_key)]
|
|
143
|
+
logger.debug(f'grouped_by_mic: {grouped_by_mic}')
|
|
144
|
+
def order_left_right(groupby_element):
|
|
145
|
+
# returns left and right path for a mic
|
|
146
|
+
name, paths = groupby_element
|
|
147
|
+
def chan(pair):
|
|
148
|
+
# (PosixPath('mic1r_ZOOM.wav'), ('mic1', 'r')) -> 'r'
|
|
149
|
+
return pair[1][1]
|
|
150
|
+
path_n_mic = sorted(paths, key=lambda pair: pair[1][1])
|
|
151
|
+
return [p[0] for p in path_n_mic] # just the path, not ('mic1', 'r')
|
|
152
|
+
left_right_paths = [order_left_right(e) for e in grouped_by_mic]
|
|
153
|
+
# logger.debug(f'left_right_paths: {left_right_paths}')
|
|
154
|
+
stereo_files = [timeline._sox_combine(pair) for pair in left_right_paths]
|
|
155
|
+
monoNstereo = monofiles + stereo_files
|
|
156
|
+
return timeline._sox_mix_files(monoNstereo)
|
|
157
|
+
|
|
158
|
+
def get_mix_file(iso_dir):
|
|
159
|
+
"""
|
|
160
|
+
If iso_dir (arg) contains a mono mix sound file or a stereo mix, returns its
|
|
161
|
+
path. If not, this creates the mix and returns it.
|
|
162
|
+
"""
|
|
163
|
+
wav_files = list(iso_dir.iterdir())
|
|
164
|
+
logger.debug(f'wav_files {wav_files}')
|
|
165
|
+
def is_mix(p):
|
|
166
|
+
re_result = re.match(r'mix([lrLR])*', p.name)
|
|
167
|
+
# logger.debug(f're_result {re_result}')
|
|
168
|
+
return re_result is not None
|
|
169
|
+
location_mix = [p for p in wav_files if is_mix(p)]
|
|
170
|
+
if location_mix == []:
|
|
171
|
+
logger.debug('no mix track, do the mix')
|
|
172
|
+
return sox_mix(iso_dir)
|
|
173
|
+
else:
|
|
174
|
+
return location_mix
|
|
101
175
|
|
|
102
176
|
def biggest_mtime_in_dir(folder: Path) -> float:
|
|
103
|
-
# return the most recent mod time in a folder
|
|
177
|
+
# return the most recent mod time of the files in a folder
|
|
104
178
|
dir_content = list(folder.iterdir())
|
|
105
179
|
stats = [p.stat() for p in dir_content]
|
|
106
180
|
mtimes = [stat.st_mtime for stat in stats]
|
|
107
181
|
return max(mtimes)
|
|
108
182
|
|
|
109
|
-
def clip_from_iso(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
183
|
+
def clip_from_iso(ISO_dir: Path) -> Path:
|
|
184
|
+
# find the sibling video file of ISO_dir. eg : MVI_01.ISO -> MVI_01.MP4
|
|
185
|
+
folder = ISO_dir.parent
|
|
186
|
+
siblings = list(folder.glob('%s.*'%ISO_dir.stem))
|
|
187
|
+
candidates =[p for p in siblings if p.name.split('.')[1] != 'ISO']
|
|
188
|
+
# should be unique
|
|
189
|
+
if len(candidates) != 1:
|
|
190
|
+
print(f'Error finding video corresponding to {ISO_dir}, quitting')
|
|
191
|
+
sys.exit(1)
|
|
192
|
+
return candidates[0]
|
|
193
|
+
|
|
194
|
+
def _get_ISO_dirs(top_dir):
|
|
195
|
+
"""
|
|
196
|
+
Check if top_dir contains (or somewhere beneath) videos with their
|
|
197
|
+
accompanying ISO folder (like the pair MVI_023.MP4 + MVI_023.ISO). If not
|
|
198
|
+
warns and exits.
|
|
199
|
+
|
|
200
|
+
Returns list of paths pointing to ISO dirs.
|
|
201
|
+
"""
|
|
202
|
+
p = Path(top_dir)
|
|
203
|
+
ISO_dirs = list(Path(top_dir).rglob('*.ISO'))
|
|
204
|
+
logger.debug('all files: %s'%pformat(ISO_dirs))
|
|
205
|
+
# validation: .ISO should be dir
|
|
206
|
+
all_are_dir = all([p.is_dir() for p in ISO_dirs])
|
|
207
|
+
logger.debug('.ISO are all dir %s'%all_are_dir)
|
|
120
208
|
if not all_are_dir:
|
|
121
|
-
print('Error:
|
|
122
|
-
print('Rerun tictacsync with one directory for each device.')
|
|
209
|
+
print('Error: some .ISO are not folders??? Quitting. %s'%ISO_dirs)
|
|
123
210
|
sys.exit(1)
|
|
124
|
-
|
|
211
|
+
# for each folder check a video file exists with the same stem
|
|
212
|
+
# but with a video format extension (listed in video_extensions)
|
|
213
|
+
for ISO in ISO_dirs:
|
|
214
|
+
logger.debug(f'checking {ISO}')
|
|
215
|
+
voisins = list(ISO.parent.glob(f'{ISO.stem}.*'))
|
|
216
|
+
voisins_suffixes = [p.suffix for p in voisins]
|
|
217
|
+
# remove ISO
|
|
218
|
+
voisins_suffixes.remove('.ISO')
|
|
219
|
+
# validations: should remain one element and should be video
|
|
220
|
+
suffix = voisins_suffixes[0].lower()[1:] # remove dot
|
|
221
|
+
logger.debug(f'remaining ext: {suffix}')
|
|
222
|
+
if len(voisins_suffixes) != 1 or suffix not in video_extensions:
|
|
223
|
+
print(f'Error with {voisins}, no video unique sibling?')
|
|
224
|
+
sys.exit(1) #-------------------------------------------------------
|
|
225
|
+
logger.debug(f'All ok, returning {ISO_dirs}')
|
|
226
|
+
return ISO_dirs
|
|
125
227
|
|
|
126
228
|
def main():
|
|
127
229
|
parser = argparse.ArgumentParser()
|
|
128
230
|
parser.add_argument(
|
|
129
231
|
"directory",
|
|
130
232
|
type=str,
|
|
131
|
-
nargs=
|
|
132
|
-
help="path of media directory containing Synced videos and their
|
|
233
|
+
nargs=1,
|
|
234
|
+
help="path of media directory containing Synced videos and their .ISO folder",
|
|
133
235
|
default='.'
|
|
134
236
|
)
|
|
135
237
|
args = parser.parse_args()
|
|
136
|
-
# logger.info('arguments: %s'%args)
|
|
137
238
|
logger.debug('args %s'%args)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
239
|
+
ISO_dirs = _get_ISO_dirs(args.directory[0])
|
|
240
|
+
logger.debug(f'Will check any change in {pformat(ISO_dirs)}')
|
|
241
|
+
changed_ISOs = [isod for isod in ISO_dirs if _changed(isod)]
|
|
242
|
+
logger.debug(f'changed_ISOs: {changed_ISOs}')
|
|
243
|
+
if changed_ISOs != []:
|
|
244
|
+
print('Will remix audio for:')
|
|
245
|
+
for p in changed_ISOs:
|
|
246
|
+
print(p.name)
|
|
247
|
+
newaudio_and_videos = [(get_mix_file(iso), clip_from_iso(iso)) for iso
|
|
248
|
+
in changed_ISOs]
|
|
249
|
+
for audio, video_clip in newaudio_and_videos:
|
|
250
|
+
print(f'Will remerge {video_clip.name}')
|
|
251
|
+
_join_audio2video(audio, video_clip)
|
|
252
|
+
if newaudio_and_videos == []:
|
|
253
|
+
print('Nothing has changed, bye.')
|
|
141
254
|
sys.exit(0)
|
|
142
255
|
|
|
143
256
|
if __name__ == '__main__':
|