Skip to content

Commit b2d38ed

Browse files
committed
feat: add precise cut controls
1 parent 342c933 commit b2d38ed

11 files changed

Lines changed: 125 additions & 18 deletions

File tree

src-tauri/src/models/download.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ pub struct OutputOverrides {
121121
pub add_metadata: Option<bool>,
122122
pub add_thumbnail: Option<bool>,
123123
pub save_thumbnail: Option<bool>,
124+
pub precise_cuts: Option<bool>,
124125
pub file_name_template: Option<String>,
125126
pub audio_file_name_template: Option<String>,
126127
pub restrict_filenames: Option<bool>,

src-tauri/src/runners/override_resolver.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ impl ApplyPatch<OutputSettings> for OutputOverrides {
7474
apply_copy_patch!(self, target, add_metadata);
7575
apply_copy_patch!(self, target, add_thumbnail);
7676
apply_copy_patch!(self, target, save_thumbnail);
77+
apply_copy_patch!(self, target, precise_cuts);
7778
apply_clone_patch!(self, target, file_name_template);
7879
apply_clone_patch!(self, target, audio_file_name_template);
7980
apply_copy_patch!(self, target, restrict_filenames);

src-tauri/src/runners/ytdlp_args.rs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -365,8 +365,10 @@ pub fn build_output_args(
365365
if partial_download.is_some_and(|value| value.section.is_some()) {
366366
// Embedding chapters breaks the video when downloading a section, as the chapter metadata track is longer than the video.
367367
args.push("--no-embed-chapters".into());
368-
// Ensure we don't get black screens or missing audio.
369-
args.push("--force-keyframes-at-cuts".into());
368+
if output_settings.precise_cuts {
369+
// Ensure we don't get black screens or missing audio when precise cuts are explicitly requested.
370+
args.push("--force-keyframes-at-cuts".into());
371+
}
370372
}
371373

372374
Ok(args)
@@ -1216,6 +1218,32 @@ mod tests {
12161218

12171219
assert!(args.contains(&"--download-sections".to_string()));
12181220
assert!(args.contains(&"--no-embed-chapters".to_string()));
1221+
assert!(!args.contains(&"--force-keyframes-at-cuts".to_string()));
1222+
assert_eq!(
1223+
args
1224+
.windows(2)
1225+
.filter(|pair| pair[0] == "--download-sections")
1226+
.map(|pair| pair[1].clone())
1227+
.collect::<Vec<_>>(),
1228+
vec!["*00:01:30-00:02:45".to_string()],
1229+
);
1230+
}
1231+
1232+
#[test]
1233+
fn partial_download_precise_cuts_add_force_keyframes_at_cuts() {
1234+
let format_options = make_video_format_options(Some(720), Some(60));
1235+
let mut settings = OutputSettings::default();
1236+
settings.precise_cuts = true;
1237+
let partial_download = PartialDownloadOverride {
1238+
section: Some(DownloadSection {
1239+
id: "a".into(),
1240+
start: "00:01:30".into(),
1241+
end: "00:02:45".into(),
1242+
}),
1243+
};
1244+
1245+
let args = build_output_args(&format_options, &settings, Some(&partial_download)).unwrap();
1246+
12191247
assert!(args.contains(&"--force-keyframes-at-cuts".to_string()));
12201248
assert_eq!(
12211249
args

src-tauri/src/runners/ytdlp_runner.rs

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,13 @@ impl<'a> YtdlpRunner<'a> {
160160
&self.cfg.sponsor_block,
161161
overrides.and_then(|value| value.sponsor_block.as_ref()),
162162
);
163-
self.args.extend(build_sponsorblock_args(&sponsor_block));
163+
let output_settings = resolve_with_patch(
164+
&self.cfg.output,
165+
overrides.and_then(|value| value.output.as_ref()),
166+
);
167+
self
168+
.args
169+
.extend(build_sponsorblock_args(&sponsor_block, output_settings.precise_cuts));
164170

165171
self
166172
}
@@ -453,19 +459,18 @@ fn normalize_extractor_args(value: &str) -> Option<String> {
453459
}
454460
}
455461

456-
fn build_sponsorblock_args(settings: &SponsorBlockSettings) -> Vec<String> {
462+
fn build_sponsorblock_args(settings: &SponsorBlockSettings, precise_cuts: bool) -> Vec<String> {
457463
let mut args = Vec::new();
458464

459465
if let Some(api_url) = &settings.api_url {
460466
args.extend_from_slice(&["--sponsorblock-api".into(), api_url.clone()]);
461467
}
462468

463469
if !settings.remove_parts.is_empty() {
464-
args.extend_from_slice(&[
465-
"--sponsorblock-remove".into(),
466-
settings.remove_parts.join(","),
467-
"--force-keyframes-at-cuts".into(),
468-
]);
470+
args.extend_from_slice(&["--sponsorblock-remove".into(), settings.remove_parts.join(",")]);
471+
if precise_cuts {
472+
args.push("--force-keyframes-at-cuts".into());
473+
}
469474
}
470475

471476
if !settings.mark_parts.is_empty() {
@@ -702,19 +707,28 @@ mod tests {
702707
}
703708

704709
#[test]
705-
fn sponsorblock_remove_adds_force_keyframes_at_cuts() {
710+
fn sponsorblock_remove_omits_force_keyframes_at_cuts_by_default() {
706711
let settings = SponsorBlockSettings {
707712
remove_parts: vec!["sponsor".into(), "intro".into()],
708713
..Default::default()
709714
};
710715

711716
assert_eq!(
712-
build_sponsorblock_args(&settings),
713-
vec![
714-
"--sponsorblock-remove",
715-
"sponsor,intro",
716-
"--force-keyframes-at-cuts",
717-
]
717+
build_sponsorblock_args(&settings, false),
718+
vec!["--sponsorblock-remove", "sponsor,intro"]
719+
);
720+
}
721+
722+
#[test]
723+
fn sponsorblock_remove_precise_cuts_add_force_keyframes_at_cuts() {
724+
let settings = SponsorBlockSettings {
725+
remove_parts: vec!["sponsor".into(), "intro".into()],
726+
..Default::default()
727+
};
728+
729+
assert_eq!(
730+
build_sponsorblock_args(&settings, true),
731+
vec!["--sponsorblock-remove", "sponsor,intro", "--force-keyframes-at-cuts"]
718732
);
719733
}
720734

src-tauri/src/state/config_models.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ pub struct OutputSettings {
128128
pub add_metadata: bool,
129129
pub add_thumbnail: bool,
130130
pub save_thumbnail: bool,
131+
pub precise_cuts: bool,
131132
pub download_dir: Option<String>,
132133
pub file_name_template: String,
133134
pub audio_file_name_template: String,
@@ -142,6 +143,7 @@ impl Default for OutputSettings {
142143
add_metadata: true,
143144
add_thumbnail: true,
144145
save_thumbnail: false,
146+
precise_cuts: false,
145147
download_dir: None,
146148
file_name_template: "%(title).200s-(%(height)sp%(fps).0d).%(ext)s".into(),
147149
audio_file_name_template: "%(title).200s-(%(abr)dk).%(ext)s".into(),

src/components/media-view/TheOutputPreferences.vue

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
:badge="t('media.preferences.badges.override')"
77
:label="t('media.preferences.labels.output')"
88
>
9-
<output-settings-editor v-model="outputState" id-prefix="override">
9+
<output-settings-editor v-model="outputState" id-prefix="override" :show-precise-cuts="false">
1010
<template #video-extra>
1111
<label class="mb-2 mt-4 font-semibold" for="video-file-template">
1212
{{ t('location.filename.outputFormat.label') }}
@@ -59,6 +59,20 @@
5959
:duration-seconds="durationSeconds"
6060
/>
6161
</base-fieldset>
62+
<div class="flex max-w-xl flex-col gap-1">
63+
<label class="font-semibold" for="override-precise-cuts">
64+
{{ t('settings.output.preciseCuts.label') }}
65+
</label>
66+
<input
67+
id="override-precise-cuts"
68+
v-model="outputState.preciseCuts"
69+
type="checkbox"
70+
class="toggle toggle-primary my-1"
71+
/>
72+
<p class="label whitespace-pre-line">
73+
{{ t('settings.output.preciseCuts.hint') }}
74+
</p>
75+
</div>
6276
</template>
6377
</output-settings-editor>
6478
</base-fieldset>
@@ -133,6 +147,7 @@ const outputOverride = computed<DownloadOverrides['output'] | undefined>(() => {
133147
addThumbnail: outputState.value.addThumbnail,
134148
saveThumbnail: outputState.value.saveThumbnail,
135149
addMetadata: outputState.value.addMetadata,
150+
preciseCuts: outputState.value.preciseCuts,
136151
restrictFilenames: outputState.value.restrictFilenames,
137152
},
138153
{
@@ -141,6 +156,7 @@ const outputOverride = computed<DownloadOverrides['output'] | undefined>(() => {
141156
addThumbnail: global.addThumbnail,
142157
saveThumbnail: global.saveThumbnail,
143158
addMetadata: global.addMetadata,
159+
preciseCuts: global.preciseCuts,
144160
restrictFilenames: global.restrictFilenames,
145161
},
146162
);

src/components/output/OutputSettingsEditor.vue

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,21 @@
179179
</p>
180180
</div>
181181

182+
<div v-if="showPreciseCuts" class="mb-4 flex flex-col gap-1">
183+
<label class="font-semibold" :for="fieldId('precise-cuts')">
184+
{{ t('settings.output.preciseCuts.label') }}
185+
</label>
186+
<input
187+
:id="fieldId('precise-cuts')"
188+
type="checkbox"
189+
v-model="outputState.preciseCuts"
190+
class="toggle toggle-primary my-1"
191+
/>
192+
<p class="label whitespace-pre-line">
193+
{{ t('settings.output.preciseCuts.hint') }}
194+
</p>
195+
</div>
196+
182197
<slot name="after-common" />
183198
</div>
184199
</template>
@@ -206,6 +221,11 @@ const { idPrefix } = defineProps({
206221
required: false,
207222
default: '',
208223
},
224+
showPreciseCuts: {
225+
type: Boolean,
226+
required: false,
227+
default: true,
228+
},
209229
});
210230
211231
const { t } = useI18n();

src/components/settings/SettingsSponsorBlock.vue

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,20 @@
5454
v-model="settings.sponsorBlock.apiUrl"
5555
placeholder="https://sponsor.ajay.app"
5656
/>
57+
<div class="mt-4 flex flex-col gap-1">
58+
<label class="font-semibold" for="sponsorblockPreciseCuts">
59+
{{ t('settings.sponsorBlock.preciseCuts.label') }}
60+
</label>
61+
<input
62+
id="sponsorblockPreciseCuts"
63+
v-model="settings.output.preciseCuts"
64+
type="checkbox"
65+
class="toggle toggle-primary my-1"
66+
/>
67+
<p class="label whitespace-pre-line">
68+
{{ t('settings.sponsorBlock.preciseCuts.hint') }}
69+
</p>
70+
</div>
5771
</base-fieldset>
5872
</template>
5973

src/locales/en.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,10 @@
518518
"label": "Add title & artist info:",
519519
"hint": "Save metadata such as title, uploader/artist and description inside the file."
520520
},
521+
"preciseCuts": {
522+
"label": "Precise cut:",
523+
"hint": "Cuts at exact frames instead of nearby keyframes. Very slow.\nEnable only if normal cuts are wrong, garbled, or have black/missing segments."
524+
},
521525
"partialDownload": {
522526
"legend": "Partial download",
523527
"hint": "Set timestamps or chapters to download only some parts.",
@@ -674,7 +678,11 @@
674678
"placeholder": "Select 1 or more parts to mark with a chapter."
675679
},
676680
"apiUrl": {
677-
"label": "API URL"
681+
"label": "API URL:"
682+
},
683+
"preciseCuts": {
684+
"label": "Precise cut:",
685+
"hint": "Makes SponsorBlock removals and partial downloads cut at exact frames. Very slow.\nEnable only if normal cuts are wrong, garbled, or have black/missing segments."
678686
},
679687
"parts": {
680688
"sponsor": {

src/tauri/types/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export interface OutputSettings {
7676
addMetadata: boolean;
7777
addThumbnail: boolean;
7878
saveThumbnail: boolean;
79+
preciseCuts: boolean;
7980
downloadDir: string | null;
8081
fileNameTemplate: string;
8182
audioFileNameTemplate: string;
@@ -185,6 +186,7 @@ export const defaultOutputSettings: OutputSettings = {
185186
addMetadata: true,
186187
addThumbnail: true,
187188
saveThumbnail: false,
189+
preciseCuts: false,
188190
downloadDir: null,
189191
fileNameTemplate: '%(title).200s-(%(height)sp%(fps).0d).%(ext)s',
190192
audioFileNameTemplate: '%(title).200s-(%(abr)dk).%(ext)s',

0 commit comments

Comments
 (0)