tictacsync 0.98a0__py3-none-any.whl → 1.4.0b0__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/timeline.py CHANGED
@@ -9,7 +9,7 @@ from rich import print
9
9
  from itertools import groupby
10
10
  # import opentimelineio as otio
11
11
  from datetime import timedelta
12
- import shutil, os, sys, stat
12
+ import shutil, os, sys, stat, subprocess
13
13
  from subprocess import Popen, PIPE
14
14
  from pprint import pformat
15
15
 
@@ -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
 
@@ -55,6 +55,10 @@ def _pathname(tempfile_or_path) -> str:
55
55
  else:
56
56
  raise Exception('%s should be Path or tempfile...'%tempfile_or_path)
57
57
 
58
+ def ffprobe_duration(f):
59
+ pr = ffmpeg.probe(f)
60
+ return pr['format']['duration']
61
+
58
62
  # utility for printing groupby results
59
63
  def print_grby(grby):
60
64
  for key, keylist in grby:
@@ -202,7 +206,7 @@ def _sox_multi2stereo(multichan_tmpfl, stereo_trxs) -> tempfile.NamedTemporaryFi
202
206
  stereo_tempfile = tempfile.NamedTemporaryFile(suffix='.wav',
203
207
  delete=DEL_TEMP)
204
208
  tfm = sox.Transformer()
205
- tfm.channels(1)
209
+ tfm.channels(1) # why ? https://pysox.readthedocs.io/en/latest/api.html?highlight=channels#sox.transform.Transformer.channels
206
210
  status = tfm.build(_pathname(multichan_tmpfl),_pathname(stereo_tempfile))
207
211
  logger.debug('n chan ouput: %s'%
208
212
  sox.file_info.channels(_pathname(stereo_tempfile)))
@@ -401,6 +405,7 @@ class AudioStitcherVideoMerger:
401
405
  Returns audio recordings that overlap self.videoclip.
402
406
  Simply keys of self.soxed_audio dict
403
407
  """
408
+ logger.debug(f'soxed_audio {pformat(self.soxed_audio)}')
404
409
  return list(self.soxed_audio.keys())
405
410
 
406
411
  def _get_audio_devices(self):
@@ -411,15 +416,16 @@ class AudioStitcherVideoMerger:
411
416
  return devices
412
417
 
413
418
  def _get_secondary_audio_devices(self):
414
- devices = set([r.device for r in self.get_matched_audio_recs()])
415
- logger.debug('get_matched_audio_recs: %s'%
416
- pformat(self.get_matched_audio_recs()))
417
- logger.debug('devices %s'%devices)
418
- return devices
419
+ # when only audio devices are synced.
420
+ # identical to _get_audio_devices()...
421
+ # name changed for clarity
422
+ return self._get_audio_devices()
419
423
 
420
424
  def _get_all_recordings_for(self, device):
421
425
  # return recordings for a particular device, sorted by time
422
- recs = [a for a in self.get_matched_audio_recs() if a.device == device]
426
+ recs = self.get_matched_audio_recs()
427
+ logger.debug(f'device: {device.name} matched audio recs: {recs}')
428
+ recs = [a for a in recs if a.device == device]
423
429
  recs.sort(key=lambda r: r.start_time)
424
430
  return recs
425
431
 
@@ -652,14 +658,16 @@ class AudioStitcherVideoMerger:
652
658
  self.soxed_audio[audio_rec] = output_fh
653
659
  return output_fh
654
660
 
655
- def _write_ISOs(self, edited_audio_all_devices):
661
+ def _write_ISOs(self, edited_audio_all_devices,
662
+ snd_root=None, synced_root=None, raw_root=None, audio_only=False):
656
663
  """
664
+ [TODO: this multiline doc is obsolete]
657
665
  Writes isolated audio files that were synced to synced_clip_file,
658
666
  each track will have its dedicated monofile, named sequentially or with
659
- the name find in TRACKSFN if any, see Scanner._get_tracks_from_file()
667
+ the name find in TRACKSFILE if any, see Scanner._get_tracks_from_file()
660
668
 
