tictacsync 1.0.2a0__py3-none-any.whl → 1.2.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/newmix.py CHANGED
@@ -1,19 +1,99 @@
1
- import os, itertools, argparse, ffmpeg, tempfile
1
+ import os, itertools, argparse, ffmpeg, tempfile, platformdirs
2
2
  from pathlib import Path
3
3
  from loguru import logger
4
- import shutil, sys, re, sox
4
+ import shutil, sys, re, sox, configparser
5
5
  from pprint import pformat
6
6
  from rich import print
7
7
 
8
+ try:
9
+ from . import mamconf
10
+ except:
11
+ import mamconf
12
+
13
+
14
+ LUA = True
8
15
  OUT_DIR_DEFAULT = 'SyncedMedia'
9
16
  MCCDIR = 'SyncedMulticamClips'
10
17
  SEC_DELAY_CHANGED_SND = 10 #sec, SND_DIR changed if diff time is bigger
11
18
  DEL_TEMP = True
19
+ CONF_FILE = 'mamsync.cfg'
12
20
 
13
21
  logger.level("DEBUG", color="<yellow>")
14
22
  logger.remove()
15
- # logger.add(sys.stdout, filter=lambda r: r["function"] == "_change_audio4video")
16
- # logger.add(sys.stdout, filter=lambda r: r["function"] == "find_SND_vids_pairs_in_dir")
23
+ # logger.add(sys.stdout, filter=lambda r: r["function"] == "main")
24
+ # logger.add(sys.stdout, filter=lambda r: r["function"] == "find_SND_vids_pairs_in_udir")
25
+
26
+ DAVINCI_RESOLVE_SCRIPT_LOCATION = '/Library/Application Support/Blackmagic Design/DaVinci Resolve/Fusion/Scripts/Utility/'
27
+ DAVINCI_RESOLVE_SCRIPT_TEMPLATE = \
28
+ """def ClipWithFilePath(file_path):
29
+ media_pool = app.GetResolve().GetProjectManager().GetCurrentProject().GetMediaPool()
30
+ queue = [media_pool.GetRootFolder()]
31
+ while len(queue) > 0:
32
+ current = queue.pop(0)
33
+ queue += current.GetSubFolderList()
34
+ got_it = [cl for cl in current.GetClipList()
35
+ if cl.GetClipProperty('File Path') == file_path]
36
+ # print(got_it)
37
+ if got_it != []:
38
+ # print('got it')
39
+ return got_it[0]
40
+
41
+ def findAndReplace(old_file, new_file):
42
+ clip_with_old_file = ClipWithFilePath(old_file)
43
+ if clip_with_old_file == None:
44
+ print(f'did not find clip with path {old_file}')
45
+ sys.exit(0)
46
+ else:
47
+ clip_with_old_file.ReplaceClip(new_file)
48
+ cfp, cn = [clip_with_old_file.GetClipProperty(p) for p in ['File Path', 'Clip Name']]
49
+ if cfp == new_file:
50
+ print(f'Loaded {cn} with a new sound track')
51
+ else:
52
+ print(f'findAndReplace {old_file} -> {new_file} failed')
53
+
54
+ """
55
+ # python is deprecated... Lua is the way to go
56
+
57
+ DAVINCI_RESOLVE_SCRIPT_TEMPLATE_LUA = """local function ClipWithPartialPath(partial_path)
58
+ local media_pool = app:GetResolve():GetProjectManager():GetCurrentProject():GetMediaPool()
59
+ local queue = {media_pool:GetRootFolder()}
60
+ while #queue > 0 do
61
+ local current = table.remove(queue, 1)
62
+ local subfolders = current:GetSubFolderList()
63
+ for _, folder in ipairs(subfolders) do
64
+ table.insert(queue, folder)
65
+ end
66
+ local got_it = {}
67
+ local clips = current:GetClipList()
68
+ for _, cl in ipairs(clips) do
69
+ if string.find(cl:GetClipProperty('File Path'), partial_path) then
70
+ table.insert(got_it, cl)
71
+ end
72
+ end
73
+ if #got_it > 0 then
74
+ return got_it[1]
75
+ end
76
+ end
77
+ end
78
+
79
+ local function findAndReplace(old_file, new_file)
80
+ local clip_with_old_file = ClipWithPartialPath(old_file)
81
+ if clip_with_old_file == nil then
82
+ print('did not find clip with path ' .. old_file)
83
+ else
84
+ clip_with_old_file:ReplaceClip(new_file)
85
+ local cfp = clip_with_old_file:GetClipProperty('File Path')
86
+ local cn = clip_with_old_file:GetClipProperty('Clip Name')
87
+ if cfp == new_file then
88
+ print('Loaded ' .. cn .. ' with a new sound track')
89
+ else
90
+ print('findAndReplace ' .. old_file .. ' -> ' .. new_file .. ' failed')
91
+ end
92
+ end
93
+ end
94
+
95
+ local changes = {
96
+ """
17
97
 
