tictacsync 1.0.0a0__tar.gz → 1.0.2a0__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 tictacsync might be problematic. Click here for more details.
- {tictacsync-1.0.0a0/tictacsync.egg-info → tictacsync-1.0.2a0}/PKG-INFO +2 -2
- {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/README.md +1 -1
- {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/setup.py +1 -1
- {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync/device_scanner.py +60 -29
- {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync/entry.py +12 -122
- tictacsync-1.0.2a0/tictacsync/mamsync.py +460 -0
- {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync/multi2polywav.py +1 -1
- {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync/newmix.py +4 -1
- {tictacsync-1.0.0a0 → tictacsync-1.0.2a0/tictacsync.egg-info}/PKG-INFO +2 -2
- {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync.egg-info/SOURCES.txt +1 -0
- {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/LICENSE +0 -0
- {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/setup.cfg +0 -0
- {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync/__init__.py +0 -0
- {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync/remergemix.py +0 -0
- {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync/timeline.py +0 -0
- {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync/yaltc.py +0 -0
- {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync.egg-info/dependency_links.txt +0 -0
- {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync.egg-info/entry_points.txt +0 -0
- {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync.egg-info/not-zip-safe +0 -0
- {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync.egg-info/requires.txt +0 -0
- {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: tictacsync
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2a0
|
|
4
4
|
Summary: command for syncing audio video recordings
|
|
5
5
|
Home-page: https://tictacsync.org/
|
|
6
6
|
Author: Raymond Lutz
|
|
@@ -58,7 +58,7 @@ Then pip install the syncing program:
|
|
|
58
58
|
This should install python dependencies _and_ the `tictacsync` command.
|
|
59
59
|
## Usage
|
|
60
60
|
|
|
61
|
-
Download multiple sample files [here](https://nuage.lutz.quebec/s/
|
|
61
|
+
Download multiple sample files [here](https://nuage.lutz.quebec/s/D4DgzHrLaAjqNmY) (625 MB, sorry) unzip and run:
|
|
62
62
|
|
|
63
63
|
> tictacsync dailies/loose
|
|
64
64
|
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:
|
|
@@ -35,7 +35,7 @@ Then pip install the syncing program:
|
|
|
35
35
|
This should install python dependencies _and_ the `tictacsync` command.
|
|
36
36
|
## Usage
|
|
37
37
|
|
|
38
|
-
Download multiple sample files [here](https://nuage.lutz.quebec/s/
|
|
38
|
+
Download multiple sample files [here](https://nuage.lutz.quebec/s/D4DgzHrLaAjqNmY) (625 MB, sorry) unzip and run:
|
|
39
39
|
|
|
40
40
|
> tictacsync dailies/loose
|
|
41
41
|
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:
|
|
@@ -33,7 +33,7 @@ setup(
|
|
|
33
33
|
'multi2polywav = tictacsync.multi2polywav:main',
|
|
34
34
|
]
|
|
35
35
|
},
|
|
36
|
-
version = '1.0.
|
|
36
|
+
version = '1.0.2-alpha',
|
|
37
37
|
description = "command for syncing audio video recordings",
|
|
38
38
|
long_description_content_type='text/markdown',
|
|
39
39
|
long_description = long_descr,
|
|
@@ -19,7 +19,7 @@ AMR APE AU AWB DSS DVF FLAC GSM IKLAX IVS M4A M4B M4P MMF MP3 MPC MSV NMF
|
|
|
19
19
|
OGG OGA MOGG OPUS RA RM RAW RF64 SLN TTA VOC VOX WAV WMA WV WEBM 8SVX CDA MOV AVI BWF""".split()
|
|
20
20
|
|
|
21
21
|
from dataclasses import dataclass
|
|
22
|
-
import ffmpeg, os, sys
|
|
22
|
+
import ffmpeg, os, sys, shutil
|
|
23
23
|
from os import listdir
|
|
24
24
|
from os.path import isfile, join, isdir
|
|
25
25
|
from collections import namedtuple
|
|
@@ -28,7 +28,7 @@ from pprint import pformat
|
|
|
28
28
|
# from collections import defaultdict
|
|
29
29
|
from loguru import logger
|
|
30
30
|
# import pathlib, os.path
|
|
31
|
-
import sox, tempfile
|
|
31
|
+
import sox, tempfile, platformdirs, filecmp
|
|
32
32
|
# from functools import reduce
|
|
33
33
|
from rich import print
|
|
34
34
|
from itertools import groupby
|
|
@@ -36,8 +36,10 @@ from itertools import groupby
|
|
|
36
36
|
# import distance
|
|
37
37
|
try:
|
|
38
38
|
from . import multi2polywav
|
|
39
|
+
from . import mamsync
|
|
39
40
|
except:
|
|
40
41
|
import multi2polywav
|
|
42
|
+
import mamsync
|
|
41
43
|
|
|
42
44
|
MCCDIR = 'SyncedMulticamClips'
|
|
43
45
|
SYNCEDFOLDER = 'SyncedMedia'
|
|
@@ -244,16 +246,7 @@ class Scanner:
|
|
|
244
246
|
CAMs = [d for d in devices if d.dev_type == 'CAM']
|
|
245
247
|
return len(set(CAMs))
|
|
246
248
|
|
|
247
|
-
def
|
|
248
|
-
"""
|
|
249
|
-
Return a device name taken from folders in path of a_media if and only
|
|
250
|
-
if all media files below it are from the same device. Goes up max two
|
|
251
|
-
levels from media; e.g., in /FOLDER2/FOLDER1/DSC00234.MOV will test up
|
|
252
|
-
to FOLDER2 as candidate.
|
|
253
|
-
"""
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
def scan_media_and_build_devices_UID(self, recursive=True):
|
|
249
|
+
def scan_media_and_build_devices_UID(self, synced_root = None):
|
|
257
250
|
"""
|
|
258
251
|
Scans Scanner.top_directory recursively for files with known audio-video
|
|
259
252
|
extensions. For each file found, a device fingerprint is obtained from
|
|
@@ -269,20 +262,59 @@ class Scanner:
|
|
|
269
262
|
Sets Scanner.input_structure = 'loose'|'ordered'
|
|
270
263
|
|
|
271
264
|
"""
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
265
|
+
logger.debug(f'on entry synced_root: {synced_root}')
|
|
266
|
+
if synced_root != None: # mam mode
|
|
267
|
+
p = Path(platformdirs.user_data_dir('mamsync', 'plutz'))/mamsync.LOG_FILE
|
|
268
|
+
with open(p, 'r') as fh:
|
|
269
|
+
done = set(fh.read().split()) # sets of strings of abs path
|
|
270
|
+
logger.debug(f'done clips: {pformat(done)}')
|
|
271
|
+
files = Path(self.top_directory).rglob('*')
|
|
272
|
+
clip_paths = []
|
|
273
|
+
some_done = False
|
|
274
|
+
for raw_path in files:
|
|
275
|
+
if raw_path.suffix[1:] in av_file_extensions:
|
|
276
|
+
if SYNCEDFOLDER not in raw_path.parts: # SyncedMedia
|
|
277
|
+
if MCCDIR not in raw_path.parts: # SyncedMulticamClips
|
|
278
|
+
if '_ISO' not in [part[-4:] for part in raw_path.parts]: # exclude ISO wav files
|
|
279
|
+
if synced_root != None and str(raw_path) in done:
|
|
280
|
+
logger.debug(f'{raw_path} done')
|
|
281
|
+
some_done = True
|
|
282
|
+
continue
|
|
283
|
+
else:
|
|
284
|
+
clip_paths.append(raw_path)
|
|
285
|
+
############################################################ MOVE
|
|
286
|
+
elif synced_root != None: # MAM mode
|
|
287
|
+
#non AV files and directories
|
|
288
|
+
synced_path = Path(synced_root)/str(raw_path)[1:] # cant join abs. paths
|
|
289
|
+
if raw_path.is_dir():
|
|
290
|
+
synced_path.mkdir(parents=True, exist_ok=True)
|
|
291
|
+
continue
|
|
292
|
+
# if here, it's a file
|
|
293
|
+
if not synced_path.exists():
|
|
294
|
+
print(f'will mirror non AV file at {synced_path}')
|
|
295
|
+
logger.debug(f'will mirror non AV file at {synced_path}')
|
|
296
|
+
shutil.copy2(raw_path, synced_path)
|
|
297
|
+
continue
|
|
298
|
+
# file exists, check if same
|
|
299
|
+
same = filecmp.cmp(raw_path, synced_path, shallow=False)
|
|
300
|
+
logger.debug(f'copy exists of:\n{raw_path}\n{synced_path}')
|
|
301
|
+
if not same:
|
|
302
|
+
print(f'file changed, copying again\n{raw_path}')
|
|
303
|
+
shutil.copy2(raw_path, synced_path)
|
|
304
|
+
else:
|
|
305
|
+
logger.debug('same content, next')
|
|
306
|
+
continue # next raw_path in loop
|
|
307
|
+
###################################################################
|
|
308
|
+
if some_done:
|
|
309
|
+
print('Somme media files were already synced...')
|
|
310
|
+
logger.debug('found media files %s'%clip_paths)
|
|
311
|
+
if len(clip_paths) == 0:
|
|
312
|
+
print('No media found, bye.')
|
|
313
|
+
sys.exit(0)
|
|
314
|
+
# self.found_media_files = []
|
|
315
|
+
# self.input_structure = 'loose'
|
|
316
|
+
# return
|
|
317
|
+
parents = [p.parent for p in clip_paths]
|
|
286
318
|
logger.debug('found parents %s'%pformat(parents))
|
|
287
319
|
# True if all elements are identical
|
|
288
320
|
AV_files_have_same_parent = parents.count(parents[0]) == len(parents)
|
|
@@ -296,7 +328,7 @@ class Scanner:
|
|
|
296
328
|
# check later if inside each folder, media have same device
|
|
297
329
|
# for now, we'll guess structure is 'ordered'
|
|
298
330
|
self.input_structure = 'ordered'
|
|
299
|
-
for p in
|
|
331
|
+
for p in clip_paths:
|
|
300
332
|
new_media = media_at_path(self.input_structure, p) # dev UID set here
|
|
301
333
|
self.found_media_files.append(new_media)
|
|
302
334
|
# for non UIDed try building UID from filenam
|
|
@@ -379,7 +411,7 @@ class Scanner:
|
|
|
379
411
|
no_name_devices = [m.device for m in self.found_media_files
|
|
380
412
|
if not m.device.name]
|
|
381
413
|
# possible if self.input_structure == 'loose'
|
|
382
|
-
def _try_name_from_metadata(media):
|
|
414
|
+
def _try_name_from_metadata(media): # unused for now
|
|
383
415
|
# search model and make from fprobe
|
|
384
416
|
file = Path(media.path)
|
|
385
417
|
logger.debug('trying to find maker model for %s'%file)
|
|
@@ -395,7 +427,6 @@ class Scanner:
|
|
|
395
427
|
# could reside in ['format','tags','com.apple.quicktime.model'],
|
|
396
428
|
# or ['format','tags','model'],
|
|
397
429
|
# or ['streams'][0]['tags']['vendor_id']) :-(
|
|
398
|
-
|
|
399
430
|
for anon_dev in no_name_devices:
|
|
400
431
|
medias = self.get_media_for_device(anon_dev)
|
|
401
432
|
guess_name = _try_name_from_files(medias)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# I know, the following is ugly, but I need those try's to
|
|
3
3
|
# run the command in my dev setting AND from
|
|
4
4
|
# a deployment set-up... surely I'm setting
|
|
5
|
-
# things wrong [TODO] find why and clean up this mess
|
|
5
|
+
# things wrong [TODO]: find why and clean up this mess
|
|
6
6
|
|
|
7
7
|
try:
|
|
8
8
|
from . import yaltc
|
|
@@ -40,23 +40,6 @@ ogg oga mogg opus ra rm raw rf64 sln tta voc vox wav wma wv webm 8svx cda""".spl
|
|
|
40
40
|
|
|
41
41
|
logger.level("DEBUG", color="<yellow>")
|
|
42
42
|
|
|
43
|
-
# def process_files_with_progress_bars(medias): # [todo, replace]
|
|
44
|
-
# recordings = []
|
|
45
|
-
# rec_with_TTC = []
|
|
46
|
-
# times = []
|
|
47
|
-
# for m in track(medias,
|
|
48
|
-
# description="1/4 Initializing Recordings:"):
|
|
49
|
-
# # file_alias = 'dummy'
|
|
50
|
-
# recordings.append(yaltc.Recording(m))
|
|
51
|
-
# for r in track(recordings,
|
|
52
|
-
# description="2/4 Looking for TicTacCode:"):
|
|
53
|
-
# if r.seems_to_have_TicTacCode_at_beginning():
|
|
54
|
-
# rec_with_TTC.append(r)
|
|
55
|
-
# for r in track(rec_with_TTC,
|
|
56
|
-
# description="3/4 Finding start times:"):
|
|
57
|
-
# times.append(r.get_start_time())
|
|
58
|
-
# return recordings, rec_with_TTC, times
|
|
59
|
-
|
|
60
43
|
def process_single(file, args):
|
|
61
44
|
# argument is a single file
|
|
62
45
|
m = device_scanner.media_at_path(None, Path(file))
|
|
@@ -122,61 +105,6 @@ def process_lag_adjustement(media_object):
|
|
|
122
105
|
logger.debug('trimmed_multichanfile %s'%timeline._pathname(trimmed_multichanfile))
|
|
123
106
|
Path(timeline._pathname(trimmed_multichanfile)).replace(media_object.path)
|
|
124
107
|
|
|
125
|
-
def start_proj(folders):
|
|
126
|
-
# if existing values are found,
|
|
127
|
-
# confirm new values with user.
|
|
128
|
-
# returns (source_RAW, destination_synced)
|
|
129
|
-
# either old (and confirmed) or new ones
|
|
130
|
-
def _write_cfg():
|
|
131
|
-
conf_dir = platformdirs.user_config_dir('tictacsync', 'plutz',
|
|
132
|
-
ensure_exists=True)
|
|
133
|
-
logger.debug('will start project with folders %s'%folders)
|
|
134
|
-
conf_file = Path(conf_dir)/'mirrored.cfg'
|
|
135
|
-
logger.debug('writing config in %s'%conf_file)
|
|
136
|
-
conf_prs = configparser.ConfigParser()
|
|
137
|
-
conf_prs['MIRRORED'] = {'source_RAW': folders[0],
|
|
138
|
-
'destination_synced': folders[1]}
|
|
139
|
-
with open(conf_file, 'w') as configfile:
|
|
140
|
-
conf_prs.write(configfile)
|
|
141
|
-
known_values = get_proj()
|
|
142
|
-
if known_values != ():
|
|
143
|
-
source_RAW, destination_synced = known_values
|
|
144
|
-
print('Warning: there is a current project')
|
|
145
|
-
print('with source (RAW) folder: %s\nand destination (synced) folder: %s'%
|
|
146
|
-
(source_RAW, destination_synced))
|
|
147
|
-
answer = input("\nDo you want to change values? [YES|NO]")
|
|
148
|
-
if answer.upper()[0] in ["Y", "YES"]:
|
|
149
|
-
_write_cfg()
|
|
150
|
-
return folders
|
|
151
|
-
elif answer.upper()[0] in ["N", "NO"]:
|
|
152
|
-
print('Ok, will keep old ones')
|
|
153
|
-
return source_RAW, destination_synced
|
|
154
|
-
else:
|
|
155
|
-
_write_cfg()
|
|
156
|
-
return folders
|
|
157
|
-
|
|
158
|
-
sys.exit(0)
|
|
159
|
-
|
|
160
|
-
def get_proj():
|
|
161
|
-
# check if user started a project before.
|
|
162
|
-
# stored in platformdirs.user_config_dir
|
|
163
|
-
# returns (source_RAW, destination_synced) if any
|
|
164
|
-
# () otherwise
|
|
165
|
-
conf_dir = platformdirs.user_config_dir('tictacsync', 'plutz')
|
|
166
|
-
conf_file = Path(conf_dir)/'mirrored.cfg'
|
|
167
|
-
if conf_file.exists():
|
|
168
|
-
logger.debug('reading config in %s'%conf_file)
|
|
169
|
-
conf_prs = configparser.ConfigParser()
|
|
170
|
-
conf_prs.read(conf_file)
|
|
171
|
-
source_RAW = conf_prs.get('MIRRORED', 'source_RAW')
|
|
172
|
-
destination_synced = conf_prs.get('MIRRORED', 'destination_synced')
|
|
173
|
-
logger.debug('read source_RAW: %s and destination_synced: %s'%
|
|
174
|
-
(source_RAW, destination_synced))
|
|
175
|
-
return (source_RAW, destination_synced)
|
|
176
|
-
else:
|
|
177
|
-
logger.debug('no config file found')
|
|
178
|
-
return ()
|
|
179
|
-
|
|
180
108
|
def main():
|
|
181
109
|
parser = argparse.ArgumentParser()
|
|
182
110
|
parser.add_argument(
|
|
@@ -191,15 +119,15 @@ def main():
|
|
|
191
119
|
action='store_true', #ie default False
|
|
192
120
|
dest='verbose_output',
|
|
193
121
|
help='Set verbose ouput')
|
|
194
|
-
parser.add_argument('--stop_mirroring',
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
parser.add_argument('--start-project', '-s' ,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
122
|
+
# parser.add_argument('--stop_mirroring',
|
|
123
|
+
# action='store_true', #ie default False
|
|
124
|
+
# dest='stop_mirroring',
|
|
125
|
+
# help='Stop mirroring mode, will write synced files alongside originals.')
|
|
126
|
+
# parser.add_argument('--start-project', '-s' ,
|
|
127
|
+
# nargs=2,
|
|
128
|
+
# dest='proj_folders',
|
|
129
|
+
# default = [],
|
|
130
|
+
# help='start mirrored tree output mode and specifies 2 folders: source (RAW) and destination (synced).')
|
|
203
131
|
parser.add_argument('-t','--timelineoffset',
|
|
204
132
|
nargs=1,
|
|
205
133
|
default=['00:00:00:00'],
|
|
@@ -242,33 +170,10 @@ def main():
|
|
|
242
170
|
# logger.add(sys.stdout, filter="__main__")
|
|
243
171
|
# logger.add(sys.stdout, filter="yaltc")
|
|
244
172
|
|
|
245
|
-
|
|
173
|
+
logger.add(sys.stdout, filter=lambda r: r["function"] == "start_proj")
|
|
246
174
|
# logger.add(sys.stdout, filter=lambda r: r["function"] == "_dev_type_for_name")
|
|
247
175
|
# logger.add(sys.stdout, filter=lambda r: r["function"] == "scan_media_and_build_devices_UID")
|
|
248
176
|
|
|
249
|
-
|
|
250
|
-
if args.proj_folders != []:
|
|
251
|
-
print('\bSorry, mirrored output mode not implemented yet: synced clips will')
|
|
252
|
-
print('be written alongside originals in a SyncedMedia folder. Bye.')
|
|
253
|
-
sys.exit(0)
|
|
254
|
-
|
|
255
|
-
if not len(args.proj_folders) in [0, 2]:
|
|
256
|
-
print('Error, -s option requires two folders')
|
|
257
|
-
sys.exit(0)
|
|
258
|
-
if len(args.proj_folders) == 2:
|
|
259
|
-
source_RAW, destination_synced = start_proj(args.proj_folders)
|
|
260
|
-
logger.debug('source_RAW: %s destination_synced: %s'%(source_RAW,
|
|
261
|
-
destination_synced))
|
|
262
|
-
proj_folders = get_proj()
|
|
263
|
-
if proj_folders != ():
|
|
264
|
-
# mirrored mode
|
|
265
|
-
source_RAW, destination_synced = proj_folders
|
|
266
|
-
logger.debug('Mirrored mode ON, with %s '%str(proj_folders))
|
|
267
|
-
else:
|
|
268
|
-
source_RAW, destination_synced = None, None
|
|
269
|
-
logger.debug('Mirrored mode OFF, ')
|
|
270
|
-
if source_RAW or destination_synced:
|
|
271
|
-
print('Mirrored not implemented yet, ignoring.')
|
|
272
177
|
top_dir = args.path[0]
|
|
273
178
|
if os.path.isfile(top_dir):
|
|
274
179
|
file = top_dir
|
|
@@ -276,15 +181,6 @@ def main():
|
|
|
276
181
|
if not os.path.isdir(top_dir):
|
|
277
182
|
print('%s is not a directory or doesnt exist.'%top_dir)
|
|
278
183
|
sys.exit(1)
|
|
279
|
-
# logger.debug('args.o %s'%args.o)
|
|
280
|
-
# print('args.o %s'%args.o)
|
|
281
|
-
# if args.anchor:
|
|
282
|
-
# anchor_dir = args.anchor[0]
|
|
283
|
-
# else:
|
|
284
|
-
# anchor_dir = None
|
|
285
|
-
# if args.anchor and not os.path.isdir(anchor_dir):
|
|
286
|
-
# print('%s is not a directory or doesnt exist.'%anchor_dir)
|
|
287
|
-
# sys.exit(1)
|
|
288
184
|
multi2polywav.poly_all(top_dir)
|
|
289
185
|
scanner = device_scanner.Scanner(top_dir, stay_silent=args.terse)
|
|
290
186
|
scanner.scan_media_and_build_devices_UID()
|
|
@@ -406,13 +302,6 @@ def main():
|
|
|
406
302
|
console.print(table)
|
|
407
303
|
print()
|
|
408
304
|
n_devices = scanner.get_devices_number()
|
|
409
|
-
# OUT_struct_for_mcam = scanner.top_dir_has_multicam and \
|
|
410
|
-
# scanner.input_structure != 'loose'
|
|
411
|
-
# OUT_struct_for_mcam = args.multicam
|
|
412
|
-
# if OUT_struct_for_mcam and scanner.input_structure == 'loose':
|
|
413
|
-
# print("\nSorry, can't output multicam structure if input is not structured:")
|
|
414
|
-
# print("each camera must have its own folder with its clips stored inside, quitting.")
|
|
415
|
-
# sys.exit(0)
|
|
416
305
|
if len(recordings_with_time) < 2:
|
|
417
306
|
if not args.terse:
|
|
418
307
|
print('\nNothing to sync, exiting.\n')
|
|
@@ -429,6 +318,7 @@ def main():
|
|
|
429
318
|
asked_ISOs = False
|
|
430
319
|
# output_dir = args.o
|
|
431
320
|
# if args.verbose_output or args.terse: # verbose, so no progress bars
|
|
321
|
+
print('Merging...')
|
|
432
322
|
for merger in matcher.mergers:
|
|
433
323
|
merger.build_audio_and_write_merged_media(top_dir,
|
|
434
324
|
args.dont_write_cam_folder,
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
|
|
2
|
+
# I know, the following is ugly, but I need those try's to
|
|
3
|
+
# run the command in my dev setting AND from
|
|
4
|
+
# a deployment set-up... surely I'm setting
|
|
5
|
+
# things wrong [TODO]: find why and clean up this mess
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from . import yaltc
|
|
9
|
+
from . import device_scanner
|
|
10
|
+
from . import timeline
|
|
11
|
+
from . import multi2polywav
|
|
12
|
+
except:
|
|
13
|
+
import yaltc
|
|
14
|
+
import device_scanner
|
|
15
|
+
import timeline
|
|
16
|
+
import multi2polywav
|
|
17
|
+
|
|
18
|
+
import argparse, tempfile, configparser
|
|
19
|
+
from loguru import logger
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
# import os, sys
|
|
22
|
+
import os, sys, sox, platformdirs
|
|
23
|
+
from rich.progress import track
|
|
24
|
+
# from pprint import pprint
|
|
25
|
+
from rich.console import Console
|
|
26
|
+
# from rich.text import Text
|
|
27
|
+
from rich.table import Table
|
|
28
|
+
from rich import print
|
|
29
|
+
from pprint import pprint, pformat
|
|
30
|
+
import numpy as np
|
|
31
|
+
|
|
32
|
+
DEL_TEMP = False
|
|
33
|
+
CONF_FILE = 'mamsync.cfg'
|
|
34
|
+
LOG_FILE = 'mamdone.txt'
|
|
35
|
+
|
|
36
|
+
av_file_extensions = \
|
|
37
|
+
"""MOV webm mkv flv flv vob ogv ogg drc gif gifv mng avi MTS M2TS TS mov qt
|
|
38
|
+
wmv yuv rm rmvb viv asf amv mp4 m4p m4v mpg mp2 mpeg mpe mpv mpg mpeg m2v
|
|
39
|
+
m4v svi 3gp 3g2 mxf roq nsv flv f4v f4p f4a f4b 3gp aa aac aax act aiff alac
|
|
40
|
+
amr ape au awb dss dvf flac gsm iklax ivs m4a m4b m4p mmf mp3 mpc msv nmf
|
|
41
|
+
ogg oga mogg opus ra rm raw rf64 sln tta voc vox wav wma wv webm 8svx cda""".split()
|
|
42
|
+
|
|
43
|
+
# logger.add(sys.stdout, level="DEBUG")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "scan_media_and_build_devices_UID")
|
|
47
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "main")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def process_single(file, args):
|
|
51
|
+
# argument is a single file
|
|
52
|
+
m = device_scanner.media_at_path(None, Path(file))
|
|
53
|
+
if args.plotting:
|
|
54
|
+
print('\nPlots can be zoomed and panned...')
|
|
55
|
+
print('Close window for next one.')
|
|
56
|
+
a_rec = yaltc.Recording(m, do_plots=args.plotting)
|
|
57
|
+
time = a_rec.get_start_time()
|
|
58
|
+
# time = a_rec.get_start_time(plots=args.plotting)
|
|
59
|
+
if time != None:
|
|
60
|
+
frac_time = int(time.microsecond / 1e2)
|
|
61
|
+
d = '%s.%s'%(time.strftime("%Y-%m-%d %H:%M:%S"),frac_time)
|
|
62
|
+
if args.terse:
|
|
63
|
+
print('%s UTC:%s pulse: %i in chan %i'%(file, d, a_rec.sync_position,
|
|
64
|
+
a_rec.TicTacCode_channel))
|
|
65
|
+
else:
|
|
66
|
+
print('\nRecording started at [gold1]%s[/gold1] UTC'%d)
|
|
67
|
+
print('true sample rate: [gold1]%.3f Hz[/gold1]'%a_rec.true_samplerate)
|
|
68
|
+
print('first sync at [gold1]%i[/gold1] samples in channel %i'%(a_rec.sync_position,
|
|
69
|
+
a_rec.TicTacCode_channel))
|
|
70
|
+
print('N.B.: all results are precise to the displayed digits!\n')
|
|
71
|
+
else:
|
|
72
|
+
if args.terse:
|
|
73
|
+
print('%s UTC: None'%(file))
|
|
74
|
+
else:
|
|
75
|
+
print('Start time couldnt be determined')
|
|
76
|
+
sys.exit(1)
|
|
77
|
+
|
|
78
|
+
def process_lag_adjustement(media_object):
|
|
79
|
+
# trim channels that are lagging (as stated in tracks.txt)
|
|
80
|
+
# replace the old file, and rename the old one with .wavbk
|
|
81
|
+
# if .wavbk exist, process was done already, so dont process
|
|
82
|
+
# returns nothing
|
|
83
|
+
lags = media_object.device.tracks.lag_values
|
|
84
|
+
logger.debug('will process %s lags'%[lags])
|
|
85
|
+
channels = timeline._sox_split_channels(media_object.path)
|
|
86
|
+
# add bk to file on filesystem, but media_object.path is unchanged (?)
|
|
87
|
+
backup_name = str(media_object.path) + 'bk'
|
|
88
|
+
if Path(backup_name).exists():
|
|
89
|
+
logger.debug('%s exists, so return now.'%backup_name)
|
|
90
|
+
return
|
|
91
|
+
media_object.path.replace(backup_name)
|
|
92
|
+
logger.debug('channels %s'%channels)
|
|
93
|
+
def _trim(lag, chan_file):
|
|
94
|
+
# for lag
|
|
95
|
+
if lag == None:
|
|
96
|
+
return chan_file
|
|
97
|
+
else:
|
|
98
|
+
logger.debug('process %s for lag of %s'%(chan_file, lag))
|
|
99
|
+
sox_transform = sox.Transformer()
|
|
100
|
+
sox_transform.trim(float(lag)*1e-3)
|
|
101
|
+
output_fh = tempfile.NamedTemporaryFile(suffix='.wav', delete=DEL_TEMP)
|
|
102
|
+
out_file = timeline._pathname(output_fh)
|
|
103
|
+
input_file = timeline._pathname(chan_file)
|
|
104
|
+
logger.debug('sox in and out files: %s %s'%(input_file, out_file))
|
|
105
|
+
logger.debug('calling sox_transform.build()')
|
|
106
|
+
status = sox_transform.build(input_file, out_file, return_output=True )
|
|
107
|
+
logger.debug('sox.build exit code %s'%str(status))
|
|
108
|
+
return output_fh
|
|
109
|
+
new_channels = [_trim(*e) for e in zip(lags, channels)]
|
|
110
|
+
logger.debug('new_channels %s'%new_channels)
|
|
111
|
+
trimmed_multichanfile = timeline._sox_combine(new_channels)
|
|
112
|
+
logger.debug('trimmed_multichanfile %s'%timeline._pathname(trimmed_multichanfile))
|
|
113
|
+
Path(timeline._pathname(trimmed_multichanfile)).replace(media_object.path)
|
|
114
|
+
|
|
115
|
+
def print_out_conf(raw_root, synced_root, snd_root):
|
|
116
|
+
print(f'RAWROOT (source with TC): "{raw_root}"')
|
|
117
|
+
print(f'SYNCEDROOT (destination of synced clips): "{synced_root}"')
|
|
118
|
+
print(f'SNDROOT (destination of ISOs files): "{snd_root}"')
|
|
119
|
+
|
|
120
|
+
def clear_log():
|
|
121
|
+
# clear the file logging clips already synced
|
|
122
|
+
data_dir = platformdirs.user_data_dir('mamsync', 'plutz', ensure_exists=True)
|
|
123
|
+
log_file = Path(data_dir)/LOG_FILE
|
|
124
|
+
print('Clearing log file "%s"'%log_file)
|
|
125
|
+
with open(log_file, 'w') as fh:
|
|
126
|
+
fh.write('done:\n')
|
|
127
|
+
|
|
128
|
+
def write_conf(raw_root, synced_root, snd_root):
|
|
129
|
+
# args are pahtlib.Paths.
|
|
130
|
+
# RAWROOT: files with TC (and ROLL folders), as is from cameras
|
|
131
|
+
# SYNCEDROOT: synced and no more TC (ROLL flattened)
|
|
132
|
+
# Writes configuration on filesystem for later retrieval
|
|
133
|
+
# Clears log of already synced clips.
|
|
134
|
+
conf_dir = platformdirs.user_config_dir('mamsync', 'plutz', ensure_exists=True)
|
|
135
|
+
logger.debug(f'will start project with raw_root:{raw_root}, synced_root:{synced_root}')
|
|
136
|
+
conf_file = Path(conf_dir)/CONF_FILE
|
|
137
|
+
logger.debug('writing config in %s'%conf_file)
|
|
138
|
+
print(f'\nWriting folders paths in configuration file "{conf_file}"')
|
|
139
|
+
print_out_conf(raw_root, synced_root, snd_root)
|
|
140
|
+
conf_prs = configparser.ConfigParser()
|
|
141
|
+
conf_prs['SECTION1'] = {'RAWROOT': raw_root,
|
|
142
|
+
'SYNCEDROOT': synced_root,
|
|
143
|
+
'SNDROOT': snd_root}
|
|
144
|
+
with open(conf_file, 'w') as configfile_handle:
|
|
145
|
+
conf_prs.write(configfile_handle)
|
|
146
|
+
with open(conf_file, 'r') as configfile_handle:
|
|
147
|
+
logger.debug(f'config file content: \n{configfile_handle.read()}')
|
|
148
|
+
clear_log()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# if known_values != ():
|
|
153
|
+
# RAWROOT, SYNCEDROOTS = known_values
|
|
154
|
+
# print('Warning: there is a current project')
|
|
155
|
+
# print('with source (RAW) folder: %s\nand destination (synced) folder: %s'%
|
|
156
|
+
# (RAWROOT, SYNCEDROOTS))
|
|
157
|
+
# answer = input("\nDo you want to change values? [YES|NO]")
|
|
158
|
+
# if answer.upper()[0] in ["Y", "YES"]:
|
|
159
|
+
# _write_cfg()
|
|
160
|
+
# return folders
|
|
161
|
+
# elif answer.upper()[0] in ["N", "NO"]:
|
|
162
|
+
# print('Ok, will keep old ones')
|
|
163
|
+
# return RAWROOT, SYNCEDROOTS
|
|
164
|
+
# else:
|
|
165
|
+
# _write_cfg()
|
|
166
|
+
# return folders
|
|
167
|
+
# sys.exit(0)
|
|
168
|
+
|
|
169
|
+
def get_proj(print_conf_stdout=False):
|
|
170
|
+
# check if user started a project before.
|
|
171
|
+
# stored in platformdirs.user_config_dir
|
|
172
|
+
# returns (RAWROOT, SYNCEDROOTS) if any, () otherwise.
|
|
173
|
+
# print location of conf file if print_conf_stdout
|
|
174
|
+
conf_dir = platformdirs.user_config_dir('mamsync', 'plutz')
|
|
175
|
+
conf_file = Path(conf_dir)/CONF_FILE
|
|
176
|
+
logger.debug('try reading config in %s'%conf_file)
|
|
177
|
+
if print_conf_stdout:
|
|
178
|
+
print(f'\nReading configuration from file {conf_file}')
|
|
179
|
+
if conf_file.exists():
|
|
180
|
+
conf_prs = configparser.ConfigParser()
|
|
181
|
+
conf_prs.read(conf_file)
|
|
182
|
+
RAWROOT = conf_prs.get('SECTION1', 'RAWROOT')
|
|
183
|
+
SYNCEDROOT = conf_prs.get('SECTION1', 'SYNCEDROOT')
|
|
184
|
+
SNDROOT = conf_prs.get('SECTION1', 'SNDROOT')
|
|
185
|
+
logger.debug('read from conf: RAWROOT= %s SYNCEDROOT= %s SNDROOT=%s'%
|
|
186
|
+
(RAWROOT, SYNCEDROOT, SNDROOT))
|
|
187
|
+
return RAWROOT, SYNCEDROOT, SNDROOT
|
|
188
|
+
else:
|
|
189
|
+
logger.debug(f'no config file found at {conf_file}')
|
|
190
|
+
return ()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def main():
|
|
194
|
+
parser = argparse.ArgumentParser()
|
|
195
|
+
parser.add_argument('--rawfolder',
|
|
196
|
+
nargs = 1,
|
|
197
|
+
dest='rawfolder',
|
|
198
|
+
help='Sets new value for raw root folder (i.e.: clips with TC)')
|
|
199
|
+
parser.add_argument('--syncedfolder',
|
|
200
|
+
nargs = 1,
|
|
201
|
+
dest='syncedfolder',
|
|
202
|
+
help='Sets new value for synced root folder (i.e.: synced clips without TC)')
|
|
203
|
+
parser.add_argument('--sndfolder',
|
|
204
|
+
nargs = 1,
|
|
205
|
+
dest='sndfolder',
|
|
206
|
+
help='Sets new value for sound folder (where ISOs will be stored)')
|
|
207
|
+
parser.add_argument('--showconf',
|
|
208
|
+
action='store_true',
|
|
209
|
+
dest='showconf',
|
|
210
|
+
help='Show current configured values.')
|
|
211
|
+
parser.add_argument('--resync',
|
|
212
|
+
action='store_true',
|
|
213
|
+
dest='resync',
|
|
214
|
+
help='Resync previously done clips.')
|
|
215
|
+
parser.add_argument(
|
|
216
|
+
"sub_dir",
|
|
217
|
+
type=str,
|
|
218
|
+
nargs='?',
|
|
219
|
+
help="Sub directory to scan, should under RAWROOT."
|
|
220
|
+
)
|
|
221
|
+
parser.add_argument('--terse',
|
|
222
|
+
action='store_true',
|
|
223
|
+
dest='terse',
|
|
224
|
+
help='Terse output')
|
|
225
|
+
parser.add_argument('--isos',
|
|
226
|
+
action='store_true',
|
|
227
|
+
dest='write_ISOs',
|
|
228
|
+
help='Cut ISO sound files')
|
|
229
|
+
parser.add_argument('-t','--timelineoffset',
|
|
230
|
+
nargs=1,
|
|
231
|
+
default=['00:00:00:00'],
|
|
232
|
+
dest='timelineoffset',
|
|
233
|
+
help='When processing multicam, where to place clips on NLE timeline (HH:MM:SS:FF)')
|
|
234
|
+
args = parser.parse_args()
|
|
235
|
+
logger.debug(f'arguments from argparse {args}')
|
|
236
|
+
# check --rawfolder, --syncedfolder and --sndfolder are used together
|
|
237
|
+
at_least_one = args.rawfolder != None or args.syncedfolder != None or args.sndfolder != None
|
|
238
|
+
tous = args.rawfolder != None and args.syncedfolder != None and args.sndfolder != None
|
|
239
|
+
if at_least_one:
|
|
240
|
+
if not tous:
|
|
241
|
+
print('Error: all --rawfolder, --syncedfolder and --sndfolder must be specified.')
|
|
242
|
+
sys.exit(0)
|
|
243
|
+
# check the 3 paths are ok
|
|
244
|
+
roots = [Path(e) for e in [args.rawfolder[0],
|
|
245
|
+
args.syncedfolder[0], args.sndfolder[0]]]
|
|
246
|
+
for r in roots:
|
|
247
|
+
if not r.is_absolute():
|
|
248
|
+
print(f'\rError: folder {r} must be an absolute path. Bye')
|
|
249
|
+
sys.exit(0)
|
|
250
|
+
if not r.exists():
|
|
251
|
+
print(f'\rError: folder {r} does not exist. Bye')
|
|
252
|
+
sys.exit(0)
|
|
253
|
+
if not r.is_dir():
|
|
254
|
+
print(f'\rError: path {r} is not a folder. Bye')
|
|
255
|
+
sys.exit(0)
|
|
256
|
+
# if still here, everything in 3 folders is ok
|
|
257
|
+
write_conf(*roots)
|
|
258
|
+
sys.exit(0)
|
|
259
|
+
if args.showconf:
|
|
260
|
+
raw_root, synced_root = get_proj(True)
|
|
261
|
+
print_out_conf(raw_root, synced_root)
|
|
262
|
+
sys.exit(0)
|
|
263
|
+
# if still here, no rawfolder, --syncedfolder --sndfolder or --showconf
|
|
264
|
+
# so go for a scan and sync, maybe with a sub_dir
|
|
265
|
+
raw_root, synced_root, snd_root = get_proj(False)
|
|
266
|
+
if args.sub_dir != None:
|
|
267
|
+
top_dir = args.sub_dir
|
|
268
|
+
logger.debug(f'sub _dir: {args.sub_dir}')
|
|
269
|
+
if not Path(top_dir).exists():
|
|
270
|
+
print(f"\rError: folder {top_dir} doesn't exist, bye.")
|
|
271
|
+
sys.exit(0)
|
|
272
|
+
else:
|
|
273
|
+
top_dir = raw_root
|
|
274
|
+
if args.resync:
|
|
275
|
+
clear_log()
|
|
276
|
+
multi2polywav.poly_all(top_dir)
|
|
277
|
+
scanner = device_scanner.Scanner(top_dir, stay_silent=args.terse)
|
|
278
|
+
scanner.scan_media_and_build_devices_UID(synced_root=synced_root)
|
|
279
|
+
for m in scanner.found_media_files:
|
|
280
|
+
if m.device.tracks:
|
|
281
|
+
if not all([lv == None for lv in m.device.tracks.lag_values]):
|
|
282
|
+
logger.debug('%s has lag_values %s'%(
|
|
283
|
+
m.path, m.device.tracks.lag_values))
|
|
284
|
+
# any lag for a channel is specified by user in tracks.txt
|
|
285
|
+
process_lag_adjustement(m)
|
|
286
|
+
audio_REC_only = all([m.device.dev_type == 'REC' for m
|
|
287
|
+
in scanner.found_media_files])
|
|
288
|
+
# if audio_REC_only:
|
|
289
|
+
# if scanner.input_structure != 'ordered':
|
|
290
|
+
# print('For merging audio only, use a directory per device, quitting')
|
|
291
|
+
# sys.exit(1)
|
|
292
|
+
# print('\n\n\nOnly audio recordings are present')
|
|
293
|
+
# print('Which device should be the reference?\n')
|
|
294
|
+
# devices = scanner.get_devices()
|
|
295
|
+
# maxch = len(devices)
|
|
296
|
+
# for i, d in enumerate(devices):
|
|
297
|
+
# print('\t%i - %s'%(i+1, d.name))
|
|
298
|
+
# while True:
|
|
299
|
+
# print('\nEnter your choice:', end='')
|
|
300
|
+
# choice = input()
|
|
301
|
+
# try:
|
|
302
|
+
# choice = int(choice)
|
|
303
|
+
# except:
|
|
304
|
+
# print('Please use numeric digits.')
|
|
305
|
+
# continue
|
|
306
|
+
# if choice not in list(range(1, maxch + 1)):
|
|
307
|
+
# print('Please enter a number in [1..%i]'%maxch)
|
|
308
|
+
# continue
|
|
309
|
+
# break
|
|
310
|
+
# ref_device = list(devices)[choice - 1]
|
|
311
|
+
# # ref_device = list(devices)[3 - 1]
|
|
312
|
+
# print('When only audio recordings are present, ISOs files will be cut and written.')
|
|
313
|
+
if not args.terse:
|
|
314
|
+
if scanner.input_structure == 'ordered':
|
|
315
|
+
print('\nDetected structured folders')
|
|
316
|
+
# if scanner.top_dir_has_multicam:
|
|
317
|
+
# print(', multicam')
|
|
318
|
+
# else:
|
|
319
|
+
# print()
|
|
320
|
+
else:
|
|
321
|
+
print('\nDetected loose structure')
|
|
322
|
+
if scanner.CAM_numbers() > 1:
|
|
323
|
+
print('\nNote: different CAMs are present, will sync audio for each of them but if you want to set their')
|
|
324
|
+
print('respective timecode for NLE timeline alignement you should regroup clips by CAM under their own DIR.')
|
|
325
|
+
print('\nFound [gold1]%i[/gold1] media files '%(
|
|
326
|
+
len(scanner.found_media_files)), end='')
|
|
327
|
+
print('from [gold1]%i[/gold1] devices:\n'%(
|
|
328
|
+
scanner.get_devices_number()))
|
|
329
|
+
all_devices = scanner.get_devices()
|
|
330
|
+
for dev in all_devices:
|
|
331
|
+
dt = 'Camera' if dev.dev_type == 'CAM' else 'Recorder'
|
|
332
|
+
print('%s [gold1]%s[/gold1] with files:'%(dt, dev.name), end = ' ')
|
|
333
|
+
medias = scanner.get_media_for_device(dev)
|
|
334
|
+
for m in medias[:-1]: # last printed out of loop
|
|
335
|
+
print('[gold1]%s[/gold1]'%m.path.name, end=', ')
|
|
336
|
+
print('[gold1]%s[/gold1]'%medias[-1].path.name)
|
|
337
|
+
a_media = medias[0]
|
|
338
|
+
# check if all audio recorders have same sampling freq
|
|
339
|
+
freqs = [dev.sampling_freq for dev in all_devices if dev.dev_type == 'REC']
|
|
340
|
+
same = np.isclose(np.std(freqs),0)
|
|
341
|
+
logger.debug('sampling freqs from audio recorders %s, same:%s'%(freqs, same))
|
|
342
|
+
if not same:
|
|
343
|
+
print('some audio recorders have different sampling frequencies:')
|
|
344
|
+
print(freqs)
|
|
345
|
+
print('resulting in undefined results: quitting...')
|
|
346
|
+
quit()
|
|
347
|
+
print()
|
|
348
|
+
# recordings, rec_with_TTC = process_files(scanner.found_media_files, args)
|
|
349
|
+
recordings = [yaltc.Recording(m, do_plots=False) for m
|
|
350
|
+
in scanner.found_media_files]
|
|
351
|
+
recordings_with_time = [
|
|
352
|
+
rec
|
|
353
|
+
for rec in recordings
|
|
354
|
+
if rec.get_start_time()
|
|
355
|
+
]
|
|
356
|
+
# if audio_REC_only:
|
|
357
|
+
# for rec in recordings:
|
|
358
|
+
# # print(rec, rec.device == ref_device)
|
|
359
|
+
# if rec.device == ref_device:
|
|
360
|
+
# rec.is_reference = True
|
|
361
|
+
if not args.terse:
|
|
362
|
+
table = Table(title="tictacsync results")
|
|
363
|
+
table.add_column("Recording\n", justify="center", style='gold1')
|
|
364
|
+
table.add_column("TTC chan\n (1st=#0)", justify="center", style='gold1')
|
|
365
|
+
# table.add_column("Device\n", justify="center", style='gold1')
|
|
366
|
+
table.add_column("UTC times\nstart:end", justify="center", style='gold1')
|
|
367
|
+
table.add_column("Clock drift\n(ppm)", justify="right", style='gold1')
|
|
368
|
+
# table.add_column("SN ratio\n(dB)", justify="center", style='gold1')
|
|
369
|
+
table.add_column("Date\n", justify="center", style='gold1')
|
|
370
|
+
rec_WO_time = [
|
|
371
|
+
rec.AVpath.name
|
|
372
|
+
for rec in recordings
|
|
373
|
+
if rec not in recordings_with_time]
|
|
374
|
+
if rec_WO_time:
|
|
375
|
+
print('No time found for: ',end='')
|
|
376
|
+
[print(rec, end=' ') for rec in rec_WO_time]
|
|
377
|
+
print('\n')
|
|
378
|
+
for r in recordings_with_time:
|
|
379
|
+
date = r.get_start_time().strftime("%y-%m-%d")
|
|
380
|
+
start_HHMMSS = r.get_start_time().strftime("%Hh%Mm%Ss")
|
|
381
|
+
end_MMSS = r.get_end_time().strftime("%Mm%Ss")
|
|
382
|
+
times_range = start_HHMMSS + ':' + end_MMSS
|
|
383
|
+
table.add_row(
|
|
384
|
+
str(r.AVpath.name),
|
|
385
|
+
str(r.TicTacCode_channel),
|
|
386
|
+
# r.device,
|
|
387
|
+
times_range,
|
|
388
|
+
# '%.6f'%(r.true_samplerate/1e3),
|
|
389
|
+
'%2i'%(r.get_samplerate_drift()),
|
|
390
|
+
# '%.0f'%r.decoder.SN_ratio,
|
|
391
|
+
date
|
|
392
|
+
)
|
|
393
|
+
console = Console()
|
|
394
|
+
console.print(table)
|
|
395
|
+
print()
|
|
396
|
+
n_devices = scanner.get_devices_number()
|
|
397
|
+
if len(recordings_with_time) < 2:
|
|
398
|
+
if not args.terse:
|
|
399
|
+
print('\nNothing to sync, exiting.\n')
|
|
400
|
+
sys.exit(1)
|
|
401
|
+
matcher = timeline.Matcher(recordings_with_time)
|
|
402
|
+
matcher.scan_audio_for_each_videoclip()
|
|
403
|
+
if not matcher.mergers:
|
|
404
|
+
if not args.terse:
|
|
405
|
+
print('\nNothing to sync, bye.\n')
|
|
406
|
+
sys.exit(1)
|
|
407
|
+
asked_ISOs = args.write_ISOs
|
|
408
|
+
if asked_ISOs and scanner.input_structure != 'ordered':
|
|
409
|
+
print('Warning, can\'t write ISOs without structured folders: [gold1]--isos[/gold1] option ignored.\n')
|
|
410
|
+
asked_ISOs = False
|
|
411
|
+
# output_dir = args.o
|
|
412
|
+
# if args.verbose_output or args.terse: # verbose, so no progress bars
|
|
413
|
+
print('Merging...')
|
|
414
|
+
for merger in matcher.mergers:
|
|
415
|
+
merger.build_audio_and_write_merged_media(top_dir,
|
|
416
|
+
False, # args.dont_write_cam_folder,
|
|
417
|
+
asked_ISOs,
|
|
418
|
+
audio_REC_only)
|
|
419
|
+
if not args.terse:
|
|
420
|
+
print("\n")
|
|
421
|
+
# find out where files were written
|
|
422
|
+
# a_merger = matcher.mergers[0]
|
|
423
|
+
# log file
|
|
424
|
+
p = Path(platformdirs.user_data_dir('mamsync', 'plutz'))/LOG_FILE
|
|
425
|
+
log_filehandle = open(p, 'a')
|
|
426
|
+
for merger in matcher.mergers:
|
|
427
|
+
print('[gold1]%s[/gold1]'%merger.videoclip.AVpath.name, end='')
|
|
428
|
+
for audio in merger.get_matched_audio_recs():
|
|
429
|
+
print(' + [gold1]%s[/gold1]'%audio.AVpath.name, end='')
|
|
430
|
+
new_file = merger.videoclip.final_synced_file.parts
|
|
431
|
+
final_p = merger.videoclip.final_synced_file
|
|
432
|
+
nameAnd2Parents = Path('').joinpath(*final_p.parts[-2:])
|
|
433
|
+
print(' became [gold1]%s[/gold1]'%nameAnd2Parents)
|
|
434
|
+
# add full path to log file
|
|
435
|
+
log_filehandle.write(f'{merger.videoclip.AVpath}\n')
|
|
436
|
+
# matcher._build_otio_tracks_for_cam()
|
|
437
|
+
log_filehandle.close()
|
|
438
|
+
matcher.set_up_clusters() # multicam
|
|
439
|
+
matcher.shrink_gaps_between_takes(args.timelineoffset)
|
|
440
|
+
logger.debug('matcher.multicam_clips_clusters %s'%
|
|
441
|
+
pformat(matcher.multicam_clips_clusters))
|
|
442
|
+
# clusters is list of {'end': t1, 'start': t2, 'vids': [r1,r3]}
|
|
443
|
+
# really_clusters is True if one of them has len() > 1
|
|
444
|
+
really_clusters = any([len(cl['vids']) > 1 for cl
|
|
445
|
+
in matcher.multicam_clips_clusters])
|
|
446
|
+
if really_clusters:
|
|
447
|
+
if scanner.input_structure == 'loose':
|
|
448
|
+
print('\nThere are synced multicam clips but without structured folders')
|
|
449
|
+
print('they were not grouped together under the same folder.')
|
|
450
|
+
else:
|
|
451
|
+
matcher.move_multicam_to_dir()
|
|
452
|
+
else:
|
|
453
|
+
logger.debug('not really a multicam cluster, nothing to move')
|
|
454
|
+
sys.exit(0)
|
|
455
|
+
|
|
456
|
+
if __name__ == '__main__':
|
|
457
|
+
main()
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
|
|
@@ -31,7 +31,7 @@ def print_grby(grby):
|
|
|
31
31
|
def wav_recursive_scan(top_directory):
|
|
32
32
|
files_lower_case = Path(top_directory).rglob('*.wav')
|
|
33
33
|
files_upper_case = Path(top_directory).rglob('*.WAV')
|
|
34
|
-
files = list(files_lower_case) + list(files_upper_case)
|
|
34
|
+
files = set(list(files_lower_case) + list(files_upper_case))
|
|
35
35
|
paths = [
|
|
36
36
|
p
|
|
37
37
|
for p in files
|
|
@@ -97,7 +97,7 @@ def parse_and_check_arguments():
|
|
|
97
97
|
parser.add_argument('-b',
|
|
98
98
|
nargs=1,
|
|
99
99
|
dest='both_audio_vid',
|
|
100
|
-
help='Directory scanned for both audio and video')
|
|
100
|
+
help='Directory scanned for both audio and video, when tictacsync was used in "alongside mode"')
|
|
101
101
|
parser.add_argument('--dry',
|
|
102
102
|
action='store_true',
|
|
103
103
|
dest='scan_only',
|
|
@@ -133,6 +133,9 @@ def parse_and_check_arguments():
|
|
|
133
133
|
# parser.print_help(sys.stderr)
|
|
134
134
|
# sys.exit(0)
|
|
135
135
|
# list of singletons, so flatten. Keep None and False as is
|
|
136
|
+
if args.scan_only:
|
|
137
|
+
print('Sorry, --dry option not implemented yet, bye.')
|
|
138
|
+
sys.exit(0)
|
|
136
139
|
return args
|
|
137
140
|
|
|
138
141
|
def get_recent_mix(SND_dir, vid):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: tictacsync
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2a0
|
|
4
4
|
Summary: command for syncing audio video recordings
|
|
5
5
|
Home-page: https://tictacsync.org/
|
|
6
6
|
Author: Raymond Lutz
|
|
@@ -58,7 +58,7 @@ Then pip install the syncing program:
|
|
|
58
58
|
This should install python dependencies _and_ the `tictacsync` command.
|
|
59
59
|
## Usage
|
|
60
60
|
|
|
61
|
-
Download multiple sample files [here](https://nuage.lutz.quebec/s/
|
|
61
|
+
Download multiple sample files [here](https://nuage.lutz.quebec/s/D4DgzHrLaAjqNmY) (625 MB, sorry) unzip and run:
|
|
62
62
|
|
|
63
63
|
> tictacsync dailies/loose
|
|
64
64
|
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:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|