feat(arr): add Syncer::settle as the alass seam
Folds an implausible result and an unusable alass binary into one SyncState both the grab handler (#199) and reconcile loop (#196) can record without re-deriving the same match arms.
This commit is contained in:
@@ -51,7 +51,9 @@ pub use openai::{OpenAi, OpenAiConfig};
|
||||
pub use opensubtitles::{moviehash, OpenSubtitles, OpenSubtitlesConfig};
|
||||
pub use podnapisi::{Podnapisi, PodnapisiBuilder, DEFAULT_BASE_URL as PODNAPISI_DEFAULT_BASE_URL};
|
||||
pub use srt::Cue;
|
||||
pub use sync::{Outcome, Rejection, Syncer, DEFAULT_BINARY as ALASS_DEFAULT_BINARY};
|
||||
pub use sync::{
|
||||
Outcome, Rejection, Settled, SyncState, Syncer, DEFAULT_BINARY as ALASS_DEFAULT_BINARY,
|
||||
};
|
||||
pub use translate::{Backend, BackendId, Batch, BatchCue, TranslateFuture, TranslatedCue};
|
||||
|
||||
// `unused_crate_dependencies` is a per-target lint. Every provider and
|
||||
|
||||
+122
-1
@@ -131,6 +131,48 @@ impl Syncer {
|
||||
})
|
||||
}
|
||||
|
||||
/// Run [`Self::sync`] and settle the result into a form neither caller
|
||||
/// has to branch on twice.
|
||||
///
|
||||
/// #199's manual grab handler and #196's reconcile loop both write a
|
||||
/// fetched or translated subtitle, then call this before recording the
|
||||
/// row — the seam issue #214 asked for, so the sync-then-classify logic
|
||||
/// exists once. An [`Error`] from `sync` — a missing binary, a timeout, a
|
||||
/// non-zero exit — degrades to [`SyncState::NotRun`] with a warning
|
||||
/// logged, rather than failing the caller: a subtitle arr already has
|
||||
/// beats one it refuses because `alass` could not run.
|
||||
pub async fn settle(&self, video: impl AsRef<Path>, subtitle: impl AsRef<Path>) -> Settled {
|
||||
let subtitle = subtitle.as_ref();
|
||||
match self.sync(video, subtitle).await {
|
||||
Ok(Outcome::Accepted { content }) => Settled {
|
||||
content: Some(content),
|
||||
state: SyncState::Synced,
|
||||
},
|
||||
Ok(Outcome::Rejected(reason)) => {
|
||||
tracing::warn!(
|
||||
subtitle = %subtitle.display(),
|
||||
%reason,
|
||||
"alass sync rejected as implausible, keeping the unsynced original"
|
||||
);
|
||||
Settled {
|
||||
content: None,
|
||||
state: SyncState::Rejected,
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
subtitle = %subtitle.display(),
|
||||
%error,
|
||||
"alass sync could not run, leaving the subtitle unsynced"
|
||||
);
|
||||
Settled {
|
||||
content: None,
|
||||
state: SyncState::NotRun,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn `alass` and wait for it to write its output.
|
||||
async fn run(&self, video: &Path, subtitle: &Path, output: &Path) -> Result<()> {
|
||||
let mut command = Command::new(&self.binary);
|
||||
@@ -177,6 +219,33 @@ impl Syncer {
|
||||
}
|
||||
}
|
||||
|
||||
/// What settling a sync ended up recording, once an unusable `alass` and an
|
||||
/// implausible result are both folded into "did not change anything" (§15).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SyncState {
|
||||
/// `alass` could not be run to a plausible conclusion — binary missing,
|
||||
/// timed out, or exited non-zero. Not the same as [`Self::Rejected`]:
|
||||
/// nothing here says the *original* is implausible, only that nothing
|
||||
/// checked.
|
||||
NotRun,
|
||||
/// Ran, and the result was accepted.
|
||||
Synced,
|
||||
/// Ran, and the result was implausible. The unsynced original stands and
|
||||
/// the file is flagged.
|
||||
Rejected,
|
||||
}
|
||||
|
||||
/// The result of [`Syncer::settle`]: what belongs on disk, and what to
|
||||
/// record.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Settled {
|
||||
/// The synced text, only when it differs from what is already at the
|
||||
/// subtitle path. `None` means the file `settle` was given is already
|
||||
/// correct — the original, untouched.
|
||||
pub content: Option<String>,
|
||||
pub state: SyncState,
|
||||
}
|
||||
|
||||
/// What one sync run decided.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Outcome {
|
||||
@@ -250,7 +319,7 @@ mod tests {
|
||||
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
use super::{plausible, Outcome, Rejection, Syncer, MAX_SHIFT};
|
||||
use super::{plausible, Outcome, Rejection, SyncState, Syncer, MAX_SHIFT};
|
||||
use crate::error::Error;
|
||||
use crate::srt::Cue;
|
||||
|
||||
@@ -505,6 +574,58 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn settle_returns_the_synced_content_to_write() {
|
||||
let dir = tempfile::tempdir().expect("test setup and fake binary succeed");
|
||||
let binary = fake_binary(
|
||||
dir.path(),
|
||||
"printf '1\\n00:00:06,000 --> 00:00:07,500\\nola\\n\\n2\\n00:00:08,000 --> 00:00:09,000\\ntext\\n' > \"$3\"\n",
|
||||
)
|
||||
.await;
|
||||
let subtitle = write_subtitle(dir.path(), "in.srt", PLAIN).await;
|
||||
|
||||
let settled = Syncer::new()
|
||||
.with_binary(&binary)
|
||||
.settle(dir.path().join("video.mkv"), &subtitle)
|
||||
.await;
|
||||
|
||||
assert_eq!(settled.state, SyncState::Synced);
|
||||
assert!(settled.content.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn settle_on_rejection_keeps_the_original_and_flags_it() {
|
||||
let dir = tempfile::tempdir().expect("test setup and fake binary succeed");
|
||||
let binary = fake_binary(
|
||||
dir.path(),
|
||||
"printf '1\\n00:01:40,000 --> 00:01:41,500\\nola\\n\\n2\\n00:01:42,000 --> 00:01:43,000\\ntext\\n' > \"$3\"\n",
|
||||
)
|
||||
.await;
|
||||
let subtitle = write_subtitle(dir.path(), "in.srt", PLAIN).await;
|
||||
|
||||
let settled = Syncer::new()
|
||||
.with_binary(&binary)
|
||||
.settle(dir.path().join("video.mkv"), &subtitle)
|
||||
.await;
|
||||
|
||||
assert_eq!(settled.state, SyncState::Rejected);
|
||||
assert_eq!(settled.content, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn settle_on_a_missing_binary_degrades_to_not_run() {
|
||||
let dir = tempfile::tempdir().expect("test setup and fake binary succeed");
|
||||
let subtitle = write_subtitle(dir.path(), "in.srt", PLAIN).await;
|
||||
|
||||
let settled = Syncer::new()
|
||||
.with_binary("alass-not-installed")
|
||||
.settle(dir.path().join("video.mkv"), &subtitle)
|
||||
.await;
|
||||
|
||||
assert_eq!(settled.state, SyncState::NotRun);
|
||||
assert_eq!(settled.content, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejections_render_as_a_sentence() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user