18
98
  video_extensions = \
19
99
  """webm mkv flv flv vob ogv ogg drc gif gifv mng avi mov
@@ -42,11 +122,12 @@ def is_synced_video(f):
42
122
  # logger.debug('ok_ext: %s ok_folders: %s'%(ok_ext, ok_folders))
43
123
  return ok_ext and ok_folders
44
124
 
45
- def find_SND_vids_pairs_in_dir(top):
46
- # look for matching video name and SND dir name
125
+ def find_SND_vids_pairs_in_udir(top):
126
+ # look for matching video name and SND in unique directory
127
+ # in alongside mode (vs MAM mode)
47
128
  # eg: IMG04.mp4 and IMG04_SND
48
129
  # maybe IMG04v2.mp4 if audio changed before (than it will be IMG04v3.mp4)
49
- # returns list of matches
130
+ # returns list of (SND, vid) matches
50
131
  # recursively search from 'top' argument
51
132
  vids = []
52
133
  SNDs = []
@@ -80,59 +161,64 @@ def find_SND_vids_pairs_in_dir(top):
80
161
  logger.debug('matches: %s'%pformat(matches))
81
162
  return matches
82
163
 
164
+ def find_SND_vids_pairs_in_dual_dir(synced_root, snd_root):
165
+ # look for matching video name and SND in unique directory
166
+ # in alongside mode (vs MAM mode)
167
+ # eg: IMG04.mp4 and directory IMG04_SND
168
+ # maybe IMG04v2.mp4 if audio changed before (than it will be IMG04v3.mp4)
169
+ # returns list of (SND, vid) matches
170
+ # recursively search from 'top' argument
171
+ vids = []
172
+ SNDs = []
173
+ print(f'Will look for new mix in {snd_root} compared to vids in {synced_root}')
174
+ for (root,dirs,files) in os.walk(synced_root):
175
+ for f in files:
176
+ pf = Path(root)/f
177
+ if pf.suffix[1:].lower() in video_extensions:
178
+ if not pf.is_symlink():
179
+ vids.append(pf)
180
+ for (root,dirs,files) in os.walk(snd_root):
181
+ for d in dirs:
182
+ if d[-4:] == '_SND':
183
+ SNDs.append(Path(root)/d)
184
+ logger.debug('vids %s SNDs %s'%(pformat(vids), pformat(SNDs)))
185
+ matches = []
186
+ def _names_match(vidname, SND_name):
187
+ # vidname is a str and has no extension
188
+ # vidname could have vNN suffix as in DSC_8064v31 so matches DSC_8064
189
+ if vidname == SND_name: # no suffix presents
190
+ return True
191
+ m = re.match(SND_name + r'v(\d+)', vidname)
192
+ if m != None:
193
+ logger.debug('its a natch and N= %s'%m.groups()[0])
194
+ return m != None
195
+ for pair in list(itertools.product(SNDs, vids)):
196
+ # print(pair)
197
+ SND, vid = pair # Paths
198
+ vidname, ext = vid.name.split('.') # string
199
+ if _names_match(vidname, SND.name[:-4]):
200
+ logger.debug('SND %s matches video %s'%(
201
+ Path('').joinpath(*SND.parts[-2:]),
202
+ Path('').joinpath(*vid.parts[-3:])))
203
+ matches.append(pair) # list of Paths
204
+ logger.debug('matches: %s'%pformat(matches))
205
+ return matches
206
+
83
207
  def parse_and_check_arguments():
84
208
  # parses directories from command arguments
85
209
  # check for consistencies and warn user and exits,
86
- # if returns, gives:
87
- # proxies_dir, originals_dir, audio_dir, both_audio_vid, scan_only
210
+ # returns parser.parse_args()
88
211
  parser = argparse.ArgumentParser()
89
- # parser.add_argument('-v',
90
- # nargs=1,
91
- # dest='video_dirs',
92
- # help='Where proxy clips and/or originals are stored')
93
- # parser.add_argument('-a',
94
- # nargs=1,
95
- # dest='audio_dir',
96
- # help='Contains newly changed mix files')
97
- parser.add_argument('-b',
212
+ parser.add_argument('-u',
98
213
  nargs=1,
99
- dest='both_audio_vid',
100
- help='Directory scanned for both audio and video, when tictacsync was used in "alongside mode"')
214
+ dest='unique_directory',
215
+ help='Directory scanned for both audio and video, when tictacsync is used rather than mamsync.')
101
216
  parser.add_argument('--dry',
102
217
  action='store_true',
103
218
  dest='scan_only',
104
219
  help="Just display changed audio, don't merge")
105
220
  args = parser.parse_args()
106
221
  logger.debug('args %s'%args)
107
- # ok cases:
108
- # -p -o -a + no -b
109
- # -o -a + no -b
110
- # args_set = [args.originals_dir != None,
111
- # args.audio_dir != None,
112
- # args.both_audio_vid != None,
113
- # ]
114
- # p, o, a, b = args_set
115
- # check that argument -b (both_audio_vid) is used alone
116
- # if b and any([o, a, p]):
117
- # print("\nDon't specify other argument than -b if both audio and video searched in the same directory.\n")
118
- # parser.print_help(sys.stderr)
119
- # sys.exit(0)
120
- # check that if proxies (-p) are specified, orginals too (-o)
121
- # if p and not o:
122
- # print("\nIf proxies directory is specified, so should originals directory.\n")
123
- # parser.print_help(sys.stderr)
124
- # sys.exit(0)
125
- # check that -o and -a are used together
126
- # if not b and not (o and a):
127
- # print("\nAt least originals and audio directories must be given (-o and -a) when audio and video are in different dir.\n")
128
- # parser.print_help(sys.stderr)
129
- # sys.exit(0)
130
- # # work in progress (aug 2025), so limit to -b:
131
- # if not b :
132
- # print("\nFor now, only -b argument is supported (a directory scanned for both audio and video) .\n")
133
- # parser.print_help(sys.stderr)
134
- # sys.exit(0)
135
- # list of singletons, so flatten. Keep None and False as is
136
222
  if args.scan_only:
137
223
  print('Sorry, --dry option not implemented yet, bye.')
138
224
  sys.exit(0)
@@ -147,7 +233,7 @@ def get_recent_mix(SND_dir, vid):
147
233
  logger.debug(f'wav_files {wav_files} in {SND_dir}')
148
234
  def is_mix(p):
149
235
  re_result = re.match(r'mix([lrLR])*', p.name)
150
- logger.debug(f'for {p.name} re_result {re_result}')
236
+ logger.debug(f'for {p.name} re_result looking for mix {re_result}')
151
237
  return re_result is not None
152
238
  mix_files = [p for p in wav_files if is_mix(p)]
153
239
  if len(mix_files) == 0:
@@ -211,6 +297,9 @@ def _change_audio4video(audio_path: Path, video: Path):
211
297
  Replace audio in video (argument) by the audio contained in
212
298
  audio_path (argument) returns nothing
213
299
  If name has version number, bump it (DSC_8064v13.MOV -> DSC_8064v14.MOV)
300
+ Returns string tuple of old and new video files
301
+ old file in incomplete e.g.: ../DSC_8064 without version and ext
302
+ new file is complete
214
303
 
215
304
  """
