Skip to content

Commit c19d0dd

Browse files
hclivessclaude
andcommitted
v3.6: duplicate-aware queue, delete source after processing, README refresh
- Queue duplicate detection by resolved, case-normalized path (also within a single drop/batch); skipped files reported in the status line - New 'Delete Source Files After Processing' option: removes each source right after its encode is verified, one by one; with 'Replace Original Files' the .old backup is dropped too; start dialog warns when enabled - Fix queue rows not showing running/success/failed colours (item stylesheet overrode per-item backgrounds); add state glyphs, auto-scroll to current file - README rewritten around current feature set; new screenshot Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 21754ea commit c19d0dd

10 files changed

Lines changed: 230 additions & 78 deletions

File tree

README.md

Lines changed: 72 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,86 @@
11
# videer
22

3-
FFmpeg batch GUI with AviSynth support for **deinterlacing** and profile configuration that can be used in [*
4-
*frameserving**](https://github.com/satishsampath/frame-server). Created as a replacement for inflexible batch files
3+
FFmpeg batch GUI with AviSynth+ support for **deinterlacing** and profile configuration that can be used in
4+
[**frameserving**](https://github.com/satishsampath/frame-server). Created as a replacement for inflexible batch files
55
that do not allow multiple encodings at the same time easily.
66
Videer integrates `QTGMC`, which provides much smoother results than
77
ffmpeg's `yadif`, [see for yourself](https://www.youtube.com/watch?v=jE47A57T5FA). `QTGMC` is the best deinterlacer in
88
existence. Including AI and DaVinci Studio.
99

10-
## Notice
11-
12-
If you are not using a frameserver and are just trying to transcode video with any feature that requires Avisynth+, and are running into issues, you should try with ffms2 enabled.
10+
![videer processing a queue](thumb.png)
1311

1412
## Why
1513

1614
FFmpeg is a superior command line video encoding tool. Videer serves as a GUI for it.
1715

18-
## New features:
19-
20-
- Multithreading using `SetFilterMTMode`
21-
- AviSynth+
22-
- 64bit Implementation
23-
- Matroska format as it is the only one to support `pgssub`
24-
- CUDA GPU acceleration support
25-
- Live queue: add files or drag-drop while encoding is running — new files are appended and processed without restarting
26-
- Optional resolution scaling (2160p → 360p presets or custom W×H, "never upscale" guard, selectable lanczos/bicubic/spline/bilinear/neighbor scaler)
27-
- Deinterlacer choice: `QTGMC` (AviSynth+, Windows) or FFmpeg's `bwdif` / `yadif` (any OS, any input)
28-
- AV1 (SVT-AV1) and VP9 encoders with per-encoder speed presets; NVENC uses the modern `p1``p7` presets
29-
- Live progress panel: per-file and total ETA, fps, speed, bitrate, output size, position — parsed from FFmpeg's machine-readable `-progress` output
30-
- Settings sanity check before starting (container/codec mismatches, ignored options, missing AviSynth plugins)
31-
- Cross-platform: runs on Windows, Linux and macOS (AviSynth+/QTGMC features are Windows-only); no shell or PowerShell calls
32-
33-
## Requirements:
34-
35-
- [FFmpeg](https://ffmpeg.org/) in the system PATH or next to videer (a Windows build is attached to the [latest release](https://github.com/hclivess/videer/releases/latest) as `ffmpeg.7z`)
36-
- For QTGMC deinterlacing / AviSynth+ processing (Windows): [AviSynth+](https://avs-plus.net/) — the tested MT installer is attached to the [latest release](https://github.com/hclivess/videer/releases/latest) as `AviSynthPlus-MT-r2772.exe`
37-
- No need to install AviSynth plugins, all are bundled in `plugins/`
38-
- Python 3.9+ — on Windows run `run.cmd` (installs `requirements.txt` via pip and starts the app), or manually `pip install -r requirements.txt && python main.py`
16+
## Notice
17+
18+
If you are not using a frameserver and are just trying to transcode video with any feature that requires AviSynth+, and
19+
are running into issues, you should try with ffms2 enabled.
20+
21+
## Features
22+
23+
### Queue
24+
25+
- Add files, add folders (recursive), or drag & drop files/folders onto the list; batches are sorted naturally
26+
(`Tape 1, Tape 2, Tape 10` — not `1, 10, 2`)
27+
- **Duplicate detection**: a file already in the queue is skipped, even if it is added again under a different spelling
28+
(relative/absolute path, different letter case, symlink) or appears twice in the same drop. Skipped files are
29+
reported in the status line.
30+
- **Live queue**: add or remove files while encoding is running — pending entries are appended/removed without
31+
restarting; files already processed or in progress cannot be removed
32+
- Drag entries to reorder; each entry is coloured by state (▶ running, ✔ done, ✖ failed) and shows its VMAF score
33+
when enabled
34+
- **Pause / Resume** the whole run (suspends FFmpeg and its children, ETAs are corrected for the pause) and **Stop**
3935

40-
### Preview:
36+
### Encoding
4137

42-
![thumb](thumb.png)
38+
- Video: H.264 (x264), H.265/HEVC (x265), AV1 (SVT-AV1), VP9, NVIDIA NVENC H.264/HEVC (`p1``p7` presets), ProRes,
39+
raw, or stream copy; CRF quality and per-encoder speed presets
40+
- Audio: AAC, MP3, Opus, AC3, FLAC, PCM, or stream copy; bitrate control and optional stereo downmix
41+
- Containers: MKV (the only one that carries `pgssub`), MP4, AVI, MOV, WebM
42+
- Deinterlacer choice: `QTGMC` (AviSynth+, Windows) or FFmpeg's `bwdif` / `yadif` (any OS, any input); field order
43+
and optional frame-rate halving
44+
- Optional resolution scaling (2160p → 360p presets or custom W×H, "never upscale" guard, lanczos / bicubic / spline /
45+
bilinear / neighbor scaler)
46+
- PAR / DAR presets and custom values, with metadata-only or resampling handling
47+
- AviSynth+ pipeline with `SetFilterMTMode` multithreading, ffms2 source filter, optional raw pre-transcode for
48+
problematic inputs, and a free-form script box; all required AviSynth plugins are bundled in `plugins/`
49+
- CUDA GPU acceleration support; 64-bit implementation
50+
- Extra FFmpeg arguments pass-through; save / load your own presets, plus bundled Web / High Quality / Archive / AV1
51+
presets; `defaults.json` support for your own startup defaults
52+
53+
### File handling
54+
55+
- Output is written next to the source as `<name>_<vcodec>_<acodec>_crf<N>_abr<N>[_<res>].<ext>`; timestamps are
56+
copied from the original
57+
- **Replace Original Files**: the encode takes over the original filename; the original is kept as `<name>.old<ext>`
58+
- **Delete Source Files After Processing**: permanently deletes each source right after *its* encode succeeds — one by
59+
one as the queue progresses, so disk space is freed while long batches are still running. The source is only removed
60+
when the output exists and is non-empty; files that fail or are stopped keep their source. Combined with *Replace
61+
Original Files* the `.old` backup is removed too. There is no recycle bin and no undo — the start dialog warns you
62+
when this is on.
63+
- Per-file `<name>.log` with the full FFmpeg command line and output; temporary `.avs` / `.ffindex` / `.trans.avi`
64+
files are cleaned up
65+
66+
### Feedback
67+
68+
- Live progress panel: per-file and total ETA, fps, speed, bitrate, output size, position — parsed from FFmpeg's
69+
machine-readable `-progress` output
70+
- Settings sanity check before starting (container/codec mismatches, ignored options, missing AviSynth plugins,
71+
destructive options)
72+
- Opt-in VMAF score after encoding (requires `libvmaf` in FFmpeg)
73+
- Cross-platform: runs on Windows, Linux and macOS (AviSynth+/QTGMC features are Windows-only); no shell or
74+
PowerShell calls
75+
76+
## Requirements
77+
78+
- [FFmpeg](https://ffmpeg.org/) in the system PATH or next to videer (a Windows build is attached to the
79+
[latest release](https://github.com/hclivess/videer/releases/latest) as `ffmpeg.7z`)
80+
- For QTGMC deinterlacing / AviSynth+ processing (Windows): [AviSynth+](https://avs-plus.net/) — the tested MT
81+
installer is attached to the [latest release](https://github.com/hclivess/videer/releases/latest) as
82+
`AviSynthPlus-MT-r2772.exe`
83+
- No need to install AviSynth plugins, all are bundled in `plugins/`
84+
- Python 3.9+ — on Windows run `run.cmd` (installs `requirements.txt` via pip and starts the app), or manually
85+
`pip install -r requirements.txt && python main.py`. Prebuilt binaries for Windows / Linux / macOS are attached to
86+
each release.

config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
# Application info
1010
APP_NAME = "videer"
11-
APP_VERSION = "3.5"
11+
APP_VERSION = "3.6"
1212
WINDOW_MIN_WIDTH = 1200
1313
WINDOW_MIN_HEIGHT = 900
1414

@@ -192,6 +192,7 @@
192192
"transcode_audio": False,
193193
"corrupt_fix": False,
194194
"replace_files": False,
195+
"delete_source": False,
195196
"threads": DEFAULT_THREADS,
196197
"par_mode": "auto",
197198
"par_value": "1:1",

main.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def connect_signals(self):
5555
# File manager signals
5656
self.file_manager.files_updated.connect(self.ui_manager.update_file_list)
5757
self.file_manager.file_count_changed.connect(self.ui_manager.update_file_count)
58+
self.file_manager.duplicates_skipped.connect(self._on_duplicates_skipped)
5859

5960
# Process manager signals
6061
self.process_manager.progress_updated.connect(self.ui_manager.update_progress)
@@ -133,6 +134,13 @@ def _on_files_updated_during_processing(self, files):
133134
if self.process_manager.is_processing():
134135
self.process_manager.sync_pending(files)
135136

137+
def _on_duplicates_skipped(self, paths):
138+
"""Tell the user which dropped/selected files were already in the queue"""
139+
names = [os.path.basename(p) for p in paths]
140+
shown = ", ".join(names[:5]) + (f" (+{len(names) - 5} more)" if len(names) > 5 else "")
141+
self.ui_manager.update_status(
142+
f"Skipped {len(names)} duplicate file{'s' if len(names) != 1 else ''} already in queue: {shown}")
143+
136144
def _on_files_removed(self, indices):
137145
"""Safe removal: block removal of already-processed or in-progress files"""
138146
if self.process_manager.is_processing():

models/file_models.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,25 @@
88
from typing import Optional, List, Dict, Any
99

1010

11+
def canonical_path(filepath: str) -> str:
12+
"""
13+
Identity key for duplicate detection: absolute, symlink-resolved,
14+
case-normalized (case-insensitive filesystems on Windows/macOS).
15+
"""
16+
try:
17+
resolved = os.path.realpath(os.path.abspath(filepath))
18+
except OSError:
19+
resolved = os.path.abspath(filepath)
20+
return os.path.normcase(resolved)
21+
22+
1123
class VideoFile:
1224
"""Represents a video file to be processed"""
1325

1426
def __init__(self, filepath: str, index: int = 0):
1527
self.index = index
1628
self.filepath = filepath
29+
self.canonical = canonical_path(filepath)
1730
self.filename = os.path.basename(filepath)
1831
self.directory = os.path.dirname(os.path.abspath(filepath))
1932
self.basename = os.path.splitext(self.filename)[0]
@@ -213,8 +226,9 @@ def clear(self):
213226
self.files.clear()
214227

215228
def contains(self, filepath: str) -> bool:
216-
"""Check if filepath is already in queue"""
217-
return any(f.filepath == filepath for f in self.files)
229+
"""Check if filepath (or another spelling of the same file) is already in queue"""
230+
key = canonical_path(filepath)
231+
return any(f.canonical == key for f in self.files)
218232

219233
def get_all(self) -> List[VideoFile]:
220234
"""Get all files in queue"""

modules/file_manager.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from typing import List, Optional
99
from PySide6.QtCore import QObject, Signal
1010

11-
from models.file_models import VideoFile, FileQueue
11+
from models.file_models import VideoFile, FileQueue, canonical_path
1212
from config import VIDEO_EXTENSIONS
1313

1414

@@ -24,44 +24,56 @@ class FileManager(QObject):
2424
# Signals
2525
files_updated = Signal(list) # List of VideoFile objects
2626
file_count_changed = Signal(int) # Number of files in queue
27+
duplicates_skipped = Signal(list) # Paths that were already queued (or repeated in the drop)
2728

2829
def __init__(self):
2930
super().__init__()
3031
self.queue = FileQueue()
3132

3233
def add_files(self, filepaths: List[str]) -> int:
3334
"""
34-
Add files to the queue
35-
Returns number of files added
35+
Add files to the queue, skipping duplicates.
36+
A file counts as a duplicate if it is already queued *or* appears twice
37+
in the same batch — compared by resolved, case-normalized path, so
38+
`C:\\Video\\a.mkv`, `c:/video/A.MKV` and a symlink to it are all one file.
39+
Returns number of files added.
3640
"""
37-
added_count = 0
3841
valid_files = []
42+
skipped = []
43+
seen = set()
44+
45+
def consider(candidate: str):
46+
if not self._is_valid_video_file(candidate):
47+
return
48+
key = canonical_path(candidate)
49+
if key in seen or self.queue.contains(candidate):
50+
skipped.append(candidate)
51+
return
52+
seen.add(key)
53+
valid_files.append(candidate)
3954

4055
for filepath in filepaths:
4156
# Dropped folders: expand recursively to the video files they contain
4257
if os.path.isdir(filepath):
4358
for root, dirs, entries in os.walk(filepath):
4459
dirs.sort(key=_natural_sort_key)
4560
for entry in sorted(entries, key=_natural_sort_key):
46-
candidate = os.path.join(root, entry)
47-
if self._is_valid_video_file(candidate) and not self.queue.contains(candidate):
48-
valid_files.append(candidate)
49-
added_count += 1
61+
consider(os.path.join(root, entry))
5062
continue
51-
if self._is_valid_video_file(filepath):
52-
if not self.queue.contains(filepath):
53-
valid_files.append(filepath)
54-
added_count += 1
55-
63+
consider(filepath)
64+
5665
if valid_files:
5766
# Multi-selects arrive in OS selection order (often lexicographic:
5867
# 1, 10, 2, 20) — order each added batch naturally instead
5968
if len(valid_files) > 1:
6069
valid_files.sort(key=_natural_sort_key)
6170
self.queue.add_files(valid_files)
6271
self._emit_updates()
63-
64-
return added_count
72+
73+
if skipped:
74+
self.duplicates_skipped.emit(skipped)
75+
76+
return len(valid_files)
6577

6678
def add_folder(self, folder_path: str) -> int:
6779
"""

modules/preset_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ def apply_settings(self, settings: Dict[str, Any]):
215215
checkbox_fields = [
216216
'stereo', 'deinterlace', 'tff', 'reduce_fps',
217217
'use_avisynth', 'use_ffms2', 'transcode_video',
218-
'transcode_audio', 'corrupt_fix', 'replace_files',
218+
'transcode_audio', 'corrupt_fix', 'replace_files', 'delete_source',
219219
'calculate_vmaf', 'no_upscale'
220220
]
221221

modules/process_manager.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,19 @@ def run(self):
124124
and self.settings.get('video_codec') != 'copy'):
125125
self._calculate_vmaf(file, file.get_full_output_path(), index)
126126

127+
output_path = file.get_full_output_path()
128+
delete_source = bool(self.settings.get('delete_source'))
127129
if self.settings.get('replace_files'):
128-
self.file_ops.replace_file(file.get_full_output_path(), file.filepath, file.logger)
130+
# With delete_source the .old backup is dropped as well
131+
self.file_ops.replace_file(output_path, file.filepath, file.logger,
132+
keep_backup=not delete_source)
129133
else:
130-
self.file_ops.preserve_timestamps(file.filepath, file.get_full_output_path(), file.logger)
134+
self.file_ops.preserve_timestamps(file.filepath, output_path, file.logger)
135+
if delete_source:
136+
# Free space as we go: remove this source right after its
137+
# encode is verified, before moving on to the next file
138+
if self.file_ops.delete_source(file.filepath, output_path, file.logger):
139+
self.info_signal.emit(f"Deleted source: {file.filename}")
131140

132141
file.cleanup_temp_files()
133142
self._completed_wall_times.append(time.time() - self._file_start_time)
@@ -656,6 +665,9 @@ def validate_settings(settings: Dict[str, Any]) -> List[str]:
656665
issues.append(f"{output_format.upper()} cannot carry text subtitles — they will be dropped.")
657666
if settings.get('transcode_video') or settings.get('transcode_audio'):
658667
issues.append("Raw pre-transcode uses an AVI intermediate: subtitles are not carried over.")
668+
if settings.get('delete_source'):
669+
issues.append("Delete Source Files is ON: each original is permanently deleted "
670+
"as soon as its encode succeeds (no .old backup, no recycle bin).")
659671
if settings.get('calculate_vmaf') and settings.get('deinterlace') and settings.get('reduce_fps'):
660672
issues.append("VMAF needs matching frame rates; halving FPS while deinterlacing will make it fail.")
661673

0 commit comments

Comments
 (0)