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.

@@ -3,8 +3,6 @@
3
3
  # while inotifywait --recursive -e close_write . ; do python entry.py tests/multi2/; done
4
4
  # above for linux
5
5
 
6
- TRACKSFN = 'tracks.txt'
7
- SILENT_TRACK_TOKENS = '-0n'
8
6
 
9
7
  av_file_extensions = \
10
8
  """webm mkv flv flv vob ogv ogg drc gif gifv mng avi MTS M2TS TS mov qt
@@ -18,8 +16,10 @@ M4V SVI 3GP 3G2 MXF ROQ NSV FLV F4V F4P F4A F4B 3GP AA AAC AAX ACT AIFF ALAC
18
16
  AMR APE AU AWB DSS DVF FLAC GSM IKLAX IVS M4A M4B M4P MMF MP3 MPC MSV NMF
19
17
  OGG OGA MOGG OPUS RA RM RAW RF64 SLN TTA VOC VOX WAV WMA WV WEBM 8SVX CDA MOV AVI BWF""".split()
20
18
 
19
+ audio_ext = 'aiff wav mp3'.split()
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,14 @@ from itertools import groupby
36
36
  # import distance
37
37
  try:
38
38
  from . import multi2polywav
39
+ from . import mamsync
40
+ from . import mamconf
41
+ from . import yaltc
39
42
  except:
40
43
  import multi2polywav
44
+ import mamsync
45
+ import mamconf
46
+ import yaltc
41
47
 
42
48
  MCCDIR = 'SyncedMulticamClips'
43
49
  SYNCEDFOLDER = 'SyncedMedia'
@@ -58,10 +64,9 @@ def print_grby(grby):
58
64
  print('\ngrouped by %s:'%key)
59
65
  for e in keylist:
60
66
  print(' ', e)
61
-
62
67
  @dataclass
63
68
  class Tracks:
64
- # track numbers start at 1 for first track (as needed by sox)
69
+ # track numbers start at 1 for first track (as needed by sox,1 Based Index)
65
70
  ttc: int # track number of TicTacCode signal
66
71
  unused: list # of unused tracks
67
72
  stereomics: list # of stereo mics track tuples (Lchan#, Rchan#)
@@ -69,7 +74,9 @@ class Tracks:
69
74
  others: list #of all other tags: (tag, track#) tuples
70
75
  rawtrx: list # list of strings read from file
71
76
  error_msg: str # 'None' if none
72
- lag_values: list # list of lag in ms, entry is None if not specified.
77
+ lag_values: list # list of lags in ms, entry is None if not specified.
78
+ # UTC_timestamp: str # to the nearest minute ISO 8601 date and time e.g.: "2007-04-05T14:30Z"
79
+
73
80
 
74
81
  @dataclass
75
82
  class Device:
@@ -210,10 +217,8 @@ class Scanner:
210
217
  top_directory : string
211
218
  String of path where to start searching for media files.
212
219
 
213
- # top_dir_has_multicam : bool
214
- # If top dir is folder structures AND more than on cam
215
-
216
- found_media_files: list of Media objects
220
+ found_media_files: list of dataclass Media instances encapsulating
221
+ the pathlibPath and the device (of Device dataclass).
217
222
  """
218
223
 
219
224
  def __init__(
@@ -244,16 +249,7 @@ class Scanner:
244
249
  CAMs = [d for d in devices if d.dev_type == 'CAM']
245
250
  return len(set(CAMs))
246
251
 
247
- def get_device_in_path(self, a_media):
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):
252
+ def scan_media_and_build_devices_UID(self, synced_root = None):
257
253
  """
258
254
  Scans Scanner.top_directory recursively for files with known audio-video
259
255
  extensions. For each file found, a device fingerprint is obtained from
@@ -269,16 +265,36 @@ class Scanner:
269
265
  Sets Scanner.input_structure = 'loose'|'ordered'
270
266
 