216
305
  vidname, video_ext = video.name.split('.')
@@ -223,14 +312,21 @@ def _change_audio4video(audio_path: Path, video: Path):
223
312
  # no suffix, add one
224
313
  out_path = video.parent / f'{vidname}v2.{video_ext}'
225
314
  out_n = _pathname(out_path)
315
+ # for Resolve search
316
+ partial_filename = str(video.parent / vidname)
226
317
  else:
227
318
  base, number_str = m.groups()
228
319
  logger.debug(f'base {base}, number_str {number_str}')
229
320
  up_tick = 1 + int(number_str)
230
321
  out_path = video.parent / f'{base}v{up_tick}.{video_ext}'
231
322
  out_n = _pathname(out_path)
323
+ # for Resolve search
324
+ partial_filename = str(video.parent / base)
232
325
  print(f'Video [bold]{video}[/bold] \nhas new sound and is now [bold]{out_path}[/bold]')
233
- video.unlink()
326
+ logger.debug(f'partial filname {partial_filename}')
327
+ old_file = str(video)
328
+ new_file = str(out_path)
329
+ # video.unlink() # remove old one
234
330
  # building args for debug purpose only:
235
331
  ffmpeg_args = (
236
332
  ffmpeg
@@ -264,6 +360,7 @@ def _change_audio4video(audio_path: Path, video: Path):
264
360
  print(e)
265
361
  print(e.stderr.decode('UTF-8'))
266
362
  sys.exit(1)
363
+ return partial_filename, new_file
267
364
 
268
365
  def _sox_combine(paths) -> Path:
269
366
  """
@@ -299,15 +396,49 @@ def _sox_combine(paths) -> Path:
299
396
  (merged_duration, nchan))
300
397
  return out_file_handle
301
398
 
399
+ def generate_script(new_mixes):
400
+ if LUA:
401
+ script = DAVINCI_RESOLVE_SCRIPT_TEMPLATE_LUA
402
+ postlude = ''
403
+ for a,b in new_mixes:
404
+ postlude += '{"%s","%s"},\n'%(a,b)
405
+ # postlude += f"('{a}','{b}'),\n"
406
+ postlude += '}\n\nfor _, pair in ipairs(changes) do\n'
407
+ postlude += ' findAndReplace(pair[1], pair[2])\n'
408
+ postlude += ' os.remove(pair[1])\n'
409
+ postlude += 'end\n'
410
+ # postlude += 'os.remove(pair[1])\n'
411
+ else: # python
412
+ script = DAVINCI_RESOLVE_SCRIPT_TEMPLATE
413
+ postlude = '\nchanges = [\n'
414
+ for a,b in new_mixes:
415
+ postlude += '("%s","%s"),\n'%(a,b)
416
+ postlude += ']\n'
417
+ postlude += '[findAndReplace(a,b) for a, b in changes]\n\n'
418
+ # foo = '[findAndReplace(a,b) for a, b in changes\n'
419
+ # print('foo',foo)
420
+ # print(postlude + foo)
421
+ return script + postlude
302
422
 
303
423
  def main():
304
- # proxies_dir, originals_dir, audio_dir, both_audio_vid, scan_only = \
424
+ # proxies_dir, originals_dir, audio_dir, unique_directory, scan_only = \
305
425
  # parse_and_check_arguments()
306
426
  args = parse_and_check_arguments()
307
- matching_pairs = find_SND_vids_pairs_in_dir(args.both_audio_vid[0])
427
+ if args.unique_directory != None:
428
+ # alongside
429
+ matching_pairs = find_SND_vids_pairs_in_udir(args.unique_directory[0])
430
+ else:
431
+ # MAM mode
432
+ raw_root, synced_root, snd_root, proxies = mamconf.get_proj(False)
433
+ if raw_root == None:
434
+ print('Error: without -u option, mamsync should be configured before using newmix')
435
+ sys.exit(0)
436
+ matching_pairs = find_SND_vids_pairs_in_dual_dir(synced_root, snd_root)
437
+ logger.debug(f'matching_pairs {pformat(matching_pairs)}')
438
+ changes = []
308
439
  for SND_dir, vid in matching_pairs:
309
440
  new_mix_files = get_recent_mix(SND_dir, vid)
310
- # logger.debug('new_mix_files: %s'%str(new_mix_files))
441
+ logger.debug('new_mix_files: %s'%str(new_mix_files))
311
442
  if new_mix_files != ():
312
443
  logger.debug(f'new mixes {new_mix_files} in {SND_dir} for {vid.name}')
313
444
  if len(new_mix_files) == 2:
@@ -315,9 +446,38 @@ def main():
315
446
  logger.debug('stereo_wav: %s'%new_audio_wav)
316
447
  else: # len == 1, mono wav file
317
448
  new_audio_wav = new_mix_files[0]
318
- _change_audio4video(new_audio_wav, vid)
319
- # print('\nVideo %s has new audio'%vid)
449
+ old_file, new_file = _change_audio4video(new_audio_wav, vid)
450
+ changes.append((old_file, new_file))
451
+ if len(changes) == 0:
452
+ print('No new mix.')
453
+ sys.exit(0)
454
+ logger.debug(f'changes {pformat(changes)}')
455
+ script = generate_script(changes)
456
+ fist_vid, _ = changes[0]
457
+ fist_name = Path(fist_vid).name
458
+ title = f'New sound for {fist_name}'
459
+ new_sound_clips = [p for p in Path(DAVINCI_RESOLVE_SCRIPT_LOCATION).iterdir() if 'New sound for' in str(p)]
460
+ if len(new_sound_clips) != 0:
461
+ print('There is already some "New sound for" Resolve scripts')
462
+ for p in new_sound_clips:
463
+ print(p)
464
+ while ( res:=input("Is it OK to delete them? (you could also do it manually). Enter y/n: ").lower() ) not in {"y", "n"}: pass
465
+ if res == 'y':
466
+ [p.unlink() for p in new_sound_clips]
467
+ # title = str(fist_vid)
468
+ if len(changes) > 1:
469
+ title += ' and others'
470
+ if LUA:
471
+ script_path = Path(DAVINCI_RESOLVE_SCRIPT_LOCATION)/f'{title}.lua'
472
+ script += f'os.remove("{script_path}")\n'
473
+ else:
474
+ # python version is deprecated...
475
+ script_path = Path(DAVINCI_RESOLVE_SCRIPT_LOCATION)/f'{title}.py'
476
+ with open(script_path, 'w') as fh:
477
+ fh.write(script)
478
+ print(f'Wrote script {script_path}')
479
+ # print(script)
320
480
 
321
481
 
322
482
  if __name__ == '__main__':
323
- main()
483
+ main()