tictacsync 0.98a0__py3-none-any.whl → 1.0.0a0__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 +8 -4
- tictacsync/newmix.py +320 -0
- tictacsync/remrgmx.py +6 -10
- tictacsync/timeline.py +8 -7
- {tictacsync-0.98a0.dist-info → tictacsync-1.0.0a0.dist-info}/METADATA +2 -2
- tictacsync-1.0.0a0.dist-info/RECORD +17 -0
- {tictacsync-0.98a0.dist-info → tictacsync-1.0.0a0.dist-info}/entry_points.txt +1 -1
- tictacsync-0.98a0.dist-info/RECORD +0 -16
- {tictacsync-0.98a0.dist-info → tictacsync-1.0.0a0.dist-info}/LICENSE +0 -0
- {tictacsync-0.98a0.dist-info → tictacsync-1.0.0a0.dist-info}/WHEEL +0 -0
- {tictacsync-0.98a0.dist-info → tictacsync-1.0.0a0.dist-info}/top_level.txt +0 -0
tictacsync/device_scanner.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# while inotifywait --recursive -e close_write . ; do python entry.py tests/multi2/; done
|
|
4
4
|
# above for linux
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
TRACKSFILE = 'tracks.txt'
|
|
7
7
|
SILENT_TRACK_TOKENS = '-0n'
|
|
8
8
|
|
|
9
9
|
av_file_extensions = \
|
|
@@ -270,12 +270,16 @@ class Scanner:
|
|
|
270
270
|
|
|
271
271
|
"""
|
|
272
272
|
files = Path(self.top_directory).rglob('*.*')
|
|
273
|
+
def _last4letters(part):
|
|
274
|
+
return
|
|
275
|
+
|
|
273
276
|
paths = [
|
|
274
277
|
p
|
|
275
278
|
for p in files
|
|
276
279
|
if p.suffix[1:] in av_file_extensions
|
|
277
280
|
and SYNCEDFOLDER not in p.parts # SyncedMedia
|
|
278
281
|
and MCCDIR not in p.parts # SyncedMulticamClips
|
|
282
|
+
and '_ISO' not in [part[-4:] for part in p.parts] # exclude ISO wav files
|
|
279
283
|
]
|
|
280
284
|
logger.debug('found media files %s'%paths)
|
|
281
285
|
parents = [p.parent for p in paths]
|
|
@@ -409,19 +413,19 @@ class Scanner:
|
|
|
409
413
|
|
|
410
414
|
def _get_tracks_from_file(self, device) -> Tracks:
|
|
411
415
|
"""
|
|
412
|
-
Look for eventual track names in
|
|
416
|
+
Look for eventual track names in TRACKSFILE file, stored inside the
|
|
413
417
|
recorder folder alongside the audio files. If there, returns a Tracks
|
|
414
418
|
object, if not returns None.
|
|
415
419
|
"""
|
|
416
420
|
source_audio_folder = device.folder
|
|
417
|
-
tracks_file = source_audio_folder/
|
|
421
|
+
tracks_file = source_audio_folder/TRACKSFILE
|
|
418
422
|
track_names = False
|
|
419
423
|
a_recording = [m for m in self.found_media_files
|
|
420
424
|
if m.device == device][0]
|
|
421
425
|
logger.debug('a_recording for device %s : %s'%(device, a_recording))
|
|
422
426
|
nchan = sox.file_info.channels(str(a_recording.path))
|
|
423
427
|
if os.path.isfile(tracks_file):
|
|
424
|
-
logger.debug('found file: %s'%(
|
|
428
|
+
logger.debug('found file: %s'%(TRACKSFILE))
|
|
425
429
|
tracks = self._parse_track_values(tracks_file)
|
|
426
430
|
if tracks.error_msg:
|
|
427
431
|
print('\nError parsing [gold1]%s[/gold1] file: %s, quitting.\n'%
|
tictacsync/newmix.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import os, itertools, argparse, ffmpeg, tempfile
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from loguru import logger
|
|
4
|
+
import shutil, sys, re, sox
|
|
5
|
+
from pprint import pformat
|
|
6
|
+
from rich import print
|
|
7
|
+
|
|
8
|
+
OUT_DIR_DEFAULT = 'SyncedMedia'
|
|
9
|
+
MCCDIR = 'SyncedMulticamClips'
|
|
10
|
+
SEC_DELAY_CHANGED_SND = 10 #sec, SND_DIR changed if diff time is bigger
|
|
11
|
+
DEL_TEMP = True
|
|
12
|
+
|
|
13
|
+
logger.level("DEBUG", color="<yellow>")
|
|
14
|
+
logger.remove()
|
|
15
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "_change_audio4video")
|
|
16
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "find_SND_vids_pairs_in_dir")
|
|
17
|
+
|
|
18
|
+
video_extensions = \
|
|
19
|
+
"""webm mkv flv flv vob ogv ogg drc gif gifv mng avi mov
|
|
20
|
+
qt wmv yuv rm rmvb viv asf mp4 m4p m4v mpg mp2 mpeg mpe
|
|
21
|
+
mpv mpg mpeg m2v m4v svi 3gp 3g2 mxf roq nsv""".split() # from wikipedia
|
|
22
|
+
|
|
23
|
+
def _pathname(tempfile_or_path) -> str:
|
|
24
|
+
# utility for obtaining a str from different filesystem objects
|
|
25
|
+
if isinstance(tempfile_or_path, str):
|
|
26
|
+
return tempfile_or_path
|
|
27
|
+
if isinstance(tempfile_or_path, Path):
|
|
28
|
+
return str(tempfile_or_path)
|
|
29
|
+
if isinstance(tempfile_or_path, tempfile._TemporaryFileWrapper):
|
|
30
|
+
return tempfile_or_path.name
|
|
31
|
+
else:
|
|
32
|
+
raise Exception('%s should be Path or tempfile...'%tempfile_or_path)
|
|
33
|
+
|
|
34
|
+
def is_synced_video(f):
|
|
35
|
+
# True if name as video extension
|
|
36
|
+
# and is under SyncedMedia or SyncedMulticamClips folders
|
|
37
|
+
# f is a Path
|
|
38
|
+
ext = f.suffix[1:] # removing leading '.'
|
|
39
|
+
ok_ext = ext.lower() in video_extensions
|
|
40
|
+
f_parts = f.parts
|
|
41
|
+
ok_folders = OUT_DIR_DEFAULT in f_parts or MCCDIR in f_parts
|
|
42
|
+
# logger.debug('ok_ext: %s ok_folders: %s'%(ok_ext, ok_folders))
|
|
43
|
+
return ok_ext and ok_folders
|
|
44
|
+
|
|
45
|
+
def find_SND_vids_pairs_in_dir(top):
|
|
46
|
+
# look for matching video name and SND dir name
|
|
47
|
+
# eg: IMG04.mp4 and IMG04_SND
|
|
48
|
+
# maybe IMG04v2.mp4 if audio changed before (than it will be IMG04v3.mp4)
|
|
49
|
+
# returns list of matches
|
|
50
|
+
# recursively search from 'top' argument
|
|
51
|
+
vids = []
|
|
52
|
+
SNDs = []
|
|
53
|
+
for (root,dirs,files) in os.walk(top):
|
|
54
|
+
for d in dirs:
|
|
55
|
+
if d[-4:] == '_SND':
|
|
56
|
+
SNDs.append(Path(root)/d)
|
|
57
|
+
for f in files:
|
|
58
|
+
if is_synced_video(Path(root)/f): # add being in SyncedMedia or SyncedMulticamClips folder
|
|
59
|
+
vids.append(Path(root)/f)
|
|
60
|
+
logger.debug('vids %s SNDs %s'%(pformat(vids), pformat(SNDs)))
|
|
61
|
+
matches = []
|
|
62
|
+
def _names_match(vidname, SND_name):
|
|
63
|
+
# vidname is a str and has no extension
|
|
64
|
+
# vidname could have vNN suffix as in DSC_8064v31 so matches DSC_8064
|
|
65
|
+
if vidname == SND_name: # no suffix presents
|
|
66
|
+
return True
|
|
67
|
+
m = re.match(SND_name + r'v(\d+)', vidname)
|
|
68
|
+
if m != None:
|
|
69
|
+
logger.debug('its a natch and N= %s'%m.groups()[0])
|
|
70
|
+
return m != None
|
|
71
|
+
for pair in list(itertools.product(SNDs, vids)):
|
|
72
|
+
# print(pair)
|
|
73
|
+
SND, vid = pair # Paths
|
|
74
|
+
vidname, ext = vid.name.split('.') # string
|
|
75
|
+
if _names_match(vidname, SND.name[:-4]):
|
|
76
|
+
logger.debug('SND %s matches video %s'%(
|
|
77
|
+
Path('').joinpath(*SND.parts[-2:]),
|
|
78
|
+
Path('').joinpath(*vid.parts[-3:])))
|
|
79
|
+
matches.append(pair) # list of Paths
|
|
80
|
+
logger.debug('matches: %s'%pformat(matches))
|
|
81
|
+
return matches
|
|
82
|
+
|
|
83
|
+
def parse_and_check_arguments():
|
|
84
|
+
# parses directories from command arguments
|
|
85
|
+
# check for consistencies and warn user and exits,
|
|
86
|
+
# if returns, gives:
|
|
87
|
+
# proxies_dir, originals_dir, audio_dir, both_audio_vid, scan_only
|
|
88
|
+
parser = argparse.ArgumentParser()
|
|
89
|
+
# parser.add_argument('-v',
|
|
90
|
+
# nargs=1,
|
|
91
|
+
# dest='video_dirs',
|
|
92
|
+
# help='Where proxy clips and/or originals are stored')
|
|
93
|
+
# parser.add_argument('-a',
|
|
94
|
+
# nargs=1,
|
|
95
|
+
# dest='audio_dir',
|
|
96
|
+
# help='Contains newly changed mix files')
|
|
97
|
+
parser.add_argument('-b',
|
|
98
|
+
nargs=1,
|
|
99
|
+
dest='both_audio_vid',
|
|
100
|
+
help='Directory scanned for both audio and video')
|
|
101
|
+
parser.add_argument('--dry',
|
|
102
|
+
action='store_true',
|
|
103
|
+
dest='scan_only',
|
|
104
|
+
help="Just display changed audio, don't merge")
|
|
105
|
+
args = parser.parse_args()
|
|
106
|
+
logger.debug('args %s'%args)
|
|
107
|
+
# ok cases:
|
|
108
|
+
# -p -o -a + no -b
|
|
109
|
+
# -o -a + no -b
|
|
110
|
+
# args_set = [args.originals_dir != None,
|
|
111
|
+
# args.audio_dir != None,
|
|
112
|
+
# args.both_audio_vid != None,
|
|
113
|
+
# ]
|
|
114
|
+
# p, o, a, b = args_set
|
|
115
|
+
# check that argument -b (both_audio_vid) is used alone
|
|
116
|
+
# if b and any([o, a, p]):
|
|
117
|
+
# print("\nDon't specify other argument than -b if both audio and video searched in the same directory.\n")
|
|
118
|
+
# parser.print_help(sys.stderr)
|
|
119
|
+
# sys.exit(0)
|
|
120
|
+
# check that if proxies (-p) are specified, orginals too (-o)
|
|
121
|
+
# if p and not o:
|
|
122
|
+
# print("\nIf proxies directory is specified, so should originals directory.\n")
|
|
123
|
+
# parser.print_help(sys.stderr)
|
|
124
|
+
# sys.exit(0)
|
|
125
|
+
# check that -o and -a are used together
|
|
126
|
+
# if not b and not (o and a):
|
|
127
|
+
# print("\nAt least originals and audio directories must be given (-o and -a) when audio and video are in different dir.\n")
|
|
128
|
+
# parser.print_help(sys.stderr)
|
|
129
|
+
# sys.exit(0)
|
|
130
|
+
# # work in progress (aug 2025), so limit to -b:
|
|
131
|
+
# if not b :
|
|
132
|
+
# print("\nFor now, only -b argument is supported (a directory scanned for both audio and video) .\n")
|
|
133
|
+
# parser.print_help(sys.stderr)
|
|
134
|
+
# sys.exit(0)
|
|
135
|
+
# list of singletons, so flatten. Keep None and False as is
|
|
136
|
+
return args
|
|
137
|
+
|
|
138
|
+
def get_recent_mix(SND_dir, vid):
|
|
139
|
+
# check if there are mixl, mixr or mix files in SND_dir
|
|
140
|
+
# and return the paths if they are more recent than vid.
|
|
141
|
+
# returns empty tuple otherwise
|
|
142
|
+
# arguments SND_dir, vid and returned values are of Path type
|
|
143
|
+
wav_files = list(SND_dir.iterdir())
|
|
144
|
+
logger.debug(f'wav_files {wav_files} in {SND_dir}')
|
|
145
|
+
def is_mix(p):
|
|
146
|
+
re_result = re.match(r'mix([lrLR])*', p.name)
|
|
147
|
+
logger.debug(f'for {p.name} re_result {re_result}')
|
|
148
|
+
return re_result is not None
|
|
149
|
+
mix_files = [p for p in wav_files if is_mix(p)]
|
|
150
|
+
if len(mix_files) == 0:
|
|
151
|
+
return ()
|
|
152
|
+
# consistency check, should be 1 or 2 files
|
|
153
|
+
if not len(mix_files) in (1,2):
|
|
154
|
+
print(f'\nError: too many mix files in [bold]{SND_dir}[/bold], bye.')
|
|
155
|
+
sys.exit(0)
|
|
156
|
+
# one file? it must be mix.wav
|
|
157
|
+
if len(mix_files) == 1:
|
|
158
|
+
fn = mix_files[0].name
|
|
159
|
+
if fn.upper() != 'MIX.WAV':
|
|
160
|
+
print(f'\nError in [bold]{SND_dir}[/bold], the only file should be mix.wav, not [bold]{fn}[/bold][/bold]; bye.')
|
|
161
|
+
sys.exit(0)
|
|
162
|
+
# two files? verify they are mixL and mixR and mono each
|
|
163
|
+
if len(mix_files) == 2:
|
|
164
|
+
first3uppercase = [p.name[:4].upper() for p in mix_files]
|
|
165
|
+
first3uppercase.sort()
|
|
166
|
+
first3uppercase = ''.join(first3uppercase)
|
|
167
|
+
if first3uppercase != 'MIXLMIXR':
|
|
168
|
+
print(f'\nError: mix names mismatch in [bold]{SND_dir}[/bold];')
|
|
169
|
+
print(f'names are [bold]{[p.name for p in mix_files]}[/bold], check they are simply mixL.wav and mixR.wav; bye.')
|
|
170
|
+
sys.exit(0)
|
|
171
|
+
def _nch(p):
|
|
172
|
+
return sox.file_info.channels(str(p))
|
|
173
|
+
are_mono = [_nch(p) == 1 for p in mix_files]
|
|
174
|
+
logger.debug('are_mono: %s'%are_mono)
|
|
175
|
+
if not all(are_mono):
|
|
176
|
+
print(f'\nError in [bold]{SND_dir}[/bold], some files are not mono, bye.')
|
|
177
|
+
sys.exit(0)
|
|
178
|
+
logger.debug(f'mix_files: {mix_files}')
|
|
179
|
+
# check dates, if two files, take first
|
|
180
|
+
mix_modification_time = mix_files[0].stat().st_mtime
|
|
181
|
+
vid_mod_time = vid.stat().st_mtime
|
|
182
|
+
# difference of modification time in secs
|
|
183
|
+
mix_more_recent_by = mix_modification_time - vid_mod_time
|
|
184
|
+
logger.debug('mix_more_recent_by: %s'%mix_more_recent_by)
|
|
185
|
+
if mix_more_recent_by > SEC_DELAY_CHANGED_SND:
|
|
186
|
+
if len(mix_files) == 1:
|
|
187
|
+
two_folders_up = mix_files[0]
|
|
188
|
+
# two_folders_up = Path('').joinpath(*mix_files[0].parts[-3:])
|
|
189
|
+
print(f'\nFound new mix: [bold]{two_folders_up}[/bold]')
|
|
190
|
+
return mix_files
|
|
191
|
+
else:
|
|
192
|
+
return ()
|
|
193
|
+
|
|
194
|
+
def _keep_VIDEO_only(video_path):
|
|
195
|
+
# return file handle to a temp video file formed from the video_path
|
|
196
|
+
# stripped of its sound
|
|
197
|
+
in1 = ffmpeg.input(_pathname(video_path))
|
|
198
|
+
video_extension = video_path.suffix
|
|
199
|
+
silenced_opts = ["-loglevel", "quiet", "-nostats", "-hide_banner"]
|
|
200
|
+
file_handle = tempfile.NamedTemporaryFile(suffix=video_extension,
|
|
201
|
+
delete=DEL_TEMP)
|
|
202
|
+
out1 = in1.output(file_handle.name, map='0:v', vcodec='copy')
|
|
203
|
+
ffmpeg.run([out1.global_args(*silenced_opts)], overwrite_output=True)
|
|
204
|
+
return file_handle
|
|
205
|
+
|
|
206
|
+
def _change_audio4video(audio_path: Path, video: Path):
|
|
207
|
+
"""
|
|
208
|
+
Replace audio in video (argument) by the audio contained in
|
|
209
|
+
audio_path (argument) returns nothing
|
|
210
|
+
If name has version number, bump it (DSC_8064v13.MOV -> DSC_8064v14.MOV)
|
|
211
|
+
|
|
212
|
+
"""
|
|
213
|
+
vidname, video_ext = video.name.split('.')
|
|
214
|
+
vid_only_handle = _keep_VIDEO_only(video)
|
|
215
|
+
a_n = _pathname(audio_path)
|
|
216
|
+
v_n = _pathname(vid_only_handle)
|
|
217
|
+
# check v suffix
|
|
218
|
+
m = re.match(f'(.*)v(\\d+)', vidname)
|
|
219
|
+
if m == None:
|
|
220
|
+
# no suffix, add one
|
|
221
|
+
out_path = video.parent / f'{vidname}v2.{video_ext}'
|
|
222
|
+
out_n = _pathname(out_path)
|
|
223
|
+
else:
|
|
224
|
+
base, number_str = m.groups()
|
|
225
|
+
logger.debug(f'base {base}, number_str {number_str}')
|
|
226
|
+
up_tick = 1 + int(number_str)
|
|
227
|
+
out_path = video.parent / f'{base}v{up_tick}.{video_ext}'
|
|
228
|
+
out_n = _pathname(out_path)
|
|
229
|
+
print(f'Video [bold]{video}[/bold] \nhas new sound and is now [bold]{out_path}[/bold]')
|
|
230
|
+
video.unlink()
|
|
231
|
+
# building args for debug purpose only:
|
|
232
|
+
ffmpeg_args = (
|
|
233
|
+
ffmpeg
|
|
234
|
+
.input(v_n)
|
|
235
|
+
.output(out_n, vcodec='copy')
|
|
236
|
+
# .output(out_n, shortest=None, vcodec='copy')
|
|
237
|
+
.global_args('-i', a_n, "-hide_banner")
|
|
238
|
+
.overwrite_output()
|
|
239
|
+
.get_args()
|
|
240
|
+
)
|
|
241
|
+
logger.debug('ffmpeg args: %s'%' '.join(ffmpeg_args))
|
|
242
|
+
try: # for real now
|
|
243
|
+
_, out = (
|
|
244
|
+
ffmpeg
|
|
245
|
+
.input(v_n)
|
|
246
|
+
# .output(out_n, shortest=None, vcodec='copy')
|
|
247
|
+
.output(out_n, vcodec='copy')
|
|
248
|
+
.global_args('-i', a_n, "-hide_banner")
|
|
249
|
+
.overwrite_output()
|
|
250
|
+
.run(capture_stderr=True)
|
|
251
|
+
)
|
|
252
|
+
logger.debug('ffmpeg output')
|
|
253
|
+
for l in out.decode("utf-8").split('\n'):
|
|
254
|
+
logger.debug(l)
|
|
255
|
+
except ffmpeg.Error as e:
|
|
256
|
+
print('ffmpeg.run error merging: \n\t %s + %s = %s\n'%(
|
|
257
|
+
audio_path,
|
|
258
|
+
video_path,
|
|
259
|
+
synced_clip_file
|
|
260
|
+
))
|
|
261
|
+
print(e)
|
|
262
|
+
print(e.stderr.decode('UTF-8'))
|
|
263
|
+
sys.exit(1)
|
|
264
|
+
|
|
265
|
+
def _sox_combine(paths) -> Path:
|
|
266
|
+
"""
|
|
267
|
+
Combines (stacks) files referred by the list of Path into a new temporary
|
|
268
|
+
files passed on return each files are stacked in a different channel, so
|
|
269
|
+
len(paths) == n_channels
|
|
270
|
+
"""
|
|
271
|
+
if len(paths) == 1: # one device only, nothing to stack
|
|
272
|
+
logger.debug('one device only, nothing to stack')
|
|
273
|
+
return paths[0] ########################################################
|
|
274
|
+
out_file_handle = tempfile.NamedTemporaryFile(suffix='.wav',
|
|
275
|
+
delete=DEL_TEMP)
|
|
276
|
+
filenames = [_pathname(p) for p in paths]
|
|
277
|
+
out_file_name = _pathname(out_file_handle)
|
|
278
|
+
logger.debug('combining files: %s into %s'%(
|
|
279
|
+
filenames,
|
|
280
|
+
out_file_name))
|
|
281
|
+
cbn = sox.Combiner()
|
|
282
|
+
cbn.set_input_format(file_type=['wav']*len(paths))
|
|
283
|
+
status = cbn.build(
|
|
284
|
+
filenames,
|
|
285
|
+
out_file_name,
|
|
286
|
+
combine_type='merge')
|
|
287
|
+
logger.debug('sox.build status: %s'%status)
|
|
288
|
+
if status != True:
|
|
289
|
+
print('Error, sox did not merge files in _sox_combine()')
|
|
290
|
+
sys.exit(1)
|
|
291
|
+
merged_duration = sox.file_info.duration(
|
|
292
|
+
_pathname(out_file_handle))
|
|
293
|
+
nchan = sox.file_info.channels(
|
|
294
|
+
_pathname(out_file_handle))
|
|
295
|
+
logger.debug('merged file duration %f s with %i channels '%
|
|
296
|
+
(merged_duration, nchan))
|
|
297
|
+
return out_file_handle
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def main():
|
|
301
|
+
# proxies_dir, originals_dir, audio_dir, both_audio_vid, scan_only = \
|
|
302
|
+
# parse_and_check_arguments()
|
|
303
|
+
args = parse_and_check_arguments()
|
|
304
|
+
matching_pairs = find_SND_vids_pairs_in_dir(args.both_audio_vid[0])
|
|
305
|
+
for SND_dir, vid in matching_pairs:
|
|
306
|
+
new_mix_files = get_recent_mix(SND_dir, vid)
|
|
307
|
+
# logger.debug('new_mix_files: %s'%str(new_mix_files))
|
|
308
|
+
if new_mix_files != ():
|
|
309
|
+
logger.debug(f'new mixes {new_mix_files} in {SND_dir} for {vid.name}')
|
|
310
|
+
if len(new_mix_files) == 2:
|
|
311
|
+
new_audio_wav = _sox_combine(new_mix_files)
|
|
312
|
+
logger.debug('stereo_wav: %s'%new_audio_wav)
|
|
313
|
+
else: # len == 1, mono wav file
|
|
314
|
+
new_audio_wav = new_mix_files[0]
|
|
315
|
+
_change_audio4video(new_audio_wav, vid)
|
|
316
|
+
# print('\nVideo %s has new audio'%vid)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
if __name__ == '__main__':
|
|
320
|
+
main()
|
tictacsync/remrgmx.py
CHANGED
|
@@ -32,9 +32,9 @@ def find_ISO_vids_pairs_in_dir(top):
|
|
|
32
32
|
if d[-4:] == '_ISO':
|
|
33
33
|
ISOs.append(Path(root)/d)
|
|
34
34
|
for f in files:
|
|
35
|
-
if is_video(f):
|
|
35
|
+
if is_video(f): # add being in SyncedMedia or SyncedMulticamClips folder
|
|
36
36
|
vids.append(Path(root)/f)
|
|
37
|
-
logger.debug('vids %s ISOs %s'%(vids, ISOs))
|
|
37
|
+
logger.debug('vids %s ISOs %s'%(pformat(vids), pformat(ISOs)))
|
|
38
38
|
matches = []
|
|
39
39
|
for pair in list(itertools.product(vids, ISOs)):
|
|
40
40
|
# print(pair)
|
|
@@ -54,14 +54,10 @@ def parse_and_check_arguments():
|
|
|
54
54
|
# if returns, gives:
|
|
55
55
|
# proxies_dir, originals_dir, audio_dir, both_audio_vid, scan_only
|
|
56
56
|
parser = argparse.ArgumentParser()
|
|
57
|
-
parser.add_argument('-
|
|
58
|
-
nargs
|
|
59
|
-
dest='
|
|
60
|
-
help='Where proxy clips are stored')
|
|
61
|
-
parser.add_argument('-o',
|
|
62
|
-
nargs=1,
|
|
63
|
-
dest='originals_dir',
|
|
64
|
-
help='Original (non-proxy) clips directory')
|
|
57
|
+
parser.add_argument('-v',
|
|
58
|
+
nargs=*,
|
|
59
|
+
dest='video_dirs',
|
|
60
|
+
help='Where proxy clips and/or originals are stored')
|
|
65
61
|
parser.add_argument('-a',
|
|
66
62
|
nargs=1,
|
|
67
63
|
dest='audio_dir',
|
tictacsync/timeline.py
CHANGED
|
@@ -27,6 +27,7 @@ DB_OSX_NORM = -6 #dB
|
|
|
27
27
|
OUT_DIR_DEFAULT = 'SyncedMedia'
|
|
28
28
|
MCCDIR = 'SyncedMulticamClips'
|
|
29
29
|
|
|
30
|
+
|
|
30
31
|
# utility to lock ISO audio files
|
|
31
32
|
def remove_write_permissions(path):
|
|
32
33
|
"""Remove write permissions from this path, while keeping all other permissions intact.
|
|
@@ -38,7 +39,6 @@ def remove_write_permissions(path):
|
|
|
38
39
|
NO_GROUP_WRITING = ~stat.S_IWGRP
|
|
39
40
|
NO_OTHER_WRITING = ~stat.S_IWOTH
|
|
40
41
|
NO_WRITING = NO_USER_WRITING & NO_GROUP_WRITING & NO_OTHER_WRITING
|
|
41
|
-
|
|
42
42
|
current_permissions = stat.S_IMODE(os.lstat(path).st_mode)
|
|
43
43
|
os.chmod(path, current_permissions & NO_WRITING)
|
|
44
44
|
|
|
@@ -202,7 +202,7 @@ def _sox_multi2stereo(multichan_tmpfl, stereo_trxs) -> tempfile.NamedTemporaryFi
|
|
|
202
202
|
stereo_tempfile = tempfile.NamedTemporaryFile(suffix='.wav',
|
|
203
203
|
delete=DEL_TEMP)
|
|
204
204
|
tfm = sox.Transformer()
|
|
205
|
-
tfm.channels(1)
|
|
205
|
+
tfm.channels(1) # why ? https://pysox.readthedocs.io/en/latest/api.html?highlight=channels#sox.transform.Transformer.channels
|
|
206
206
|
status = tfm.build(_pathname(multichan_tmpfl),_pathname(stereo_tempfile))
|
|
207
207
|
logger.debug('n chan ouput: %s'%
|
|
208
208
|
sox.file_info.channels(_pathname(stereo_tempfile)))
|
|
@@ -656,7 +656,7 @@ class AudioStitcherVideoMerger:
|
|
|
656
656
|
"""
|
|
657
657
|
Writes isolated audio files that were synced to synced_clip_file,
|
|
658
658
|
each track will have its dedicated monofile, named sequentially or with
|
|
659
|
-
the name find in
|
|
659
|
+
the name find in TRACKSFILE if any, see Scanner._get_tracks_from_file()
|
|
660
660
|
|
|
661
661
|
edited_audio_all_devices:
|
|
662
662
|
a list of (name, mono_tempfile)
|
|
@@ -712,7 +712,8 @@ class AudioStitcherVideoMerger:
|
|
|
712
712
|
synced_clip_dir = synced_clip_file.parent
|
|
713
713
|
# build ISOs subfolders structure, see comment string below
|
|
714
714
|
video_stem_WO_suffix = synced_clip_file.stem
|
|
715
|
-
ISOdir = synced_clip_dir/(video_stem_WO_suffix + '
|
|
715
|
+
# ISOdir = synced_clip_dir/(video_stem_WO_suffix + 'ISO')
|
|
716
|
+
ISOdir = synced_clip_dir/(video_stem_WO_suffix + '_SND')/'ISOfiles'
|
|
716
717
|
os.makedirs(ISOdir, exist_ok=True)
|
|
717
718
|
logger.debug('edited_audio_all_devices %s'%edited_audio_all_devices)
|
|
718
719
|
logger.debug('ISOdir %s'%ISOdir)
|
|
@@ -720,7 +721,7 @@ class AudioStitcherVideoMerger:
|
|
|
720
721
|
destination = ISOdir/('%s.wav'%name)
|
|
721
722
|
mono_tmpfl_trimpad = _fit_length(mono_tmpfl)
|
|
722
723
|
shutil.copy(_pathname(mono_tmpfl_trimpad), destination)
|
|
723
|
-
remove_write_permissions(destination)
|
|
724
|
+
# remove_write_permissions(destination)
|
|
724
725
|
logger.debug('destination:%s'%destination)
|
|
725
726
|
|
|
726
727
|
def _get_device_mix(self, device, multichan_tmpfl) -> tempfile.NamedTemporaryFile:
|
|
@@ -795,7 +796,7 @@ class AudioStitcherVideoMerger:
|
|
|
795
796
|
print('Error: TicTacCode channel detected is [gold1]%i[/gold1]'%
|
|
796
797
|
(device.ttc), end=' ')
|
|
797
798
|
print('and [gold1]%s[/gold1] for the device [gold1]%s[/gold1] specifies channel [gold1]%i[/gold1],'%
|
|
798
|
-
(device_scanner.
|
|
799
|
+
(device_scanner.TRACKSFILE,
|
|
799
800
|
device.name, device.tracks.ttc-1))
|
|
800
801
|
print('Please correct the discrepancy and rerun. Quitting.')
|
|
801
802
|
sys.exit(1)
|
|
@@ -1500,7 +1501,7 @@ class Matcher:
|
|
|
1500
1501
|
dest = r.final_synced_file.replace(multicam_dir/cam/clip_name)
|
|
1501
1502
|
logger.debug('dest: %s'%dest)
|
|
1502
1503
|
origin_folder = r.final_synced_file.parent
|
|
1503
|
-
folder_now_empty = len(list(origin_folder.glob('
|
|
1504
|
+
folder_now_empty = len(list(origin_folder.glob('*'))) == 0
|
|
1504
1505
|
if folder_now_empty:
|
|
1505
1506
|
logger.debug('after moving %s, folder is now empty, removing it'%dest)
|
|
1506
1507
|
origin_folder.rmdir()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: tictacsync
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0a0
|
|
4
4
|
Summary: command for syncing audio video recordings
|
|
5
5
|
Home-page: https://tictacsync.org/
|
|
6
6
|
Author: Raymond Lutz
|
|
@@ -68,7 +68,7 @@ Then pip install the syncing program:
|
|
|
68
68
|
This should install python dependencies _and_ the `tictacsync` command.
|
|
69
69
|
## Usage
|
|
70
70
|
|
|
71
|
-
Download multiple sample files [here](https://nuage.lutz.quebec/s/
|
|
71
|
+
Download multiple sample files [here](https://nuage.lutz.quebec/s/P3gbZR4GgGy8xQp/download/dailies1_1.zip) (625 MB, sorry) unzip and run:
|
|
72
72
|
|
|
73
73
|
> tictacsync dailies/loose
|
|
74
74
|
The program `tictacsync` will recursively scan the directory given as argument, find all audio that coincide with any video and merge them into a subfolder named `SyncedMedia`. When the argument is an unique media file (not a directory), no syncing will occur but the decoded starting time will be printed to stdout:
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
tictacsync/LTCcheck.py,sha256=IEfpB_ZajWuRTWtqji0H-B2g7GQvWmGVjfT0Icumv7o,15704
|
|
2
|
+
tictacsync/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
tictacsync/device_scanner.py,sha256=lNoOyPuAsPzjGs5a6iKJQ-BAVXKKINk8fvTEgXgFeK0,35593
|
|
4
|
+
tictacsync/entry.py,sha256=KOhB8ivgme3GPpWShad2adS1lvIU9v0yMFY0CELwAmM,20673
|
|
5
|
+
tictacsync/multi2polywav.py,sha256=-nX5reZo6QNxFYdhsliHTs8bTfMjPzcONDT8vJbkZUA,7291
|
|
6
|
+
tictacsync/newmix.py,sha256=-_bZrbZHMrLaP3As_tc5Q96Y-pI96sOvnOQiQDeY7x8,13009
|
|
7
|
+
tictacsync/remergemix.py,sha256=bRyi1hyNcyM1rTkHh8DmSsIQjYpwPprxSyyVipnxz30,9909
|
|
8
|
+
tictacsync/remrgmx.py,sha256=FxaAo5qqynpj6O56ekQGD31YP6X2g-kEdwVpHSCoh4Q,4265
|
|
9
|
+
tictacsync/synciso.py,sha256=XmUcdUF9rl4VdCm7XW4PeYWYWM0vgAY9dC2hapoul9g,4821
|
|
10
|
+
tictacsync/timeline.py,sha256=aMu1ntVrpFtH1YfyIgp80k7DT2RFo5B0E-BlMWE8wAs,72723
|
|
11
|
+
tictacsync/yaltc.py,sha256=xbMucI19UJKrEvIzyfpOsi3piSWzqM1gKgooeT9DV8g,53167
|
|
12
|
+
tictacsync-1.0.0a0.dist-info/LICENSE,sha256=ZAOPXLh1zlQAnhHUd7oLslKM01YZ5UiAu3STYjwIxck,1068
|
|
13
|
+
tictacsync-1.0.0a0.dist-info/METADATA,sha256=toUxrfuNZkQNbpNXBQGOSRBbN9WLD8w07_e5Gg_y54U,5697
|
|
14
|
+
tictacsync-1.0.0a0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
|
15
|
+
tictacsync-1.0.0a0.dist-info/entry_points.txt,sha256=bMsk7T_7fwCtAOUbFyvECvHOzCJb2fmWjkUKQTkwbsc,131
|
|
16
|
+
tictacsync-1.0.0a0.dist-info/top_level.txt,sha256=eaCWG-BsYTRR-gLTJbK4RfcaXajr0gjQ6wG97MkGRrg,11
|
|
17
|
+
tictacsync-1.0.0a0.dist-info/RECORD,,
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
tictacsync/LTCcheck.py,sha256=IEfpB_ZajWuRTWtqji0H-B2g7GQvWmGVjfT0Icumv7o,15704
|
|
2
|
-
tictacsync/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
tictacsync/device_scanner.py,sha256=6XTO4N0ipJ3HNa1I0aSKkTNIgPk_BtCoDDjwCVhOjpI,35446
|
|
4
|
-
tictacsync/entry.py,sha256=KOhB8ivgme3GPpWShad2adS1lvIU9v0yMFY0CELwAmM,20673
|
|
5
|
-
tictacsync/multi2polywav.py,sha256=-nX5reZo6QNxFYdhsliHTs8bTfMjPzcONDT8vJbkZUA,7291
|
|
6
|
-
tictacsync/remergemix.py,sha256=bRyi1hyNcyM1rTkHh8DmSsIQjYpwPprxSyyVipnxz30,9909
|
|
7
|
-
tictacsync/remrgmx.py,sha256=nGuNg55BtXpKTpklwZqunsgVNi-1h-_22OFSnGk7K8k,4340
|
|
8
|
-
tictacsync/synciso.py,sha256=XmUcdUF9rl4VdCm7XW4PeYWYWM0vgAY9dC2hapoul9g,4821
|
|
9
|
-
tictacsync/timeline.py,sha256=2CkTzMDiazYlBq2F1fhM2w4r6CIgmpQk1L2ZvAYRcnA,72532
|
|
10
|
-
tictacsync/yaltc.py,sha256=xbMucI19UJKrEvIzyfpOsi3piSWzqM1gKgooeT9DV8g,53167
|
|
11
|
-
tictacsync-0.98a0.dist-info/LICENSE,sha256=ZAOPXLh1zlQAnhHUd7oLslKM01YZ5UiAu3STYjwIxck,1068
|
|
12
|
-
tictacsync-0.98a0.dist-info/METADATA,sha256=PuGAcwkwrbbkh8wwudBmw4s5DU0kbgKsXJq27dYSXzY,5693
|
|
13
|
-
tictacsync-0.98a0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
|
14
|
-
tictacsync-0.98a0.dist-info/entry_points.txt,sha256=g3tdFFrVRcrKpuyKOCLUVBMgYfV65q9kpLZUOD_XCKg,139
|
|
15
|
-
tictacsync-0.98a0.dist-info/top_level.txt,sha256=eaCWG-BsYTRR-gLTJbK4RfcaXajr0gjQ6wG97MkGRrg,11
|
|
16
|
-
tictacsync-0.98a0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|