271
267
  """
272
- files = Path(self.top_directory).rglob('*.*')
273
- paths = [
274
- p
275
- for p in files
276
- if p.suffix[1:] in av_file_extensions
277
- and SYNCEDFOLDER not in p.parts # SyncedMedia
278
- and MCCDIR not in p.parts # SyncedMulticamClips
279
- ]
280
- logger.debug('found media files %s'%paths)
281
- parents = [p.parent for p in paths]
268
+ logger.debug(f'on entry synced_root: {synced_root}')
269
+ if synced_root != None: # mam mode
270
+ p = Path(platformdirs.user_data_dir('mamsync', 'plutz'))/mamconf.LOG_FILE
271
+ with open(p, 'r') as fh:
272
+ done = set(fh.read().split()) # sets of strings of abs path
273
+ logger.debug(f'done clips: {pformat(done)}')
274
+ files = Path(self.top_directory).rglob('*')
275
+ clip_paths = []
276
+ some_done = False
277
+ for raw_path in files:
278
+ if raw_path.suffix[1:] in av_file_extensions:
279
+ if SYNCEDFOLDER not in raw_path.parts: # SyncedMedia
280
+ if MCCDIR not in raw_path.parts: # SyncedMulticamClips
281
+ if '_ISO' not in [part[-4:] for part in raw_path.parts]: # exclude ISO wav files
282
+ if synced_root != None and str(raw_path) in done:
283
+ logger.debug(f'{raw_path} done')
284
+ some_done = True
285
+ continue
286
+ else:
287
+ clip_paths.append(raw_path)
288
+ if some_done:
289
+ print('Somme media files were already synced...')
290
+ logger.debug('found media files %s'%clip_paths)
291
+ if len(clip_paths) == 0:
292
+ print('No media found, bye.')
293
+ sys.exit(0)
294
+ # self.found_media_files = []
295
+ # self.input_structure = 'loose'
296
+ # return
297
+ parents = [p.parent for p in clip_paths]
282
298
  logger.debug('found parents %s'%pformat(parents))
283
299
  # True if all elements are identical
284
300
  AV_files_have_same_parent = parents.count(parents[0]) == len(parents)
@@ -292,7 +308,7 @@ class Scanner:
292
308
  # check later if inside each folder, media have same device
293
309
  # for now, we'll guess structure is 'ordered'
294
310
  self.input_structure = 'ordered'
295
- for p in paths:
311
+ for p in clip_paths:
296
312
  new_media = media_at_path(self.input_structure, p) # dev UID set here
297
313
  self.found_media_files.append(new_media)
298
314
  # for non UIDed try building UID from filenam
@@ -334,17 +350,19 @@ class Scanner:
334
350
  logger.debug('Scanner.found_media_files = %s'%pformat(self.found_media_files))
335
351
  if self.input_structure == 'ordered':
336
352
  self._confirm_folders_have_same_device()
337
- # self._use_folder_as_device_name()
338
- devices = set([m.device for m in self.found_media_files])
339
- audio_devices = [d for d in devices if d.dev_type == 'REC']
340
- for recorder in audio_devices:
341
- # process tracks.txt for audio recorders
342
- recorder.tracks = self._get_tracks_from_file(recorder)
343
- # logging only:
344
- if recorder.tracks:
345
- if not all([lv == None for lv in recorder.tracks.lag_values]):
346
- logger.debug('%s has lag_values %s'%(
347
- recorder.name, recorder.tracks.lag_values))
353
+ # [TODO] move this where Recordings have been TCed for any tracks timestamps
354
+ # <begin>
355
+ # devices = set([m.device for m in self.found_media_files])
356
+ # audio_devices = [d for d in devices if d.dev_type == 'REC']
357
+ # for recorder in audio_devices:
358
+ # # process tracks.txt for audio recorders
359
+ # recorder.tracks = self._get_tracks_from_file(recorder)
360
+ # # logging only:
361
+ # if recorder.tracks:
362
+ # if not all([lv == None for lv in recorder.tracks.lag_values]):
363
+ # logger.debug('%s has lag_values %s'%(
364
+ # recorder.name, recorder.tracks.lag_values))
365
+ # </end>
348
366
  # check if device is in fact two parents up (and parent = ROLLxx):
349
367
  # Group media by folder 2up and verify all media for each
350
368
  # group have same device.
@@ -371,11 +389,10 @@ class Scanner:
371
389
  for m in self.found_media_files:
372
390
  if m.device.UID == UID:
373
391
  m.device.name = name
374
- # logger.debug('renamed device media: %s'%pformat(self.found_media_files))
375
392
  no_name_devices = [m.device for m in self.found_media_files
376
393
  if not m.device.name]
377
394
  # possible if self.input_structure == 'loose'
378
- def _try_name_from_metadata(media):
395
+ def _try_name_from_metadata(media): # unused for now
379
396
  # search model and make from fprobe
380
397
  file = Path(media.path)
381
398
  logger.debug('trying to find maker model for %s'%file)
@@ -391,7 +408,6 @@ class Scanner:
391
408
  # could reside in ['format','tags','com.apple.quicktime.model'],
392
409
  # or ['format','tags','model'],
393
410
  # or ['streams'][0]['tags']['vendor_id']) :-(
394
-
395
411
  for anon_dev in no_name_devices:
396
412
  medias = self.get_media_for_device(anon_dev)
397
413
  guess_name = _try_name_from_files(medias)
@@ -405,50 +421,6 @@ class Scanner:
405
421
  if not any(dev_is_REC): # no audio recordings!
406
422
  print('\rNo audio recording found, nothing to sync, bye.')
407
423
  sys.exit(0)
408
- # print('devices 312 %s'%set([m.device for m in self.found_media_files]))
409
-
410
- def _get_tracks_from_file(self, device) -> Tracks:
411
- """
412
- Look for eventual track names in TRACKSFN file, stored inside the
413
- recorder folder alongside the audio files. If there, returns a Tracks
414
- object, if not returns None.
415
- """
416
- source_audio_folder = device.folder
417
- tracks_file = source_audio_folder/TRACKSFN
418
- track_names = False
419
- a_recording = [m for m in self.found_media_files
420
- if m.device == device][0]
421
- logger.debug('a_recording for device %s : %s'%(device, a_recording))
422
- nchan = sox.file_info.channels(str(a_recording.path))
423
- if os.path.isfile(tracks_file):
424
- logger.debug('found file: %s'%(TRACKSFN))
425
- tracks = self._parse_track_values(tracks_file)
426
- if tracks.error_msg:
427
- print('\nError parsing [gold1]%s[/gold1] file: %s, quitting.\n'%
428
- (tracks_file, tracks.error_msg))
429
- sys.exit(1)
430
- logger.debug('parsed tracks %s'%tracks)
431
- ntracks = 2*len(tracks.stereomics)
432
- ntracks += len(tracks.mix)
433
- ntracks += len(tracks.unused)
434
- ntracks += len(tracks.others)
435
- ntracks += 1 # for ttc track
436
- logger.debug(' n chan: %i n tracks file: %i'%(nchan, ntracks))
437
- if ntracks != nchan:
438
- print('\nError parsing %s content'%tracks_file)
439
- print('incoherent number of tracks, %i vs %i quitting\n'%
440
- (nchan, ntracks))
441
- sys.exit(1)
442
- err_msg = tracks.error_msg
443
- if err_msg != None:
444
- print('\nError, quitting: in file %s, %s'%(tracks_file, err_msg))
445
- raise Exception
446
- else:
447
- logger.debug('tracks object%s'%tracks)
448
- return tracks
449
- else:
450
- logger.debug('no tracks.txt file found')
451
- return None
452
424
 
453
425
  def _confirm_folders_have_same_device(self):
454
426
  """
