Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1bdeb33a6f | |||
| 387ed7bc6a | |||
| f268d61fe4 | |||
| 24db32c09f | |||
| 0f6ae88ea6 | |||
| 4d99cf6015 | |||
| b75fa85ff5 | |||
| e7d47331c6 | |||
| 7cd31ebe55 | |||
| 3a37dddfd9 |
+1
-1
@@ -737,7 +737,7 @@ def prefetch_audio(video_path: str, embed_model: str | None = None,
|
|||||||
def scan_video(
|
def scan_video(
|
||||||
video_path: str,
|
video_path: str,
|
||||||
model: dict = None,
|
model: dict = None,
|
||||||
threshold: float = 0.30,
|
threshold: float = 0.50,
|
||||||
hop: float = 1.0,
|
hop: float = 1.0,
|
||||||
window: float = _WINDOW,
|
window: float = _WINDOW,
|
||||||
cancel_flag: object = None,
|
cancel_flag: object = None,
|
||||||
|
|||||||
+32
-11
@@ -418,27 +418,45 @@ class ProcessedDB:
|
|||||||
pass
|
pass
|
||||||
return max_n
|
return max_n
|
||||||
|
|
||||||
|
def delete_scan_exports(self, filename: str, profile: str) -> int:
|
||||||
|
"""Delete all scan_export entries for *filename* in *profile*.
|
||||||
|
|
||||||
|
Returns the number of rows deleted.
|
||||||
|
"""
|
||||||
|
if not self._enabled:
|
||||||
|
return 0
|
||||||
|
cur = self._con.execute(
|
||||||
|
"DELETE FROM processed"
|
||||||
|
" WHERE filename = ? AND profile = ? AND scan_export = 1",
|
||||||
|
(filename, profile),
|
||||||
|
)
|
||||||
|
self._con.commit()
|
||||||
|
return cur.rowcount
|
||||||
|
|
||||||
def get_vid_folder(self, filename: str, profile: str,
|
def get_vid_folder(self, filename: str, profile: str,
|
||||||
export_folder: str) -> str:
|
export_folder: str) -> str:
|
||||||
"""Return the vid_NNN folder name for a source video.
|
"""Return the vid_NNN folder name for a source video.
|
||||||
|
|
||||||
Checks existing DB output_paths first; if the video already has a
|
Checks existing DB output_paths first; if the video already has a
|
||||||
vid_NNN folder, returns it. Otherwise assigns the next available
|
vid_NNN folder, returns it. Otherwise assigns max(existing) + 1,
|
||||||
number, also checking disk for orphan vid folders.
|
also checking disk for orphan vid folders.
|
||||||
"""
|
"""
|
||||||
if not self._enabled:
|
if not self._enabled:
|
||||||
return "vid_001"
|
return "vid_001"
|
||||||
|
# Use the most recent entry (ORDER BY rowid DESC) for determinism
|
||||||
|
# when a file has entries across multiple vid folders.
|
||||||
row = self._con.execute(
|
row = self._con.execute(
|
||||||
"SELECT output_path FROM processed"
|
"SELECT output_path FROM processed"
|
||||||
" WHERE filename = ? AND profile = ? LIMIT 1",
|
" WHERE filename = ? AND profile = ?"
|
||||||
|
" ORDER BY rowid DESC LIMIT 1",
|
||||||
(filename, profile),
|
(filename, profile),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
if row:
|
if row:
|
||||||
parent = os.path.basename(os.path.dirname(row[0]))
|
parent = os.path.basename(os.path.dirname(row[0]))
|
||||||
if parent.startswith("vid_"):
|
if parent.startswith("vid_"):
|
||||||
return parent
|
return parent
|
||||||
# Collect all existing vid_NNN names from DB + disk
|
# Collect max vid_NNN number from DB + disk (never reuse old numbers)
|
||||||
existing: set[str] = set()
|
max_n = 0
|
||||||
rows = self._con.execute(
|
rows = self._con.execute(
|
||||||
"SELECT DISTINCT output_path FROM processed WHERE profile = ?",
|
"SELECT DISTINCT output_path FROM processed WHERE profile = ?",
|
||||||
(profile,),
|
(profile,),
|
||||||
@@ -446,17 +464,20 @@ class ProcessedDB:
|
|||||||
for (op,) in rows:
|
for (op,) in rows:
|
||||||
p = os.path.basename(os.path.dirname(op))
|
p = os.path.basename(os.path.dirname(op))
|
||||||
if p.startswith("vid_"):
|
if p.startswith("vid_"):
|
||||||
existing.add(p)
|
try:
|
||||||
|
max_n = max(max_n, int(p.split("_")[1]))
|
||||||
|
except (IndexError, ValueError):
|
||||||
|
pass
|
||||||
if os.path.isdir(export_folder):
|
if os.path.isdir(export_folder):
|
||||||
for d in os.listdir(export_folder):
|
for d in os.listdir(export_folder):
|
||||||
if d.startswith("vid_") and os.path.isdir(
|
if d.startswith("vid_") and os.path.isdir(
|
||||||
os.path.join(export_folder, d)
|
os.path.join(export_folder, d)
|
||||||
):
|
):
|
||||||
existing.add(d)
|
try:
|
||||||
n = 1
|
max_n = max(max_n, int(d.split("_")[1]))
|
||||||
while f"vid_{n:03d}" in existing:
|
except (IndexError, ValueError):
|
||||||
n += 1
|
pass
|
||||||
return f"vid_{n:03d}"
|
return f"vid_{max_n + 1:03d}"
|
||||||
|
|
||||||
def get_export_folders(self, profile: str = "default",
|
def get_export_folders(self, profile: str = "default",
|
||||||
include_scan_exports: bool = False) -> list[str]:
|
include_scan_exports: bool = False) -> list[str]:
|
||||||
|
|||||||
+10
-1
@@ -128,7 +128,16 @@ def build_ffmpeg_command(
|
|||||||
os.path.join(output_path, "frame_%04d.webp"),
|
os.path.join(output_path, "frame_%04d.webp"),
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
cmd += ["-c:v", encoder, "-c:a", "pcm_s16le", output_path]
|
cmd += ["-c:v", encoder]
|
||||||
|
if "nvenc" in encoder:
|
||||||
|
cmd += ["-preset", "p4", "-cq", "28"]
|
||||||
|
elif "vaapi" in encoder:
|
||||||
|
cmd += ["-qp", "28"]
|
||||||
|
elif "qsv" in encoder:
|
||||||
|
cmd += ["-global_quality", "28"]
|
||||||
|
elif "amf" in encoder:
|
||||||
|
cmd += ["-qp_i", "28", "-qp_p", "28"]
|
||||||
|
cmd += ["-c:a", "pcm_s16le", output_path]
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ class ScanWorker(QThread):
|
|||||||
progress = pyqtSignal(str) # status message
|
progress = pyqtSignal(str) # status message
|
||||||
|
|
||||||
def __init__(self, video_path: str, model: dict,
|
def __init__(self, video_path: str, model: dict,
|
||||||
threshold: float = 0.30,
|
threshold: float = 0.50,
|
||||||
prefetched_audio=None):
|
prefetched_audio=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._video_path = video_path
|
self._video_path = video_path
|
||||||
@@ -881,6 +881,8 @@ class ScanResultsPanel(QWidget):
|
|||||||
|
|
||||||
table.itemSelectionChanged.connect(
|
table.itemSelectionChanged.connect(
|
||||||
lambda t=table: self._on_selection_changed(t))
|
lambda t=table: self._on_selection_changed(t))
|
||||||
|
table.cellClicked.connect(
|
||||||
|
lambda r, c, t=table: self._on_cell_clicked(t, r, c))
|
||||||
table.cellChanged.connect(
|
table.cellChanged.connect(
|
||||||
lambda r, c, t=table: self._on_cell_changed(t, r, c))
|
lambda r, c, t=table: self._on_cell_changed(t, r, c))
|
||||||
container_layout.addWidget(table)
|
container_layout.addWidget(table)
|
||||||
@@ -973,9 +975,24 @@ class ScanResultsPanel(QWidget):
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
def _on_selection_changed(self, table: QTableWidget) -> None:
|
def _on_selection_changed(self, table: QTableWidget) -> None:
|
||||||
items = table.selectedItems()
|
"""Handle keyboard navigation (arrows) — seek to start of current row."""
|
||||||
if items:
|
cur = table.currentItem()
|
||||||
row = items[0].row()
|
if cur is None or not cur.isSelected():
|
||||||
|
selected = table.selectedItems()
|
||||||
|
if not selected:
|
||||||
|
return
|
||||||
|
cur = selected[-1]
|
||||||
|
start = table.item(cur.row(), 0).data(Qt.ItemDataRole.UserRole + 1)
|
||||||
|
if start is not None:
|
||||||
|
self.seek_requested.emit(float(start))
|
||||||
|
|
||||||
|
def _on_cell_clicked(self, table: QTableWidget, row: int, col: int) -> None:
|
||||||
|
"""Click Time → seek to start; click End → seek to last 3s of clip."""
|
||||||
|
if col == 1:
|
||||||
|
end = table.item(row, 1).data(Qt.ItemDataRole.UserRole)
|
||||||
|
if end is not None:
|
||||||
|
self.seek_requested.emit(max(0.0, float(end) - 3.0))
|
||||||
|
else:
|
||||||
start = table.item(row, 0).data(Qt.ItemDataRole.UserRole + 1)
|
start = table.item(row, 0).data(Qt.ItemDataRole.UserRole + 1)
|
||||||
if start is not None:
|
if start is not None:
|
||||||
self.seek_requested.emit(float(start))
|
self.seek_requested.emit(float(start))
|
||||||
@@ -1362,8 +1379,14 @@ class ScanResultsPanel(QWidget):
|
|||||||
super().keyPressEvent(event)
|
super().keyPressEvent(event)
|
||||||
|
|
||||||
|
|
||||||
|
_WAVEFORM_CACHE_DIR = os.path.join(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||||
|
"cache", "waveforms",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class WaveformWorker(QThread):
|
class WaveformWorker(QThread):
|
||||||
"""Extract a low-res waveform envelope in the background."""
|
"""Extract a low-res waveform envelope in the background (with disk cache)."""
|
||||||
done = pyqtSignal(object) # emits numpy array of peak values
|
done = pyqtSignal(object) # emits numpy array of peak values
|
||||||
|
|
||||||
def __init__(self, video_path: str, n_bins: int = 2000):
|
def __init__(self, video_path: str, n_bins: int = 2000):
|
||||||
@@ -1371,9 +1394,22 @@ class WaveformWorker(QThread):
|
|||||||
self._path = video_path
|
self._path = video_path
|
||||||
self._n_bins = n_bins
|
self._n_bins = n_bins
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _cache_path(video_path: str) -> str:
|
||||||
|
import hashlib
|
||||||
|
h = hashlib.md5(video_path.encode()).hexdigest()
|
||||||
|
return os.path.join(_WAVEFORM_CACHE_DIR, f"{h}.npy")
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
import numpy as np
|
import numpy as np
|
||||||
try:
|
try:
|
||||||
|
# Check cache first
|
||||||
|
cache = self._cache_path(self._path)
|
||||||
|
if os.path.exists(cache):
|
||||||
|
peaks = np.load(cache)
|
||||||
|
self.done.emit(peaks)
|
||||||
|
return
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
_bin("ffmpeg"), "-i", self._path,
|
_bin("ffmpeg"), "-i", self._path,
|
||||||
"-vn", "-ac", "1", "-ar", "8000",
|
"-vn", "-ac", "1", "-ar", "8000",
|
||||||
@@ -1393,6 +1429,9 @@ class WaveformWorker(QThread):
|
|||||||
mx = peaks.max()
|
mx = peaks.max()
|
||||||
if mx > 0:
|
if mx > 0:
|
||||||
peaks = peaks / mx
|
peaks = peaks / mx
|
||||||
|
# Save to cache
|
||||||
|
os.makedirs(_WAVEFORM_CACHE_DIR, exist_ok=True)
|
||||||
|
np.save(cache, peaks)
|
||||||
self.done.emit(peaks)
|
self.done.emit(peaks)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -2694,6 +2733,7 @@ class MainWindow(QMainWindow):
|
|||||||
self._cursor: float = 0.0
|
self._cursor: float = 0.0
|
||||||
self._export_counter: int = 1
|
self._export_counter: int = 1
|
||||||
self._export_worker: ExportWorker | None = None
|
self._export_worker: ExportWorker | None = None
|
||||||
|
self._export_queue: list[dict] = []
|
||||||
self._last_export_path: str = ""
|
self._last_export_path: str = ""
|
||||||
self._overwrite_path: str = "" # set when a marker is selected for re-export
|
self._overwrite_path: str = "" # set when a marker is selected for re-export
|
||||||
self._overwrite_group: list[str] = [] # all output_paths in the selected group
|
self._overwrite_group: list[str] = [] # all output_paths in the selected group
|
||||||
@@ -2959,7 +2999,7 @@ class MainWindow(QMainWindow):
|
|||||||
self._sld_threshold.setDecimals(2)
|
self._sld_threshold.setDecimals(2)
|
||||||
self._sld_threshold.setRange(0.0, 1.0)
|
self._sld_threshold.setRange(0.0, 1.0)
|
||||||
self._sld_threshold.setSingleStep(0.01)
|
self._sld_threshold.setSingleStep(0.01)
|
||||||
self._sld_threshold.setValue(0.30)
|
self._sld_threshold.setValue(0.50)
|
||||||
self._sld_threshold.setPrefix("Thr: ")
|
self._sld_threshold.setPrefix("Thr: ")
|
||||||
self._sld_threshold.setToolTip("Similarity threshold (0=match everything, 1=exact match)")
|
self._sld_threshold.setToolTip("Similarity threshold (0=match everything, 1=exact match)")
|
||||||
|
|
||||||
@@ -4181,6 +4221,8 @@ class MainWindow(QMainWindow):
|
|||||||
def _on_scan_seek(self, t: float) -> None:
|
def _on_scan_seek(self, t: float) -> None:
|
||||||
"""Seek player when a scan result row is clicked."""
|
"""Seek player when a scan result row is clicked."""
|
||||||
if self._file_path:
|
if self._file_path:
|
||||||
|
if not self._btn_scan_mode.isChecked():
|
||||||
|
self._btn_scan_mode.setChecked(True)
|
||||||
self._cursor = t
|
self._cursor = t
|
||||||
self._mpv.seek(t)
|
self._mpv.seek(t)
|
||||||
self._timeline.set_cursor(t)
|
self._timeline.set_cursor(t)
|
||||||
@@ -4491,9 +4533,6 @@ class MainWindow(QMainWindow):
|
|||||||
if not self._file_path:
|
if not self._file_path:
|
||||||
self._show_status("No video loaded")
|
self._show_status("No video loaded")
|
||||||
return
|
return
|
||||||
if self._export_worker and self._export_worker.isRunning():
|
|
||||||
self._show_status("Export already running…")
|
|
||||||
return
|
|
||||||
if self._scan_worker and self._scan_worker.isRunning():
|
if self._scan_worker and self._scan_worker.isRunning():
|
||||||
self._show_status("Scan already running")
|
self._show_status("Scan already running")
|
||||||
return
|
return
|
||||||
@@ -4614,38 +4653,77 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
# Clips go flat inside vid folder, numbered by video
|
# Clips go flat inside vid folder, numbered by video
|
||||||
jobs = []
|
jobs = []
|
||||||
self._auto_export_positions = []
|
positions = []
|
||||||
for area_idx, group in enumerate(groups):
|
for area_idx, group in enumerate(groups):
|
||||||
group_name = f"{name}_{vid_num:03d}_a{area_idx + 1}"
|
group_name = f"{name}_{vid_num:03d}_a{area_idx + 1}"
|
||||||
for sub, start_t in enumerate(group):
|
for sub, start_t in enumerate(group):
|
||||||
fname = f"{group_name}_{sub}{ext}"
|
fname = f"{group_name}_{sub}{ext}"
|
||||||
out = os.path.join(vid_folder, fname)
|
out = os.path.join(vid_folder, fname)
|
||||||
jobs.append((start_t, out, None, 0.5))
|
jobs.append((start_t, out, None, 0.5))
|
||||||
self._auto_export_positions.append((start_t, out))
|
positions.append((start_t, out))
|
||||||
|
|
||||||
self._show_status(f"Auto: exporting {len(jobs)} clips...")
|
|
||||||
|
|
||||||
short_side = self._spn_resize.value() or None
|
short_side = self._spn_resize.value() or None
|
||||||
self._export_short_side = short_side
|
|
||||||
self._export_portrait = "Off"
|
|
||||||
self._export_crop_center = 0.5
|
|
||||||
self._export_format = fmt
|
|
||||||
self._export_clip_count = 1
|
|
||||||
self._export_spread = spread
|
|
||||||
self._export_folder = folder
|
|
||||||
self._export_folder_suffix = ""
|
|
||||||
self._export_profile = self._profile
|
|
||||||
|
|
||||||
hw_on = self._chk_hw.isChecked() and self._hw_encoders
|
hw_on = self._chk_hw.isChecked() and self._hw_encoders
|
||||||
encoder = self._hw_encoders[0] if hw_on else "libx264"
|
encoder = self._hw_encoders[0] if hw_on else "libx264"
|
||||||
max_workers = min(self._spn_workers.value(), 3) if hw_on else self._spn_workers.value()
|
max_workers = min(self._spn_workers.value(), 3) if hw_on else self._spn_workers.value()
|
||||||
|
is_scan = getattr(self, '_auto_export_no_markers', False)
|
||||||
|
|
||||||
|
batch = {
|
||||||
|
"jobs": jobs,
|
||||||
|
"positions": positions,
|
||||||
|
"file_path": self._file_path,
|
||||||
|
"short_side": short_side,
|
||||||
|
"image_sequence": image_sequence,
|
||||||
|
"max_workers": max_workers,
|
||||||
|
"encoder": encoder,
|
||||||
|
"spread": spread,
|
||||||
|
"folder": folder,
|
||||||
|
"format": fmt,
|
||||||
|
"profile": self._profile,
|
||||||
|
"is_scan": is_scan,
|
||||||
|
}
|
||||||
|
|
||||||
|
if self._export_worker and self._export_worker.isRunning():
|
||||||
|
self._export_queue.append(batch)
|
||||||
|
n = len(self._export_queue)
|
||||||
|
self._show_status(f"Auto: queued ({n} pending)")
|
||||||
|
self._btn_auto_export.setEnabled(True)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._start_export_batch(batch)
|
||||||
|
|
||||||
|
def _start_export_batch(self, batch: dict) -> None:
|
||||||
|
"""Start an export batch immediately."""
|
||||||
|
self._auto_export_positions = batch["positions"]
|
||||||
|
self._export_short_side = batch["short_side"]
|
||||||
|
self._export_portrait = "Off"
|
||||||
|
self._export_crop_center = 0.5
|
||||||
|
self._export_format = batch["format"]
|
||||||
|
self._export_clip_count = 1
|
||||||
|
self._export_spread = batch["spread"]
|
||||||
|
self._export_folder = batch["folder"]
|
||||||
|
self._export_folder_suffix = ""
|
||||||
|
self._export_profile = batch["profile"]
|
||||||
|
self._auto_export_no_markers = batch["is_scan"]
|
||||||
|
self._export_batch_file = batch["file_path"]
|
||||||
|
|
||||||
|
# Replace old scan export entries for this video
|
||||||
|
if batch["is_scan"]:
|
||||||
|
fname = os.path.basename(batch["file_path"])
|
||||||
|
n_old = self._db.delete_scan_exports(fname, batch["profile"])
|
||||||
|
if n_old:
|
||||||
|
_log(f"Replacing {n_old} old scan export entries for {fname}")
|
||||||
|
|
||||||
|
n_queued = len(self._export_queue)
|
||||||
|
q_msg = f" ({n_queued} queued)" if n_queued else ""
|
||||||
|
self._show_status(f"Auto: exporting {len(batch['jobs'])} clips...{q_msg}")
|
||||||
|
|
||||||
self._export_worker = ExportWorker(
|
self._export_worker = ExportWorker(
|
||||||
self._file_path, jobs,
|
batch["file_path"], batch["jobs"],
|
||||||
short_side=short_side,
|
short_side=batch["short_side"],
|
||||||
image_sequence=image_sequence,
|
image_sequence=batch["image_sequence"],
|
||||||
max_workers=max_workers,
|
max_workers=batch["max_workers"],
|
||||||
encoder=encoder,
|
encoder=batch["encoder"],
|
||||||
)
|
)
|
||||||
self._export_worker.finished.connect(self._on_auto_clip_done)
|
self._export_worker.finished.connect(self._on_auto_clip_done)
|
||||||
self._export_worker.all_done.connect(self._on_auto_batch_done)
|
self._export_worker.all_done.connect(self._on_auto_batch_done)
|
||||||
@@ -4664,10 +4742,11 @@ class MainWindow(QMainWindow):
|
|||||||
start_t = t
|
start_t = t
|
||||||
break
|
break
|
||||||
is_scan = getattr(self, '_auto_export_no_markers', False)
|
is_scan = getattr(self, '_auto_export_no_markers', False)
|
||||||
|
batch_file = getattr(self, '_export_batch_file', self._file_path)
|
||||||
label = self._txt_label.currentText().strip()
|
label = self._txt_label.currentText().strip()
|
||||||
category = self._cmb_category.currentText()
|
category = self._cmb_category.currentText()
|
||||||
self._db.add(
|
self._db.add(
|
||||||
os.path.basename(self._file_path),
|
os.path.basename(batch_file),
|
||||||
start_t,
|
start_t,
|
||||||
path,
|
path,
|
||||||
label=label,
|
label=label,
|
||||||
@@ -4679,27 +4758,45 @@ class MainWindow(QMainWindow):
|
|||||||
clip_count=1,
|
clip_count=1,
|
||||||
spread=self._export_spread,
|
spread=self._export_spread,
|
||||||
profile=self._export_profile,
|
profile=self._export_profile,
|
||||||
source_path=self._file_path,
|
source_path=batch_file,
|
||||||
scan_export=is_scan,
|
scan_export=is_scan,
|
||||||
)
|
)
|
||||||
if not is_scan:
|
if not is_scan:
|
||||||
upsert_clip_annotation(self._export_folder, path, label)
|
upsert_clip_annotation(self._export_folder, path, label)
|
||||||
self._show_status(f"Auto: {os.path.basename(path)}")
|
n_queued = len(self._export_queue)
|
||||||
|
q_msg = f" ({n_queued} queued)" if n_queued else ""
|
||||||
|
self._show_status(f"Auto: {os.path.basename(path)}{q_msg}")
|
||||||
_log(f" auto clip done: {os.path.basename(path)}")
|
_log(f" auto clip done: {os.path.basename(path)}")
|
||||||
|
|
||||||
def _on_auto_batch_done(self):
|
def _on_auto_batch_done(self):
|
||||||
n = len(self._auto_export_positions)
|
n = len(self._auto_export_positions)
|
||||||
|
batch_file = getattr(self, '_export_batch_file', self._file_path)
|
||||||
|
batch_profile = self._export_profile
|
||||||
|
|
||||||
|
# Mark the batch's video as done in playlist
|
||||||
|
n_clips = self._db.get_clip_count(os.path.basename(batch_file), batch_profile)
|
||||||
|
self._playlist.mark_done(batch_file, n_clips)
|
||||||
|
|
||||||
|
# If current video matches the batch, refresh its markers
|
||||||
|
if self._file_path == batch_file:
|
||||||
|
self._refresh_markers()
|
||||||
|
self._update_next_label()
|
||||||
|
|
||||||
|
_log(f"Auto export complete: {n} clips ({os.path.basename(batch_file)})")
|
||||||
|
|
||||||
|
# Drain queue
|
||||||
|
if self._export_queue:
|
||||||
|
next_batch = self._export_queue.pop(0)
|
||||||
|
self._show_status(f"Auto: starting next batch ({len(self._export_queue)} remaining)")
|
||||||
|
self._start_export_batch(next_batch)
|
||||||
|
return
|
||||||
|
|
||||||
self._btn_auto_export.setEnabled(True)
|
self._btn_auto_export.setEnabled(True)
|
||||||
self._btn_cancel.setEnabled(False)
|
self._btn_cancel.setEnabled(False)
|
||||||
self._btn_export.setEnabled(True)
|
self._btn_export.setEnabled(True)
|
||||||
self._set_subprofile_btns_enabled(True)
|
self._set_subprofile_btns_enabled(True)
|
||||||
self._auto_export_no_markers = False
|
self._auto_export_no_markers = False
|
||||||
self._refresh_markers()
|
|
||||||
n_clips = self._db.get_clip_count(os.path.basename(self._file_path), self._profile)
|
|
||||||
self._playlist.mark_done(self._file_path, n_clips)
|
|
||||||
self._update_next_label()
|
|
||||||
self._show_status(f"Auto export complete: {n} clips")
|
self._show_status(f"Auto export complete: {n} clips")
|
||||||
_log(f"Auto export complete: {n} clips")
|
|
||||||
|
|
||||||
def _jump_to_next_scan_region(self) -> None:
|
def _jump_to_next_scan_region(self) -> None:
|
||||||
regions = sorted(self._timeline._scan_regions, key=lambda r: r[0])
|
regions = sorted(self._timeline._scan_regions, key=lambda r: r[0])
|
||||||
@@ -5005,7 +5102,9 @@ class MainWindow(QMainWindow):
|
|||||||
self._show_status("Cancelling export…")
|
self._show_status("Cancelling export…")
|
||||||
|
|
||||||
def _on_export_cancelled(self):
|
def _on_export_cancelled(self):
|
||||||
_log("Export cancelled")
|
n_dropped = len(self._export_queue)
|
||||||
|
self._export_queue.clear()
|
||||||
|
_log(f"Export cancelled (dropped {n_dropped} queued)")
|
||||||
self._btn_export.setEnabled(True)
|
self._btn_export.setEnabled(True)
|
||||||
self._btn_auto_export.setEnabled(True)
|
self._btn_auto_export.setEnabled(True)
|
||||||
self._set_subprofile_btns_enabled(True)
|
self._set_subprofile_btns_enabled(True)
|
||||||
@@ -5016,7 +5115,10 @@ class MainWindow(QMainWindow):
|
|||||||
n_clips = self._db.get_clip_count(os.path.basename(self._file_path), self._profile)
|
n_clips = self._db.get_clip_count(os.path.basename(self._file_path), self._profile)
|
||||||
if n_clips:
|
if n_clips:
|
||||||
self._playlist.mark_done(self._file_path, n_clips)
|
self._playlist.mark_done(self._file_path, n_clips)
|
||||||
self._show_status("Export cancelled", 4000)
|
msg = "Export cancelled"
|
||||||
|
if n_dropped:
|
||||||
|
msg += f" ({n_dropped} queued batches dropped)"
|
||||||
|
self._show_status(msg, 4000)
|
||||||
|
|
||||||
def changeEvent(self, event):
|
def changeEvent(self, event):
|
||||||
super().changeEvent(event)
|
super().changeEvent(event)
|
||||||
|
|||||||
Reference in New Issue
Block a user