661
669
  edited_audio_all_devices:
662
- a list of (name, mono_tempfile)
670
+ a list of (name, mono_tempfile, dev) -------------------------------------------> add argument for device for calling _get_all_recordings_for() for file for metada
663
671
 
664
672
  Returns nothing, output is written to filesystem as below.
665
673
  ISOs subfolders structure when user invokes the --isos flag:
@@ -671,7 +679,7 @@ class AudioStitcherVideoMerger:
671
679
  canon24fps01.MOV ━━━━┓ name of clip is name of folder
672
680
  canon24fps01_ISO/ <━━┛
673
681
  chan_1.wav
674
- chan_2.wav
682
+ chan_2.wav [UPDATE FOR MAM mode]
675
683
  canon24fps02.MOV
676
684
  canon24fps01_ISO/
677
685
  chan_1.wav
@@ -707,20 +715,52 @@ class AudioStitcherVideoMerger:
707
715
  sox.file_info.duration(_pathname(out_tf)))
708
716
  logger.debug('video duration %.2f s'%
709
717
  self.videoclip.get_duration())
718
+ logger.debug(f'video {self.videoclip}')
710
719
  return out_tf
711
- synced_clip_file = self.videoclip.final_synced_file
712
- synced_clip_dir = synced_clip_file.parent
720
+ def _meta_wav_dest(p1, p2, p3):
721
+ """
722
+ takes metadata from p1, sound from p2 and combine them to create p3.
723
+ arguments are pathlib.Path or string;
724
+ returns nothing.
725
+ """
726
+ f1, f2, f3 = [_pathname(p) for p in [p1, p2, p3]]
727
+ process_list = ['ffmpeg', '-y', '-loglevel', 'quiet', '-nostats', '-hide_banner',
728
+ '-i', f1, '-i', f2, '-map', '1',
729
+ '-map_metadata', '0', '-c', 'copy', f3]
730
+ proc = subprocess.run(process_list)
731
+ logger.debug(f'synced_clip_file raw')
732
+ if snd_root == None:
733
+ # alongside mode
734
+ synced_clip_file = self.videoclip.final_synced_file
735
+ logger.debug('alongside mode')
736
+ synced_clip_dir = synced_clip_file.parent
737
+ else:
738
+ # MAM mode
739
+ synced_clip_file = self.videoclip.AVpath
740
+ logger.debug('MAM mode')
741
+ rel = synced_clip_file.parent.relative_to(raw_root)
742
+ synced_clip_dir = Path(snd_root)/Path(raw_root).name/rel
743
+ logger.debug(f'synced_clip_dir: {synced_clip_dir}')
713
744
  # build ISOs subfolders structure, see comment string below
714
745
  video_stem_WO_suffix = synced_clip_file.stem
715
- ISOdir = synced_clip_dir/(video_stem_WO_suffix + '_ISO')
746
+ # ISOdir = synced_clip_dir/(video_stem_WO_suffix + 'ISO')
747
+ ISOdir = synced_clip_dir/(video_stem_WO_suffix + '_SND')/'ISOfiles'
716
748
  os.makedirs(ISOdir, exist_ok=True)
717
749
  logger.debug('edited_audio_all_devices %s'%edited_audio_all_devices)
718
750
  logger.debug('ISOdir %s'%ISOdir)
719
- for name, mono_tmpfl in edited_audio_all_devices:
720
- destination = ISOdir/('%s.wav'%name)
751
+ for name, mono_tmpfl, device in edited_audio_all_devices:
752
+ logger.debug(f'name:{name} mono_tmpfl:{mono_tmpfl} device:{pformat(device)}')
753
+ # destination = ISOdir/(f'{video_stem_WO_suffix}_{name}.wav')
754
+ destination = ISOdir/(f'{name}_{video_stem_WO_suffix}.wav')
721
755
  mono_tmpfl_trimpad = _fit_length(mono_tmpfl)
