tictacsync 1.0.1a0__tar.gz → 1.1.0a0__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.1a0/tictacsync.egg-info → tictacsync-1.1.0a0}/PKG-INFO +3 -3
- {tictacsync-1.0.1a0 → tictacsync-1.1.0a0}/README.md +1 -1
- {tictacsync-1.0.1a0 → tictacsync-1.1.0a0}/setup.py +4 -2
- {tictacsync-1.0.1a0 → tictacsync-1.1.0a0}/tictacsync/device_scanner.py +41 -29
- {tictacsync-1.0.1a0 → tictacsync-1.1.0a0}/tictacsync/entry.py +46 -159
- tictacsync-1.1.0a0/tictacsync/mamconf.py +175 -0
- tictacsync-1.1.0a0/tictacsync/mamsync.py +387 -0
- {tictacsync-1.0.1a0 → tictacsync-1.1.0a0}/tictacsync/multi2polywav.py +4 -2
- {tictacsync-1.0.1a0 → tictacsync-1.1.0a0}/tictacsync/newmix.py +217 -57
- {tictacsync-1.0.1a0 → tictacsync-1.1.0a0}/tictacsync/timeline.py +137 -84
- {tictacsync-1.0.1a0 → tictacsync-1.1.0a0}/tictacsync/yaltc.py +2 -2
- {tictacsync-1.0.1a0 → tictacsync-1.1.0a0/tictacsync.egg-info}/PKG-INFO +3 -3
- {tictacsync-1.0.1a0 → tictacsync-1.1.0a0}/tictacsync.egg-info/SOURCES.txt +2 -0
- {tictacsync-1.0.1a0 → tictacsync-1.1.0a0}/tictacsync.egg-info/entry_points.txt +2 -0
- {tictacsync-1.0.1a0 → tictacsync-1.1.0a0}/LICENSE +0 -0
- {tictacsync-1.0.1a0 → tictacsync-1.1.0a0}/setup.cfg +0 -0
- {tictacsync-1.0.1a0 → tictacsync-1.1.0a0}/tictacsync/__init__.py +0 -0
- {tictacsync-1.0.1a0 → tictacsync-1.1.0a0}/tictacsync/remergemix.py +0 -0
- {tictacsync-1.0.1a0 → tictacsync-1.1.0a0}/tictacsync.egg-info/dependency_links.txt +0 -0
- {tictacsync-1.0.1a0 → tictacsync-1.1.0a0}/tictacsync.egg-info/not-zip-safe +0 -0
- {tictacsync-1.0.1a0 → tictacsync-1.1.0a0}/tictacsync.egg-info/requires.txt +0 -0
- {tictacsync-1.0.1a0 → tictacsync-1.1.0a0}/tictacsync.egg-info/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: tictacsync
|
|
3
|
-
Version: 1.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 1.1.0a0
|
|
4
|
+
Summary: commands for syncing audio video recordings
|
|
5
5
|
Home-page: https://tictacsync.org/
|
|
6
6
|
Author: Raymond Lutz
|
|
7
7
|
Author-email: lutzrayblog@mac.com
|
|
@@ -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/4jw4xgqysLPS8EQ/download/dailies1_3.zip) (700+ 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/4jw4xgqysLPS8EQ/download/dailies1_3.zip) (700+ 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:
|
|
@@ -30,11 +30,13 @@ setup(
|
|
|
30
30
|
"console_scripts": [
|
|
31
31
|
'tictacsync = tictacsync.entry:main',
|
|
32
32
|
'newmix = tictacsync.newmix:main',
|
|
33
|
+
'mamsync = tictacsync.mamsync:main',
|
|
34
|
+
'mamconf = tictacsync.mamconf:main',
|
|
33
35
|
'multi2polywav = tictacsync.multi2polywav:main',
|
|
34
36
|
]
|
|
35
37
|
},
|
|
36
|
-
version = '1.0
|
|
37
|
-
description = "
|
|
38
|
+
version = '1.1.0-alpha',
|
|
39
|
+
description = "commands for syncing audio video recordings",
|
|
38
40
|
long_description_content_type='text/markdown',
|
|
39
41
|
long_description = long_descr,
|
|
40
42
|
include_package_data=True,
|
|
@@ -18,8 +18,10 @@ M4V SVI 3GP 3G2 MXF ROQ NSV FLV F4V F4P F4A F4B 3GP AA AAC AAX ACT AIFF ALAC
|
|
|
18
18
|
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
|
+
audio_ext = 'aiff wav mp3'.split()
|
|
22
|
+
|
|
21
23
|
from dataclasses import dataclass
|
|
22
|
-
import ffmpeg, os, sys
|
|
24
|
+
import ffmpeg, os, sys, shutil
|
|
23
25
|
from os import listdir
|
|
24
26
|
from os.path import isfile, join, isdir
|
|
25
27
|
from collections import namedtuple
|
|
@@ -28,7 +30,7 @@ from pprint import pformat
|
|
|
28
30
|
# from collections import defaultdict
|
|
29
31
|
from loguru import logger
|
|
30
32
|
# import pathlib, os.path
|
|
31
|
-
import sox, tempfile
|
|
33
|
+
import sox, tempfile, platformdirs, filecmp
|
|
32
34
|
# from functools import reduce
|
|
33
35
|
from rich import print
|
|
34
36
|
from itertools import groupby
|
|
@@ -36,8 +38,12 @@ from itertools import groupby
|
|
|
36
38
|
# import distance
|
|
37
39
|
try:
|
|
38
40
|
from . import multi2polywav
|
|
41
|
+
from . import mamsync
|
|
42
|
+
from . import mamconf
|
|
39
43
|
except:
|
|
40
44
|
import multi2polywav
|
|
45
|
+
import mamsync
|
|
46
|
+
import mamconf
|
|
41
47
|
|
|
42
48
|
MCCDIR = 'SyncedMulticamClips'
|
|
43
49
|
SYNCEDFOLDER = 'SyncedMedia'
|
|
@@ -244,16 +250,7 @@ class Scanner:
|
|
|
244
250
|
CAMs = [d for d in devices if d.dev_type == 'CAM']
|
|
245
251
|
return len(set(CAMs))
|
|
246
252
|
|
|
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):
|
|
253
|
+
def scan_media_and_build_devices_UID(self, synced_root = None):
|
|
257
254
|
"""
|
|
258
255
|
Scans Scanner.top_directory recursively for files with known audio-video
|
|
259
256
|
extensions. For each file found, a device fingerprint is obtained from
|
|
@@ -269,20 +266,36 @@ class Scanner:
|
|
|
269
266
|
Sets Scanner.input_structure = 'loose'|'ordered'
|
|
270
267
|
|
|
271
268
|
"""
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
269
|
+
logger.debug(f'on entry synced_root: {synced_root}')
|
|
270
|
+
if synced_root != None: # mam mode
|
|
271
|
+
p = Path(platformdirs.user_data_dir('mamsync', 'plutz'))/mamconf.LOG_FILE
|
|
272
|
+
with open(p, 'r') as fh:
|
|
273
|
+
done = set(fh.read().split()) # sets of strings of abs path
|
|
274
|
+
logger.debug(f'done clips: {pformat(done)}')
|
|
275
|
+
files = Path(self.top_directory).rglob('*')
|
|
276
|
+
clip_paths = []
|
|
277
|
+
some_done = False
|
|
278
|
+
for raw_path in files:
|
|
279
|
+
if raw_path.suffix[1:] in av_file_extensions:
|
|
280
|
+
if SYNCEDFOLDER not in raw_path.parts: # SyncedMedia
|
|
281
|
+
if MCCDIR not in raw_path.parts: # SyncedMulticamClips
|
|
282
|
+
if '_ISO' not in [part[-4:] for part in raw_path.parts]: # exclude ISO wav files
|
|
283
|
+
if synced_root != None and str(raw_path) in done:
|
|
284
|
+
logger.debug(f'{raw_path} done')
|
|
285
|
+
some_done = True
|
|
286
|
+
continue
|
|
287
|
+
else:
|
|
288
|
+
clip_paths.append(raw_path)
|
|
289
|
+
if some_done:
|
|
290
|
+
print('Somme media files were already synced...')
|
|
291
|
+
logger.debug('found media files %s'%clip_paths)
|
|
292
|
+
if len(clip_paths) == 0:
|
|
293
|
+
print('No media found, bye.')
|
|
294
|
+
sys.exit(0)
|
|
295
|
+
# self.found_media_files = []
|
|
296
|
+
# self.input_structure = 'loose'
|
|
297
|
+
# return
|
|
298
|
+
parents = [p.parent for p in clip_paths]
|
|
286
299
|
logger.debug('found parents %s'%pformat(parents))
|
|
287
300
|
# True if all elements are identical
|
|
288
301
|
AV_files_have_same_parent = parents.count(parents[0]) == len(parents)
|
|
@@ -296,7 +309,7 @@ class Scanner:
|
|
|
296
309
|
# check later if inside each folder, media have same device
|
|
297
310
|
# for now, we'll guess structure is 'ordered'
|
|
298
311
|
self.input_structure = 'ordered'
|
|
299
|
-
for p in
|
|
312
|
+
for p in clip_paths:
|
|
300
313
|
new_media = media_at_path(self.input_structure, p) # dev UID set here
|
|
301
314
|
self.found_media_files.append(new_media)
|
|
302
315
|
# for non UIDed try building UID from filenam
|
|
@@ -379,7 +392,7 @@ class Scanner:
|
|
|
379
392
|
no_name_devices = [m.device for m in self.found_media_files
|
|
380
393
|
if not m.device.name]
|
|
381
394
|
# possible if self.input_structure == 'loose'
|
|
382
|
-
def _try_name_from_metadata(media):
|
|
395
|
+
def _try_name_from_metadata(media): # unused for now
|
|
383
396
|
# search model and make from fprobe
|
|
384
397
|
file = Path(media.path)
|
|
385
398
|
logger.debug('trying to find maker model for %s'%file)
|
|
@@ -395,7 +408,6 @@ class Scanner:
|
|
|
395
408
|
# could reside in ['format','tags','com.apple.quicktime.model'],
|
|
396
409
|
# or ['format','tags','model'],
|
|
397
410
|
# or ['streams'][0]['tags']['vendor_id']) :-(
|
|
398
|
-
|
|
399
411
|
for anon_dev in no_name_devices:
|
|
400
412
|
medias = self.get_media_for_device(anon_dev)
|
|
401
413
|
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'],
|
|
@@ -236,39 +164,17 @@ def main():
|
|
|
236
164
|
quit()
|
|
237
165
|
if args.verbose_output:
|
|
238
166
|
logger.add(sys.stderr, level="DEBUG")
|
|
239
|
-
|
|
240
|
-
# logger.add(sys.stdout, filter=lambda r: r["function"] == "
|
|
167
|
+
|
|
168
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "_write_ISOs")
|
|
169
|
+
logger.add(sys.stdout, filter=lambda r: r["function"] == "_build_and_write_audio")
|
|
241
170
|
#
|
|
242
171
|
# logger.add(sys.stdout, filter="__main__")
|
|
243
172
|
# logger.add(sys.stdout, filter="yaltc")
|
|
244
173
|
|
|
245
|
-
# logger.add(sys.stdout, filter=lambda r: r["function"] == "
|
|
174
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "move_multicam_to_dir")
|
|
246
175
|
# logger.add(sys.stdout, filter=lambda r: r["function"] == "_dev_type_for_name")
|
|
247
176
|
# logger.add(sys.stdout, filter=lambda r: r["function"] == "scan_media_and_build_devices_UID")
|
|
248
177
|
|
|
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
178
|
top_dir = args.path[0]
|
|
273
179
|
if os.path.isfile(top_dir):
|
|
274
180
|
file = top_dir
|
|
@@ -276,15 +182,6 @@ def main():
|
|
|
276
182
|
if not os.path.isdir(top_dir):
|
|
277
183
|
print('%s is not a directory or doesnt exist.'%top_dir)
|
|
278
184
|
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
185
|
multi2polywav.poly_all(top_dir)
|
|
289
186
|
scanner = device_scanner.Scanner(top_dir, stay_silent=args.terse)
|
|
290
187
|
scanner.scan_media_and_build_devices_UID()
|
|
@@ -307,20 +204,7 @@ def main():
|
|
|
307
204
|
maxch = len(devices)
|
|
308
205
|
for i, d in enumerate(devices):
|
|
309
206
|
print('\t%i - %s'%(i+1, d.name))
|
|
310
|
-
|
|
311
|
-
print('\nEnter your choice:', end='')
|
|
312
|
-
choice = input()
|
|
313
|
-
try:
|
|
314
|
-
choice = int(choice)
|
|
315
|
-
except:
|
|
316
|
-
print('Please use numeric digits.')
|
|
317
|
-
continue
|
|
318
|
-
if choice not in list(range(1, maxch + 1)):
|
|
319
|
-
print('Please enter a number in [1..%i]'%maxch)
|
|
320
|
-
continue
|
|
321
|
-
break
|
|
322
|
-
ref_device = list(devices)[choice - 1]
|
|
323
|
-
# ref_device = list(devices)[3 - 1]
|
|
207
|
+
ref_device = list(devices)[3 - 1]
|
|
324
208
|
print('When only audio recordings are present, ISOs files will be cut and written.')
|
|
325
209
|
if not args.terse:
|
|
326
210
|
if scanner.input_structure == 'ordered':
|
|
@@ -369,7 +253,7 @@ def main():
|
|
|
369
253
|
for rec in recordings:
|
|
370
254
|
# print(rec, rec.device == ref_device)
|
|
371
255
|
if rec.device == ref_device:
|
|
372
|
-
rec.
|
|
256
|
+
rec.is_audio_reference = True
|
|
373
257
|
if not args.terse:
|
|
374
258
|
table = Table(title="tictacsync results")
|
|
375
259
|
table.add_column("Recording\n", justify="center", style='gold1')
|
|
@@ -406,13 +290,6 @@ def main():
|
|
|
406
290
|
console.print(table)
|
|
407
291
|
print()
|
|
408
292
|
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
293
|
if len(recordings_with_time) < 2:
|
|
417
294
|
if not args.terse:
|
|
418
295
|
print('\nNothing to sync, exiting.\n')
|
|
@@ -429,11 +306,20 @@ def main():
|
|
|
429
306
|
asked_ISOs = False
|
|
430
307
|
# output_dir = args.o
|
|
431
308
|
# if args.verbose_output or args.terse: # verbose, so no progress bars
|
|
309
|
+
print('Merging...')
|
|
310
|
+
# for merger in matcher.mergers:
|
|
311
|
+
# merger.build_audio_and_write_merged_media(top_dir,
|
|
312
|
+
# args.dont_write_cam_folder,
|
|
313
|
+
# asked_ISOs,
|
|
314
|
+
# audio_REC_only)
|
|
432
315
|
for merger in matcher.mergers:
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
316
|
+
if audio_REC_only:
|
|
317
|
+
# rare
|
|
318
|
+
merger._build_and_write_audio(top_dir)
|
|
319
|
+
else:
|
|
320
|
+
# almost always syncing audio to video clips
|
|
321
|
+
merger._build_audio_and_write_video(top_dir,
|
|
322
|
+
args.dont_write_cam_folder, asked_ISOs)
|
|
437
323
|
if not args.terse:
|
|
438
324
|
print("\n")
|
|
439
325
|
# find out where files were written
|
|
@@ -447,23 +333,24 @@ def main():
|
|
|
447
333
|
nameAnd2Parents = Path('').joinpath(*final_p.parts[-2:])
|
|
448
334
|
print(' became [gold1]%s[/gold1]'%nameAnd2Parents)
|
|
449
335
|
# matcher._build_otio_tracks_for_cam()
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
if
|
|
460
|
-
|
|
461
|
-
|
|
336
|
+
if not audio_REC_only:
|
|
337
|
+
matcher.set_up_clusters() # multicam
|
|
338
|
+
matcher.shrink_gaps_between_takes(args.timelineoffset)
|
|
339
|
+
logger.debug('matcher.multicam_clips_clusters %s'%
|
|
340
|
+
pformat(matcher.multicam_clips_clusters))
|
|
341
|
+
# clusters is list of {'end': t1, 'start': t2, 'vids': [r1,r3]}
|
|
342
|
+
# really_clusters is True if one of them has len() > 1
|
|
343
|
+
really_clusters = any([len(cl['vids']) > 1 for cl
|
|
344
|
+
in matcher.multicam_clips_clusters])
|
|
345
|
+
if really_clusters:
|
|
346
|
+
if scanner.input_structure == 'loose':
|
|
347
|
+
print('\nThere are synced multicam clips but without structured folders')
|
|
348
|
+
print('they were not grouped together under the same folder.')
|
|
349
|
+
else:
|
|
350
|
+
matcher.move_multicam_to_dir()
|
|
462
351
|
else:
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
logger.debug('not really a multicam cluster, nothing to move')
|
|
466
|
-
sys.exit(0)
|
|
352
|
+
logger.debug('not really a multicam cluster, nothing to move')
|
|
353
|
+
sys.exit(0)
|
|
467
354
|
|
|
468
355
|
if __name__ == '__main__':
|
|
469
356
|
main()
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import argparse, platformdirs, configparser, sys
|
|
2
|
+
from loguru import logger
|
|
3
|
+
from pprint import pprint, pformat
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
CONF_FILE = 'mamsync.cfg'
|
|
8
|
+
LOG_FILE = 'mamdone.txt'
|
|
9
|
+
|
|
10
|
+
logger.remove()
|
|
11
|
+
# logger.add(sys.stdout, level="DEBUG")
|
|
12
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "write_conf")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def print_out_conf(raw_root, synced_root, snd_root, proxies=''):
|
|
16
|
+
print(f'RAWROOT (source with TC): "{raw_root}"')
|
|
17
|
+
print(f'SYNCEDROOT (destination of synced folder): "{synced_root}"')
|
|
18
|
+
print(f'SNDROOT (destination of ISOs sound files): "{snd_root}"')
|
|
19
|
+
if proxies != '':
|
|
20
|
+
print(f'PROXIES (NLE proxy clips folder): "{proxies}"')
|
|
21
|
+
|
|
22
|
+
def write_conf(conf_key, conf_val):
|
|
23
|
+
# args are pahtlib.Paths.
|
|
24
|
+
# RAWROOT: files with TC (and ROLL folders), as is from cameras
|
|
25
|
+
# SYNCEDROOT: synced and no more TC (ROLL flattened)
|
|
26
|
+
# Writes configuration on filesystem for later retrieval
|
|
27
|
+
# Clears log of already synced clips.
|
|
28
|
+
conf_dir = platformdirs.user_config_dir('mamsync', 'plutz', ensure_exists=True)
|
|
29
|
+
current_values = dict(zip(['RAWROOT', 'SYNCEDROOT', 'SNDROOT', 'PROXIES'],
|
|
30
|
+
get_proj()))
|
|
31
|
+
logger.debug(f'old values {current_values}')
|
|
32
|
+
current_values[conf_key] = conf_val
|
|
33
|
+
logger.debug(f'updated values {current_values}')
|
|
34
|
+
conf_file = Path(conf_dir)/CONF_FILE
|
|
35
|
+
logger.debug('writing config in %s'%conf_file)
|
|
36
|
+
# print(f'\nWriting folders paths in configuration file "{conf_file}"')
|
|
37
|
+
# print_out_conf(raw_root, synced_root, snd_root)
|
|
38
|
+
conf_prs = configparser.ConfigParser()
|
|
39
|
+
conf_prs['SECTION1'] = current_values
|
|
40
|
+
with open(conf_file, 'w') as configfile_handle:
|
|
41
|
+
conf_prs.write(configfile_handle)
|
|
42
|
+
with open(conf_file, 'r') as configfile_handle:
|
|
43
|
+
logger.debug(f'config file content: \n{configfile_handle.read()}')
|
|
44
|
+
|
|
45
|
+
def get_proj(print_conf_stdout=False):
|
|
46
|
+
# check if user started a project before.
|
|
47
|
+
# stored in platformdirs.user_config_dir
|
|
48
|
+
# returns a tuple of strings (RAWROOT, SYNCEDROOTS, SNDROOT, PROXIES)
|
|
49
|
+
# if any, or a tuple of 4 empty strings '' otherwise.
|
|
50
|
+
# print location of conf file if print_conf_stdout
|
|
51
|
+
conf_dir = platformdirs.user_config_dir('mamsync', 'plutz')
|
|
52
|
+
conf_file = Path(conf_dir)/CONF_FILE
|
|
53
|
+
logger.debug('try reading config in %s'%conf_file)
|
|
54
|
+
if print_conf_stdout:
|
|
55
|
+
print(f'\nTrying to read configuration from file {conf_file}')
|
|
56
|
+
if conf_file.exists():
|
|
57
|
+
conf_prs = configparser.ConfigParser()
|
|
58
|
+
conf_prs.read(conf_file)
|
|
59
|
+
try:
|
|
60
|
+
RAWROOT = conf_prs.get('SECTION1', 'RAWROOT')
|
|
61
|
+
except configparser.NoOptionError:
|
|
62
|
+
RAWROOT = ''
|
|
63
|
+
try:
|
|
64
|
+
SYNCEDROOT = conf_prs.get('SECTION1', 'SYNCEDROOT')
|
|
65
|
+
except configparser.NoOptionError:
|
|
66
|
+
SYNCEDROOT = ''
|
|
67
|
+
try:
|
|
68
|
+
PROXIES = conf_prs.get('SECTION1', 'PROXIES')
|
|
69
|
+
except configparser.NoOptionError:
|
|
70
|
+
PROXIES = ''
|
|
71
|
+
try:
|
|
72
|
+
SNDROOT = conf_prs.get('SECTION1', 'SNDROOT')
|
|
73
|
+
except configparser.NoOptionError:
|
|
74
|
+
SNDROOT = ''
|
|
75
|
+
logger.debug('read from conf: RAWROOT= %s SYNCEDROOT= %s SNDROOT=%s PROXIES=%s'%
|
|
76
|
+
(RAWROOT, SYNCEDROOT, SNDROOT, PROXIES))
|
|
77
|
+
return RAWROOT, SYNCEDROOT, SNDROOT, PROXIES
|
|
78
|
+
else:
|
|
79
|
+
logger.debug(f'no config file found at {conf_file}')
|
|
80
|
+
print('No configuration found.')
|
|
81
|
+
return '', '', '', ''
|
|
82
|
+
|
|
83
|
+
def new_parser():
|
|
84
|
+
parser = argparse.ArgumentParser()
|
|
85
|
+
parser.add_argument('--rawroot',
|
|
86
|
+
nargs = 1,
|
|
87
|
+
dest='rawroot',
|
|
88
|
+
help='Sets new value for raw root folder (i.e.: clips with TC)')
|
|
89
|
+
parser.add_argument('--syncedroot',
|
|
90
|
+
nargs = 1,
|
|
91
|
+
dest='syncedroot',
|
|
92
|
+
help="""Sets where the synced files will be written, to be used by the NLE. Will contain a mirror copy of RAWROOT """)
|
|
93
|
+
parser.add_argument('--proxies',
|
|
94
|
+
nargs = 1,
|
|
95
|
+
dest='proxies',
|
|
96
|
+
help='Sets where the proxy files are stored by the NLE')
|
|
97
|
+
parser.add_argument('--sndfolder',
|
|
98
|
+
nargs = 1,
|
|
99
|
+
dest='sndfolder',
|
|
100
|
+
help='Sets new value for sound folder (where ISOs sound files will be stored)')
|
|
101
|
+
parser.add_argument('--clearconf',
|
|
102
|
+
action='store_true',
|
|
103
|
+
dest='clearconf',
|
|
104
|
+
help='Clear configured values.')
|
|
105
|
+
parser.add_argument('--showconf',
|
|
106
|
+
action='store_true',
|
|
107
|
+
dest='showconf',
|
|
108
|
+
help='Show current configured values.')
|
|
109
|
+
return parser
|
|
110
|
+
|
|
111
|
+
def main():
|
|
112
|
+
parser = new_parser()
|
|
113
|
+
args = parser.parse_args()
|
|
114
|
+
logger.debug(f'arguments from argparse {args}')
|
|
115
|
+
if args.rawroot:
|
|
116
|
+
val = args.rawroot[0]
|
|
117
|
+
write_conf('RAWROOT', val)
|
|
118
|
+
print(f'Set source folder of unsynced clips (rawroot) to:\n{val}')
|
|
119
|
+
sys.exit(0)
|
|
120
|
+
if args.syncedroot:
|
|
121
|
+
val = args.syncedroot[0]
|
|
122
|
+
write_conf('SYNCEDROOT', args.syncedroot[0])
|
|
123
|
+
print(f'Set destination folder of synced clips (syncedroot) to:\n{val}')
|
|
124
|
+
sys.exit(0)
|
|
125
|
+
if args.proxies:
|
|
126
|
+
val = args.proxies[0]
|
|
127
|
+
write_conf('PROXIES', args.proxies[0])
|
|
128
|
+
print(f'Set proxies folder to:\n{val}')
|
|
129
|
+
sys.exit(0)
|
|
130
|
+
if args.sndfolder:
|
|
131
|
+
val = args.sndfolder[0]
|
|
132
|
+
write_conf('SNDROOT', args.sndfolder[0])
|
|
133
|
+
print(f'Set destination folder of ISOs sound files (sndfolder) to:\n{val}')
|
|
134
|
+
sys.exit(0)
|
|
135
|
+
if args.clearconf:
|
|
136
|
+
write_conf('RAWROOT', '')
|
|
137
|
+
write_conf('SYNCEDROOT', '')
|
|
138
|
+
write_conf('SNDROOT', '')
|
|
139
|
+
write_conf('PROXIES', '')
|
|
140
|
+
print_out_conf('','','','')
|
|
141
|
+
sys.exit(0)
|
|
142
|
+
if args.showconf:
|
|
143
|
+
get_proj()
|
|
144
|
+
print_out_conf(*get_proj(True))
|
|
145
|
+
sys.exit(0)
|
|
146
|
+
# roots = get_proj(False)
|
|
147
|
+
# if any([r == '' for r in roots]):
|
|
148
|
+
# print("Can't sync if some folders are not set:")
|
|
149
|
+
# print_out_conf(*get_proj())
|
|
150
|
+
# print('Bye.')
|
|
151
|
+
# sys.exit(0)
|
|
152
|
+
# for r in roots:
|
|
153
|
+
# if not r.is_absolute():
|
|
154
|
+
# print(f'\rError: folder {r} must be an absolute path. Bye')
|
|
155
|
+
# sys.exit(0)
|
|
156
|
+
# if not r.exists():
|
|
157
|
+
# print(f'\rError: folder {r} does not exist. Bye')
|
|
158
|
+
# sys.exit(0)
|
|
159
|
+
# if not r.is_dir():
|
|
160
|
+
# print(f'\rError: path {r} is not a folder. Bye')
|
|
161
|
+
# sys.exit(0)
|
|
162
|
+
# raw_root, synced_root, snd_root = roots
|
|
163
|
+
# if args.sub_dir != None:
|
|
164
|
+
# top_dir = args.sub_dir
|
|
165
|
+
# logger.debug(f'sub _dir: {args.sub_dir}')
|
|
166
|
+
# if not Path(top_dir).exists():
|
|
167
|
+
# print(f"\rError: folder {top_dir} doesn't exist, bye.")
|
|
168
|
+
# sys.exit(0)
|
|
169
|
+
# else:
|
|
170
|
+
# top_dir = raw_root
|
|
171
|
+
# if args.resync:
|
|
172
|
+
# clear_log()
|
|
173
|
+
|
|
174
|
+
if __name__ == '__main__':
|
|
175
|
+
main()
|