@@ -586,184 +558,6 @@ class Scanner:
586
558
  logger.debug('n_CAM_folder %i'%n_CAM_folder)
587
559
  return
588
560
 
589
- def _parse_track_values(self, tracks_file) -> Tracks:
590
- """
591
- read track names for naming separated ISOs
592
- from tracks_file.
593
-
594
- tokens looked for: mix; mix L; mix R; 0 and TC
595
-
596
- repeating "mic*" pattern signals a stereo track
597
- and entries will correspondingly panned into
598
- a stero mix named mixL.wav and mixL.wav
599
-
600
- mic L # spaces are ignored |
601
- mic R | stereo pair
602
- micB L
603
- micB R
604
-
605
- Returns: a Tracks instance:
606
- # track numbers start at 1 for first track (as needed by sox)
607
- ttc: int # track number of TicTacCode signal
608
- unused: list # of unused tracks
609
- stereomics: list # of stereo mics track tuples (Lchan#, Rchan#)
610
- mix: list # of mixed tracks, if a pair, order is L than R
611
- others: list #of all other tags: (tag, track#) tuples
612
- rawtrx: list # list of strings read from file
613
- error_msg: str # 'None' if none
614
- e.g.: Tracks( ttc=2,
615
- unused=[],
616
- stereomics=[('mic', (4, 3)), ('mic2', (6, 5))],
617
- mix=[], others=[('clics', 1)],
618
- rawtrx=['clics', 'TC', 'micL', 'micR', 'mic2L;1000', 'mic2R;1000', 'mixL', 'mixR'],
619
- error_msg=None, lag_values=[None, None, None, None, '1000', '1000', None, None])
620
- """
621
- def _WOspace(chaine):
622
- ch = [c for c in chaine if c != ' ']
623
- return ''.join(ch)
624
- # def _WO_LR(chaine):
625
- # ch = [c for c in chaine if c not in 'LR']
626
- # return ''.join(ch)
627
- # def _seemsStereoMic(tag):
628
- # # is tag likely a stereo pair tag?
629
- # # should starts with 'mic' and ends with 'l' or 'r'
630
- # return tag[1:4]=='mic' and tag[0] in 'lr'
631
- file=open(tracks_file,"r")
632
- whole_txt = file.read()
633
- logger.debug('file %s all_lines:\n%s'%(tracks_file, whole_txt))
634
- tracks_lines_wspaces = [l.split('#')[0] for l in whole_txt.splitlines()
635
- if len(l) > 0 ]
636
- tracks_lines = [_WOspace(l) for l in tracks_lines_wspaces if len(l) > 0 ]
637
- rawtrx = [l for l in tracks_lines_wspaces if len(l) > 0 ]
638
- # add index with tuples, starting at 1
639
- logger.debug('tracks_lines whole: %s'%tracks_lines)
640
- def _detach_lag_value(line):
641
- # look for ";number" ending any line, returns a two-list
642
- splt = line.split(';')
643
- if len(splt) == 1:
644
- splt += [None]
645
- if len(splt) != 2:
646
- # error
647
- print('\nText error in %s, line %s has too many ";"'%(
648
- tracks_file, line))
649
- return splt
650
- tracks_lines, lag_values = zip(*[_detach_lag_value(l) for l
651
- in tracks_lines])
652
- lag_values = [e for e in lag_values] # from tuple to list
653
- # logger.debug('tracks_lines WO lag: %s'%tracks_lines)
654
- tracks_lines = [l.lower() for l in tracks_lines]
655
- logger.debug('tracks_lines lower case: %s'%tracks_lines)
656
- # print(lag_values)
657
- logger.debug('lag_values: %s'%lag_values)
658
- tagsWOl_r = [e[:-1] for e in tracks_lines] # skip last letter
659
- logger.debug('tags WO LR letter %s'%tagsWOl_r)
660
- # find idx of start of pairs
661
- # ['clics', 'TC', 'micL', 'micR', 'mic2L', 'mic2R', 'mixL', 'mixR']
662
- def _micOrmix(a,b):
663
- # test if same and mic mic or mix mix
664
- if len(a) == 0:
665
- return False
666
- return (a == b) and (a in 'micmix')
667
- pair_idx_start =[i for i, same in enumerate([_micOrmix(a,b) for a,b
668
- in zip(tagsWOl_r,tagsWOl_r[1:])]) if same]
669
- logger.debug('pair_idx_start %s'%pair_idx_start)
670
- def LR_OK(idx):
671
- # in tracks_lines, check if idx ends a LR pair
672
- # delays, if any, have been removed
673
- a = tracks_lines[idx][-1]
674
- b = tracks_lines[idx+1][-1]
675
- return a+b in ['lr', 'rl']
676
- LR_OKs = [LR_OK(p) for p in pair_idx_start]
677
- logger.debug('LR_OKs %s'%LR_OKs)
678
- if not all(LR_OKs):
679
- print('\nError in %s'%tracks_file)
680
- print('Some tracks are paired but not L and R: %s'%rawtrx)
681
- print('quitting...')
682
- quit()
683
- complete_pairs_idx = pair_idx_start + [i + 1 for i in pair_idx_start]
684
- singles = set(range(len(tracks_lines))).difference(complete_pairs_idx)
685
- logger.debug('complete_pairs_idx %s'%complete_pairs_idx)
686
- logger.debug('singles %s'%singles)
687
- singles_tag = [tracks_lines[i] for i in singles]
688
- logger.debug('singles_tag %s'%singles_tag)
689
- n_tc_token = sum([t == 'tc' for t in singles_tag])
690
- logger.debug('n tc tags %s'%n_tc_token)
691
- if n_tc_token == 0:
692
- print('\nError in %s'%tracks_file)
693
- print('with %s'%rawtrx)
694
- print('no TC track found, quitting...')
695
- quit()
696
- if n_tc_token > 1:
697
- print('\nError in %s'%tracks_file)
698
- print('with %s'%rawtrx)
699
- print('more than one TC track, quitting...')
700
- quit()
701
- output_tracks = Tracks(None,[],[],[],[],rawtrx,None,[])
702
- output_tracks.ttc = tracks_lines.index('tc') + 1 # 1st = 1
703
- logger.debug('ttc_chan %s'%output_tracks.ttc)
704
- zeroed = [i+1 for i, t in enumerate(tracks_lines) if t == '0']
705
- logger.debug('zeroed %s'%zeroed)
706
- output_tracks.unused = zeroed
707
- output_tracks.others = [(st, tracks_lines.index(st)+1) for st
708
- in singles_tag if st not
709
- in ['tc', 'monomix', '0']]
710
- logger.debug('output_tracks.others %s'%output_tracks.others)
711
- # check for monomix
712
- if 'monomix' in tracks_lines:
713
- output_tracks.mix = [tracks_lines.index('monomix')+1]
714
- else:
715
- output_tracks.mix = []
716
- # check for stereo mix
717
- def _findLR(i_first):
718
- # returns L R indexes (+1 for sox non zero based indexing)
719
- i_2nd = i_first + 1
720
- a = tracks_lines[i_first][-1] # l|r at end
721
- b = tracks_lines[i_2nd][-1] # l|r at end
722
- if a == 'l':
723
- if b == 'r':
724
- # sequence is mixL mixR
725
- return i_first+1, i_2nd+1
726
- else:
727
- print('\nError in %s'%tracks_file)
728
- print('with %s'%rawtrx)
729
- print('can not find stereo mix')
730
- quit()
731
- elif a == 'r':
732
- if b == 'l':
733
- # sequence is mixR mixL
734
- return i_2nd+1, i_first+1
735
- else:
736
- print('\nError in %s'%tracks_file)
737
- print('with %s'%rawtrx)
738
- print('can not find stereo mix')
739
- quit()
740
- logger.debug('for now, output_tracks.mix %s'%output_tracks.mix)
741
- mix_pair = [p for p in pair_idx_start if tracks_lines[p][1:] == 'mix']
742
- if len(mix_pair) == 1:
743
- # one stereo mix, remove it from other pairs
744
- i = mix_pair[0]
745
- LR_pair = _findLR(i)
746
- logger.debug('LR_pair %s'%str(LR_pair))
747
- pair_idx_start.remove(i)
748
- # consistency check
749
- if output_tracks.mix != []:
750
- # already found a mono mix above!
751
- print('\nError in %s'%tracks_file)
752
- print('with %s'%rawtrx)
753
- print('found a mono mix AND a stereo mix')
754
- quit()
755
- output_tracks.mix = LR_pair
756
- logger.debug('finally, output_tracks.mix %s'%str(output_tracks.mix))
757
- logger.debug('remaining pairs %s'%pair_idx_start)
758
- # those are stereo pairs
759
- stereo_pairs = []
760
- for first_in_pair in pair_idx_start:
761
- suffix = tracks_lines[first_in_pair][:-1]
762
- stereo_pairs.append((suffix, _findLR(first_in_pair)))
763
- logger.debug('stereo_pairs %s'%stereo_pairs)
764
- output_tracks.stereomics = stereo_pairs
765
- logger.debug('finished: %s'%output_tracks)
766
- return output_tracks
767
561
 
768
562
 
769
563