722
- shutil.copy(_pathname(mono_tmpfl_trimpad), destination)
723
- remove_write_permissions(destination)
756
+ # if audio_only, self.ref_audio does not have itself as matching audio
757
+ if audio_only and device == self.ref_audio.device:
758
+ first_rec = self.ref_audio
759
+ else:
760
+ first_rec = self._get_all_recordings_for(device)[0]
761
+ logger.debug(f'will use {first_rec} for metadata source to copy over {destination}')
762
+ _meta_wav_dest(first_rec.AVpath, mono_tmpfl_trimpad, destination)
763
+ # remove_write_permissions(destination)
724
764
  logger.debug('destination:%s'%destination)
725
765
 
726
766
  def _get_device_mix(self, device, multichan_tmpfl) -> tempfile.NamedTemporaryFile:
@@ -794,8 +834,8 @@ class AudioStitcherVideoMerger:
794
834
  if device.ttc + 1 != device.tracks.ttc: # warn and quit
795
835
  print('Error: TicTacCode channel detected is [gold1]%i[/gold1]'%
796
836
  (device.ttc), end=' ')
797
- print('and [gold1]%s[/gold1] for the device [gold1]%s[/gold1] specifies channel [gold1]%i[/gold1],'%
798
- (device_scanner.TRACKSFN,
837
+ print('and file [gold1]%s[/gold1]\nfor the device [gold1]%s[/gold1] specifies channel [gold1]%i[/gold1],'%
838
+ (device.folder/Path(yaltc.TRACKSFILE),
799
839
  device.name, device.tracks.ttc-1))
800
840
  print('Please correct the discrepancy and rerun. Quitting.')
801
841
  sys.exit(1)
@@ -866,19 +906,7 @@ class AudioStitcherVideoMerger:
866
906
  stereo_files = mic_stereo_files + new_stereo_files
867
907
  return _sox_mix_files(stereo_files)
868
908
 
869
- def build_audio_and_write_merged_media(self, top_dir,
870
- dont_write_cam_folder, asked_ISOs, audio_REC_only):
871
- # simply bifurcates depending if ref media is video (prob 99%)
872
- # (then audio_REC_only == False)
873
- # or if ref media is audio (no camera detected, 1% of cases)
874
- # (with audio_REC_only == True)
875
- if not audio_REC_only:
876
- self._build_audio_and_write_video(top_dir,
877
- dont_write_cam_folder, asked_ISOs)
878
- else:
879
- self._build_and_write_audio(top_dir, anchor_dir)
880
-
881
- def _build_and_write_audio(self, top_dir, anchor_dir):
909
+ def _build_and_write_audio(self, top_dir, anchor_dir=None):
882
910
  """
883
911
  This is called when only audio recorders were found (no cam).
884
912
 
@@ -910,8 +938,7 @@ class AudioStitcherVideoMerger:
910
938
  self.synced_clip_dir = synced_clip_dir
911
939
  os.makedirs(synced_clip_dir, exist_ok=True)
912
940
  logger.debug('synced_clip_dir is: %s'%synced_clip_dir)
913
- synced_clip_file = synced_clip_dir/\
914
- Path(self.videoclip.new_rec_name).name
941
+ synced_clip_file = synced_clip_dir/self.videoclip.AVpath.name
915
942
  logger.debug('editing files for synced_clip_file%s'%synced_clip_file)
916
943
  self.ref_audio.final_synced_file = synced_clip_file # relative path
917
944
  # Collecting edited audio by device, in (Device, tempfile) pairs:
@@ -971,14 +998,15 @@ class AudioStitcherVideoMerger:
971
998
  logger.debug('track_name %s'%track_name)
972
999
  if track_name[0] == '0': # muted, skip
973
1000
  continue
974
- names_audio_tempfiles.append((track_name, monotf))
975
- logger.debug('names_audio_tempfiles %s'%names_audio_tempfiles)
976
- self._write_ISOs(names_audio_tempfiles)
1001
+ names_audio_tempfiles.append((track_name, monotf, dev))
1002
+ logger.debug('names_audio_tempfiles %s'%pformat(names_audio_tempfiles))
1003
+ self._write_ISOs(names_audio_tempfiles, audio_only=True)
977
1004
  logger.debug('merged_audio_files_by_device %s'%
978
1005
  merged_audio_files_by_device)
979
1006
 
980
1007
  def _build_audio_and_write_video(self, top_dir, dont_write_cam_folder,
981
- asked_ISOs):
1008
+ asked_ISOs, synced_root = None,
1009
+ snd_root = None, raw_root = None):
982
1010
  """
983
1011
  top_dir: Path, directory where media were looked for
984
1012
 
@@ -1003,22 +1031,25 @@ class AudioStitcherVideoMerger:
1003
1031
  (top_dir, dont_write_cam_folder, asked_ISOs))
1004
1032
  logger.debug('device for rec %s: %s'%(self.videoclip,
1005
1033
  self.videoclip.device))
1006
- # eg, suppose the user called tictacsync with 'mondayPM' as top folder
1007
- # to scan for dailies (and 'somefolder' for output):
1008
- # if anchor_dir == None:
1009
- # then "alongside mode" i.e., in a folder neighboring the video clip
1010
- # WAS: synced_clip_dir = Path(top_dir)/OUT_DIR_DEFAULT # = mondayPM/SyncedMedia
1011
- logger.debug('"alongside mode" for clip: %s'%self.videoclip.AVpath)
1012
- synced_clip_dir = self.videoclip.AVpath.parent/OUT_DIR_DEFAULT
1013
- # else: # [TODO: replace original clip in a mirrored file structure]
1014
- # logger.debug('mirrored mode for clip: %s'%self.videoclip.AVpath)
1015
- # synced_clip_dir = Path(anchor_dir)/Path(top_dir).name # = somefolder/mondayPM
1016
- # if write_multicam_structure:
1017
- # device_name = self.videoclip.device.name
1018
- # synced_clip_dir = synced_clip_dir/device_name # = synced_clip_dir/ZOOM
1034
+ if synced_root == None:
1035
+ # alongside, within SyncedMedia dirs
1036
+ synced_clip_dir = self.videoclip.AVpath.parent/OUT_DIR_DEFAULT
1037
+ logger.debug('"alongside mode" for clip: %s'%self.videoclip.AVpath)
1038
+ logger.debug(f'will save in {synced_clip_dir}')
1039
+ else:
1040
+ # MAM mode
1041
+ logger.debug('MAM mode')
1042
+ synced_clip_dir = Path(synced_root)/str(self.videoclip.AVpath.parent)[1:] # strip leading /
1043
+ logger.debug(f'self.videoclip.AVpath.parent: {self.videoclip.AVpath.parent}')
1044
+ logger.debug(f'raw_root {raw_root}')
1045
+ # rel = self.videoclip.AVpath.parent.relative_to(raw_root).parent # removes ROLL01?
1046
+ rel = self.videoclip.AVpath.parent.relative_to(raw_root)
1047
+ logger.debug(f'relative path {rel}')
1048
+ synced_clip_dir = Path(synced_root)/Path(raw_root).name/rel
1049
+ logger.debug(f'will save in {synced_clip_dir}')
1019
1050
  self.synced_clip_dir = synced_clip_dir
1020
1051
  os.makedirs(synced_clip_dir, exist_ok=True)
1021
- logger.debug('synced_clip_dir is: %s'%synced_clip_dir)
1052
+ # logger.debug('synced_clip_dir is: %s'%synced_clip_dir)
1022
1053
  synced_clip_file = synced_clip_dir/self.videoclip.AVpath.name
1023
1054
  logger.debug('editing files for synced_clip_file %s'%synced_clip_file)
1024
1055
  self.videoclip.final_synced_file = synced_clip_file # relative path
@@ -1104,6 +1135,7 @@ class AudioStitcherVideoMerger:
1104
1135
  # generates track name for later if asked_ISOs
1105
1136
  # idx is from 0 to nchan-1 for this device
1106
1137
  if dev.tracks == None:
1138
+ logger.debug('dev.tracks == None')
1107
1139
  # no tracks.txt was found so use ascending numbers for name
1108
1140
  chan_name = 'chan%s'%str(idx+1).zfill(2)
1109
1141
  else:
@@ -1131,9 +1163,10 @@ class AudioStitcherVideoMerger:
1131
1163
  logger.debug('track_name %s'%track_name)
1132
1164
  if track_name[0] == '0': # muted, skip
1133
1165
  continue
1134
- names_audio_tempfiles.append((track_name, monotf))
1166
+ names_audio_tempfiles.append((track_name, monotf, dev))
1135
1167
  logger.debug('names_audio_tempfiles %s'%pformat(names_audio_tempfiles))
1136
- self._write_ISOs(names_audio_tempfiles)
1168
+ self._write_ISOs(names_audio_tempfiles,
1169
+ snd_root=snd_root, synced_root=synced_root, raw_root=raw_root)
1137
1170
  logger.debug('merged_audio_files_by_device %s'%
1138
1171
  merged_audio_files_by_device)
1139
1172
  # This loop below for logging purpose only:
@@ -1177,9 +1210,12 @@ class AudioStitcherVideoMerger:
1177
1210
  """
1178
1211
  synced_clip_file = self.videoclip.final_synced_file
1179
1212
  video_path = self.videoclip.AVpath
1213
+ logger.debug(f'original clip {video_path}')
1214
+ logger.debug(f'clip duration {ffprobe_duration(video_path)} s')
1180
1215
  timecode = self.videoclip.get_start_timecode_string()
1181
1216
  # self.videoclip.synced_audio = audio_path
1182
1217
  audio_path = self.videoclip.synced_audio
1218
+ logger.debug(f'audio duration {sox.file_info.duration(_pathname(audio_path))}')
1183
1219
  vid_only_handle = self._keep_VIDEO_only(video_path)
1184
1220
  a_n = _pathname(audio_path)
1185
1221
  v_n = str(vid_only_handle.name)
@@ -1193,8 +1229,8 @@ class AudioStitcherVideoMerger:
1193
1229
  ffmpeg_args = (
1194
1230
  ffmpeg
1195
1231
  .input(v_n)
1196
- # .output(out_n, shortest=None, vcodec='copy',
1197
- .output(out_n, vcodec='copy',
1232
+ .output(out_n, shortest=None, vcodec='copy',
1233
+ # .output(out_n, vcodec='copy',
1198
1234
  timecode=timecode)
1199
1235
  .global_args('-i', a_n, "-hide_banner")
1200
1236
  .overwrite_output()
@@ -1205,9 +1241,8 @@ class AudioStitcherVideoMerger:
1205
1241
  _, out = (
1206
1242
  ffmpeg
1207
1243
  .input(v_n)
1208
- .output(out_n, vcodec='copy',
1209
- # .output(out_n, shortest=None, vcodec='copy',
1210
- # metadata='reel_name=foo', not all container support gen MD
1244
+ # .output(out_n, vcodec='copy',
1245
+ .output(out_n, shortest=None, vcodec='copy',
1211
1246
  timecode=timecode,
1212
1247
  )
1213
1248
  .global_args('-i', a_n, "-hide_banner")
@@ -1215,8 +1250,8 @@ class AudioStitcherVideoMerger:
1215
1250
  .run(capture_stderr=True)
1216
1251
  )
1217
1252
  logger.debug('ffmpeg output')
1218
- for l in out.decode("utf-8").split('\n'):
1219
- logger.debug(l)
1253
+ # for l in out.decode("utf-8").split('\n'):
1254
+ # logger.debug(l)
1220
1255
  except ffmpeg.Error as e:
1221
1256
  print('ffmpeg.run error merging: \n\t %s + %s = %s\n'%(
1222
1257
  audio_path,
@@ -1226,6 +1261,9 @@ class AudioStitcherVideoMerger:
1226
1261
  print(e)
1227
1262
  print(e.stderr.decode('UTF-8'))
1228
1263
  sys.exit(1)
1264
+ logger.debug(f'merged clip {out_n}')
1265
+ logger.debug(f'clip duration {ffprobe_duration(out_n)} s')
1266
+
1229
1267
 
1230
1268
  class Matcher:
1231
1269
  """
@@ -1267,21 +1305,6 @@ class Matcher:
1267
1305
  self.recordings = recordings_list
1268
1306
  self.mergers = []
1269
1307
 
1270
- # def _rename_all_recs(self):
1271
- # """
1272
- # Add _synced to filenames of synced video files. Change stored name only:
1273
- # files have yet to be written to.
1274
- # """
1275
- # # match IO_structure:
1276
- # # case 'foldercam':
1277
- # for rec in self.recordings:
1278
- # rec_extension = rec.AVpath.suffix
1279
- # rel_path_new_name = '%s%s'%(rec.AVpath.stem, rec_extension)
1280
- # rec.new_rec_name = Path(rel_path_new_name)
1281
- # logger.debug('for %s new name: %s'%(
1282
- # _pathname(rec.AVpath),
1283
- # _pathname(rec.new_rec_name)))
1284
-
1285
1308
  def scan_audio_for_each_videoclip(self):
1286
1309
  """
1287
1310
  For each video (and for the Main Sound) in self.recordings, this finds
@@ -1293,10 +1316,11 @@ class Matcher:
1293
1316
  V3 checked against ...
1294
1317
  Main Sound checked against A1, A2, A3, A4
1295
1318
  """
1296
- video_recordings = [r for r in self.recordings if r.is_video()
1297
- or r.is_reference]
1319
+ video_recordings = [r for r in self.recordings
1320
+ if r.is_video() or r.is_audio_reference]
1321
+ # if r.is_audio_reference then audio, and will pass as video
1298
1322
  audio_recs = [r for r in self.recordings if r.is_audio()
1299
- and not r.is_reference]
1323
+ and not r.is_audio_reference]
1300
1324
  if not audio_recs:
