vflow-cli 0.1.1__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.
@@ -0,0 +1,301 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ import subprocess
5
+ from pathlib import Path
6
+
7
+ import typer
8
+
9
+ from .core.fs_ops import copy_and_verify
10
+ from .core.media_ops import tag_media_file, copy_metadata_between_files
11
+
12
+
13
+ def archive_file(
14
+ shoot_name: str,
15
+ file_name: str,
16
+ tags_str: str,
17
+ keep_log: bool,
18
+ work_ssd_path: Path,
19
+ archive_path: Path,
20
+ ) -> None:
21
+ """
22
+ Tags a final rendered file, copies it to the archive Graded folder,
23
+ and optionally cleans up source files.
24
+ """
25
+ export_file_path = work_ssd_path / shoot_name / "03_Exports" / file_name
26
+ archive_graded_dir = archive_path / "Video" / "Graded"
27
+
28
+ if not export_file_path.exists():
29
+ typer.echo(f"Export file not found: {export_file_path}", err=True)
30
+ raise typer.Exit(code=1)
31
+
32
+ try:
33
+ archive_graded_dir.mkdir(parents=True, exist_ok=True)
34
+ except Exception as e:
35
+ typer.echo(
36
+ f"Could not create destination directories: {e}", err=True
37
+ )
38
+ raise typer.Exit(code=1)
39
+
40
+ tagged_file_path = tag_media_file(export_file_path, tags_str)
41
+
42
+ typer.echo(f"Copying tagged file to archive: {archive_graded_dir}")
43
+ if not copy_and_verify(tagged_file_path, archive_graded_dir):
44
+ typer.echo("Aborting cleanup due to copy failure.", err=True)
45
+ tagged_file_path.unlink()
46
+ raise typer.Exit(code=1)
47
+
48
+ tagged_file_path.unlink()
49
+
50
+ if not keep_log:
51
+ try:
52
+ source_folder = work_ssd_path / shoot_name / "01_Source"
53
+ if source_folder.exists():
54
+ typer.echo(
55
+ f"Cleaning up source files from {source_folder}..."
56
+ )
57
+ video_extensions = {
58
+ ".mp4",
59
+ ".mov",
60
+ ".mxf",
61
+ ".mts",
62
+ ".avi",
63
+ ".m4v",
64
+ ".braw",
65
+ ".r3d",
66
+ ".crm",
67
+ }
68
+ for video_file in source_folder.iterdir():
69
+ if (
70
+ video_file.is_file()
71
+ and video_file.suffix.lower() in video_extensions
72
+ ):
73
+ video_file.unlink()
74
+ typer.echo(f"Deleted: {video_file.name}")
75
+ except Exception as e:
76
+ typer.echo(
77
+ f"Warning: Could not clean up source files: {e}", err=True
78
+ )
79
+
80
+ typer.echo("\nArchive complete.")
81
+
82
+
83
+ def copy_metadata_folder(source_folder: Path, target_folder: Path) -> None:
84
+ """
85
+ Copies metadata from files in source_folder to matching files in target_folder.
86
+ Matches by filename stem (ignoring extension).
87
+ """
88
+ if not target_folder.exists() or not target_folder.is_dir():
89
+ typer.echo(f"Target folder not found: {target_folder}", err=True)
90
+ raise typer.Exit(code=1)
91
+
92
+ if not source_folder.exists() or not source_folder.is_dir():
93
+ typer.echo(f"Source folder not found: {source_folder}", err=True)
94
+ raise typer.Exit(code=1)
95
+
96
+ video_extensions = {
97
+ ".mp4",
98
+ ".mov",
99
+ ".mxf",
100
+ ".mts",
101
+ ".avi",
102
+ ".m4v",
103
+ ".braw",
104
+ ".r3d",
105
+ ".crm",
106
+ }
107
+ target_files = [
108
+ f
109
+ for f in target_folder.iterdir()
110
+ if f.is_file() and f.suffix.lower() in video_extensions
111
+ ]
112
+
113
+ if not target_files:
114
+ typer.echo("No files found in the target directory.")
115
+ return
116
+
117
+ success_count = 0
118
+ fail_count = 0
119
+
120
+ with typer.progressbar(target_files, label="Processing files") as progress:
121
+ for target_file in progress:
122
+ source_files = list(source_folder.glob(f"{target_file.stem}.*"))
123
+
124
+ if not source_files:
125
+ typer.echo(
126
+ f"\nWarning: No matching source file found for '{target_file.name}'. Skipping.",
127
+ err=True,
128
+ )
129
+ fail_count += 1
130
+ continue
131
+
132
+ source_file = source_files[0]
133
+ temp_output = target_file.with_name(
134
+ f"{target_file.stem}_temp_meta{target_file.suffix}"
135
+ )
136
+
137
+ try:
138
+ ffmpeg_cmd = [
139
+ "ffmpeg",
140
+ "-i",
141
+ str(target_file),
142
+ "-i",
143
+ str(source_file),
144
+ "-map",
145
+ "0:v",
146
+ "-map",
147
+ "0:a",
148
+ "-map_metadata",
149
+ "1",
150
+ "-c:v",
151
+ "copy",
152
+ "-c:a",
153
+ "copy",
154
+ str(temp_output),
155
+ ]
156
+ subprocess.run(
157
+ ffmpeg_cmd + ["-y"],
158
+ check=True,
159
+ capture_output=True,
160
+ text=True,
161
+ )
162
+
163
+ shutil.move(str(temp_output), str(target_file))
164
+ success_count += 1
165
+
166
+ except FileNotFoundError:
167
+ typer.echo(
168
+ "\nError: ffmpeg not found. Please install it and ensure it's in your PATH.",
169
+ err=True,
170
+ )
171
+ raise typer.Exit(code=1)
172
+ except subprocess.CalledProcessError as e:
173
+ typer.echo(
174
+ f"\nError processing '{target_file.name}': {e.stderr}",
175
+ err=True,
176
+ )
177
+ fail_count += 1
178
+ if temp_output.exists():
179
+ temp_output.unlink()
180
+
181
+ typer.echo(
182
+ f"\nMetadata copy complete. {success_count} files updated, {fail_count} files skipped."
183
+ )
184
+
185
+
186
+ def create_select_file(
187
+ shoot_name: str,
188
+ file_name: str,
189
+ tags_str: str,
190
+ work_ssd_path: Path,
191
+ archive_path: Path,
192
+ ) -> None:
193
+ """
194
+ Tags a graded select, copies metadata from source, and distributes it to the
195
+ archive and the local SSD selects folder.
196
+ """
197
+ export_file_path = work_ssd_path / shoot_name / "03_Exports" / file_name
198
+ source_folder = work_ssd_path / shoot_name / "01_Source"
199
+ archive_selects_dir = archive_path / "Video" / "Graded_Selects" / shoot_name
200
+ ssd_selects_dir = work_ssd_path / shoot_name / "05_Graded_Selects"
201
+
202
+ if not export_file_path.exists():
203
+ typer.echo(f"Export file not found: {export_file_path}", err=True)
204
+ raise typer.Exit(code=1)
205
+
206
+ try:
207
+ archive_selects_dir.mkdir(parents=True, exist_ok=True)
208
+ ssd_selects_dir.mkdir(parents=True, exist_ok=True)
209
+ except Exception as e:
210
+ typer.echo(
211
+ f"Could not create destination directories: {e}", err=True
212
+ )
213
+ raise typer.Exit(code=1)
214
+
215
+ typer.echo("Tagging file with new metadata...")
216
+ tagged_file_path = tag_media_file(export_file_path, tags_str)
217
+
218
+ if source_folder.exists():
219
+ source_files = list(source_folder.glob(f"{export_file_path.stem}.*"))
220
+ if source_files:
221
+ source_file = source_files[0]
222
+ typer.echo(f"Copying metadata from source file: {source_file.name}")
223
+ if copy_metadata_between_files(source_file, tagged_file_path):
224
+ typer.echo("✓ Metadata copied successfully from source file.")
225
+ else:
226
+ typer.echo(
227
+ "⚠ Warning: Could not copy metadata from source file. Continuing with tags only."
228
+ )
229
+ else:
230
+ typer.echo(
231
+ f"⚠ No matching source file found in 01_Source for '{export_file_path.stem}'. Skipping metadata copy."
232
+ )
233
+ else:
234
+ typer.echo(
235
+ f"⚠ Source folder not found: {source_folder}. Skipping metadata copy."
236
+ )
237
+
238
+ typer.echo(f"\nCopying tagged select to archive: {archive_selects_dir}")
239
+ archive_dest_file = archive_selects_dir / tagged_file_path.name
240
+ should_copy_to_archive = True
241
+ if archive_dest_file.exists():
242
+ try:
243
+ if (
244
+ tagged_file_path.stat().st_size
245
+ == archive_dest_file.stat().st_size
246
+ ):
247
+ typer.echo(
248
+ "⚠ File already exists in archive (same size). Skipping archive copy."
249
+ )
250
+ should_copy_to_archive = False
251
+ else:
252
+ typer.echo(
253
+ "⚠ File exists in archive but with different size. Copying anyway."
254
+ )
255
+ except Exception:
256
+ typer.echo(
257
+ "⚠ Could not check size of existing file in archive. Copying anyway."
258
+ )
259
+
260
+ if should_copy_to_archive:
261
+ if not copy_and_verify(tagged_file_path, archive_selects_dir):
262
+ typer.echo("Aborting due to archive copy failure.", err=True)
263
+ tagged_file_path.unlink()
264
+ raise typer.Exit(code=1)
265
+ else:
266
+ typer.echo("✓ File copied to archive.")
267
+
268
+ typer.echo(f"Copying tagged select to SSD: {ssd_selects_dir}")
269
+ ssd_dest_file = ssd_selects_dir / tagged_file_path.name
270
+ should_copy_to_ssd = True
271
+ if ssd_dest_file.exists():
272
+ try:
273
+ if (
274
+ tagged_file_path.stat().st_size
275
+ == ssd_dest_file.stat().st_size
276
+ ):
277
+ typer.echo(
278
+ "⚠ File already exists in SSD selects folder (same size). Skipping SSD copy."
279
+ )
280
+ should_copy_to_ssd = False
281
+ else:
282
+ typer.echo(
283
+ "⚠ File exists in SSD selects but with different size. Copying anyway."
284
+ )
285
+ except Exception:
286
+ typer.echo(
287
+ "⚠ Could not check size of existing file on SSD. Copying anyway."
288
+ )
289
+
290
+ if should_copy_to_ssd:
291
+ if not copy_and_verify(tagged_file_path, ssd_selects_dir):
292
+ typer.echo(
293
+ "Warning: Could not copy select to SSD. It is safely in the archive.",
294
+ err=True,
295
+ )
296
+ else:
297
+ typer.echo("✓ File copied to SSD selects folder.")
298
+
299
+ tagged_file_path.unlink()
300
+
301
+ typer.echo("\n✓ Create select complete.")