1301
1325
  print('\nNo audio recording found, syncing of videos only not implemented yet, exiting...\n')
1302
1326
  sys.exit(1)
@@ -1452,7 +1476,7 @@ class Matcher:
1452
1476
  vid.write_file_timecode(tc)
1453
1477
  return
1454
1478
 
1455
- def move_multicam_to_dir(self):
1479
+ def move_multicam_to_dir(self, raw_root=None, synced_root=None):
1456
1480
  # creates a dedicated multicam directory and move clusters there
1457
1481
  # e.g., for "top/day01/camA/roll02"
1458
1482
  # ^ at that level
@@ -1461,6 +1485,7 @@ class Matcher:
1461
1485
  #
1462
1486
  # check for consistency: are all clips at the same level from topdir?
1463
1487
  # Only for video, not audio (which doesnt fill up cards)
1488
+ logger.debug(f'synced_root: {synced_root}')
1464
1489
  video_medias = [m for m in self.recordings if m.device.dev_type == 'CAM']
1465
1490
  video_paths = [m.AVpath.parts for m in video_medias]
1466
1491
  AV_path_lengths = [len(p) for p in video_paths]
@@ -1479,32 +1504,63 @@ class Matcher:
1479
1504
  sys.exit(0)
1480
1505
  # pick first
1481
1506
  CAM_level, avp = CAM_levels[0], video_medias[0].AVpath
1482
- logger.debug('CAM_levels: %s for ex \n%s'%(CAM_level, avp))
1507
+ logger.debug('CAM_levels: %s for %s\n'%(CAM_level, avp))
1483
1508
  # MCCDIR = 'SyncedMulticamClips'
1484
- parts_up_a_level = avp.parts[:CAM_level]
1485
- multicam_dir = Path('').joinpath(*parts_up_a_level)/MCCDIR
1509
+ parts_up_a_level = [prt for prt in avp.parts[:CAM_level] if prt != '/']
1510
+ logger.debug(f'parts_up_a_level: {parts_up_a_level}')
1511
+ if synced_root == None:
1512
+ # alongside mode
1513
+ logger.debug('alongside mode')
1514
+ multicam_dir = Path('/').joinpath(*parts_up_a_level)/MCCDIR
1515
+ else:
1516
+ # MAM mode
1517
+ logger.debug('MAM mode')
1518
+ abs_path_up = Path('/').joinpath(*parts_up_a_level)/MCCDIR
1519
+ logger.debug(f'abs_path_up: {abs_path_up}')
1520
+ rel_up = abs_path_up.relative_to(raw_root)
1521
+ logger.debug(f'rel_up: {rel_up}')
1522
+ multicam_dir = Path(synced_root)/Path(raw_root).name/rel_up
1523
+ # multicam_dir = Path(synced_root).joinpath(*parts_up_a_level)/MCCDIR
1486
1524
  logger.debug('multicam_dir: %s'%multicam_dir)
1487
1525
  Path.mkdir(multicam_dir, exist_ok=True)
1488
1526
  cam_clips = []
1489
1527
  [cam_clips.append(cl['vids']) for cl in self.multicam_clips_clusters]
1490
1528
  cam_clips = _flatten(cam_clips)
1491
- logger.debug('cam_clips: %s'%cam_clips)
1529
+ logger.debug('cam_clips: %s'%pformat(cam_clips))
1492
1530
  cam_names = set([r.device.name for r in cam_clips])
1493
1531
  # create new dirs for each CAM
1494
1532
  [Path.mkdir(multicam_dir/cam_name, exist_ok=True)
1495
1533
  for cam_name in cam_names]
1496
1534
  # move clips there
1497
- for r in cam_clips:
1498
- cam = r.device.name
1499
- clip_name = r.AVpath.name
1500
- dest = r.final_synced_file.replace(multicam_dir/cam/clip_name)
1501
- logger.debug('dest: %s'%dest)
1502
- origin_folder = r.final_synced_file.parent
1503
- folder_now_empty = len(list(origin_folder.glob('*.*'))) == 0
1504
- if folder_now_empty:
1505
- logger.debug('after moving %s, folder is now empty, removing it'%dest)
1506
- origin_folder.rmdir()
1507
- print('\nMoved %i multicam clips in %s'%(len(cam_clips), multicam_dir))
1535
+ if synced_root == None:
1536
+ # alongside mode
1537
+ for r in cam_clips:
1538
+ cam = r.device.name
1539
+ clip_name = r.AVpath.name
1540
+ dest = r.final_synced_file.replace(multicam_dir/cam/clip_name)
1541
+ logger.debug('dest: %s'%dest)
1542
+ origin_folder = r.final_synced_file.parent
1543
+ folder_now_empty = len(list(origin_folder.glob('*'))) == 0
1544
+ if folder_now_empty:
1545
+ logger.debug('after moving %s, folder is now empty, removing it'%dest)
1546
+ origin_folder.rmdir()
1547
+ print('\nMoved %i multicam clips in %s'%(len(cam_clips), multicam_dir))
1548
+ else:
1549
+ # MAM mode
1550
+ for r in cam_clips:
1551
+ cam = r.device.name
1552
+ clip_name = r.AVpath.name
1553
+ logger.debug(f'r.final_synced_file: {r.final_synced_file}')
1554
+ dest = r.final_synced_file.replace(multicam_dir/cam/clip_name)
1555
+ # leave a symlink behind
1556
+ os.symlink(multicam_dir/cam/clip_name, r.final_synced_file)
1557
+ logger.debug('dest: %s'%dest)
1558
+ origin_folder = r.final_synced_file.parent
1559
+ # folder_now_empty = len(list(origin_folder.glob('*'))) == 0
1560
+ # if folder_now_empty:
1561
+ # logger.debug('after moving %s, folder is now empty, removing it'%dest)
1562
+ # origin_folder.rmdir()
1563
+ print('\nMoved %i multicam clips in %s'%(len(cam_clips), multicam_dir))
1508
1564
 
1509
1565
 
1510
1566