Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f03f803
feat(desktop): the Import form says which program is missing, with Tr…
mbeisser1 Oct 8, 2026
187a7f0
fix(desktop): a check that ends mid-download fails the download, and …
mbeisser1 Oct 8, 2026
888459e
fix(desktop): a record that can't be written doesn't fail a download …
mbeisser1 Oct 8, 2026
cdc1413
fix(desktop): a Try again whose thread can't start says so, for the p…
mbeisser1 Oct 8, 2026
b7b543a
fix(desktop): a program the app has no download for is unavailable, w…
mbeisser1 Oct 8, 2026
4e27b32
fix(web): Try again says when a check is already running (#1053)
mbeisser1 Oct 8, 2026
2b3281e
fix(desktop): a run waiting for a download is marked as such and carr…
mbeisser1 Oct 8, 2026
1f0f886
fix(desktop): a run that fails on a failed download names the trouble…
mbeisser1 Oct 8, 2026
89d63a4
refactor(web): one status line per program state for Settings and the…
mbeisser1 Oct 8, 2026
0997c34
test(web): Try again at the Staging Review unblocks it, and a review …
mbeisser1 Oct 8, 2026
ddcc20c
docs: Try again on the Import form, in the troubleshooting sections, …
mbeisser1 Oct 8, 2026
615dafa
docs(desktop): a failed download's comments name Try again too (#1053)
mbeisser1 Oct 8, 2026
0c8d078
fix(desktop): Try again says when another app's check holds the Tools…
mbeisser1 Oct 8, 2026
df63696
fix(desktop): a Try again whose thread can't start never shows its pr…
mbeisser1 Oct 8, 2026
8a9ca6b
refactor(desktop): one borrow of the check's downloads, and the run's…
mbeisser1 Oct 8, 2026
2d46755
refactor(web): Settings writes the troubleshooting sentence with trou…
mbeisser1 Oct 8, 2026
f657f17
refactor(web): ToolName sits above ToolStatus, so each doc comment is…
mbeisser1 Oct 8, 2026
f86693e
fix(web): the already-running line goes when the check it spoke of ends
mbeisser1 Oct 8, 2026
5be1a49
Merge remote-tracking branch 'origin/main' into HEAD
mbeisser1 Oct 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ released versions carry their date on the heading.

### Features

- 2026-10-08: **The Import form says when a program the import needs is
missing, with Try again.** A WhatsApp import needs wtsexporter, and
**Convert** and **Compress** need ffmpeg and ffprobe. When one hasn't been
downloaded or its download failed, the Import form names it and gives the
reason, with **Try again**, which downloads it at once, and a link to
Troubleshooting. A WhatsApp import can't start without wtsexporter. An
import with **Convert** or **Compress** still starts, as before. An import
started while its program is still downloading waits for it, and its
progress line says so, such as "Waiting for the wtsexporter download: 12 MB
of 30 MB (40%)". When that download fails, the run fails with the reason
and names the troubleshooting section (#1053).

- 2026-10-08: **The desktop app downloads ffmpeg, ffprobe and wtsexporter
by itself.** Each time it starts, it downloads in the background whatever
is missing from its Tools Directory, with no button and nothing to wait
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,12 @@ These commands install both on `PATH`:
The desktop app finds the programs on `PATH`, else in its Tools Directory, and takes both from the same one.
[**Settings → System**](/docs/user/features/settings/system/#media) shows each under **Media**: `Found` with its path, its download's progress, why the download failed, `not found`, or `not used` with the reason.

An Import Run looks for ffmpeg at the Staging Review, not when the run starts, because Staging copies the original files and needs neither program.
When ffmpeg or ffprobe can't be used, the review names it, such as `Media can't use ffprobe.`, and the **Convert media** or **Compress media** button is disabled.
The desktop app downloads both programs into the Tools Directory when it starts, so a program missing at the review is usually still downloading.
When **Settings → System** says the download failed, the package manager commands above install ffmpeg and ffprobe instead.
With **Convert** or **Compress** chosen, the Import form says when ffmpeg or ffprobe can't be used, and why, but the run still starts, because Staging copies the original files and needs neither program.
At the Staging Review, the review names the program, such as `Media can't use ffprobe.`, and the **Convert media** or **Compress media** button is disabled.
**Try again**, on the form and at the review, downloads the program into the Tools Directory at once.
A program still downloading doesn't stop the review.
Media waits for the download, and its progress line says so.
When the download fails, the package manager commands above install ffmpeg and ffprobe instead.
The run keeps waiting at the review until the program is there.

## The size limit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ Import continues once the field is empty.

A WhatsApp import runs a separate program, `wtsexporter`, which the desktop app downloads when it starts.
[**Settings → System**](/docs/user/features/settings/system/#media) shows whether it arrived, or why the download failed, and [WhatsApp](/docs/user/import-sources/whatsapp/#wtsexporter) says where it comes from.
The Import form names the program and the reason, and its **Try again** downloads it at once instead of at the next start.

The app has no `wtsexporter` download for some computers, such as Linux on ARM.
There the Import form says so and offers no **Try again**, because nothing can be downloaded.
A `wtsexporter` put in the Tools Directory by hand is used instead.

### A WhatsApp import doesn't stop when cancelled

Expand All @@ -112,6 +117,7 @@ Two of the **Attachments** choices on the Import form run `ffmpeg` and `ffprobe`
In the desktop app, **Settings → System** shows the Tools Directory under **Media**.
The app looks on the system `PATH`, then in the Tools Directory, and takes both programs from the same one.
The lines there report each program as found, with its path, not found, or not used, with the reason.
When a download failed, **Try again** on the Import form, or at the Staging Review, downloads the program at once instead of at the next start.

## Convert

Expand Down
7 changes: 5 additions & 2 deletions docs/src/content/docs/docs/user/features/settings/system.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,16 @@ One line per program reports the result:
- A cross with `<program> not found` and where to put it.
A missing ffmpeg or ffprobe goes beside the other one, or both go in the Tools Directory, because the two are used only from one place.
A missing wtsexporter goes in the Tools Directory.
- A cross with `<program> not found, and the app has no download of it for this computer` and where to put a copy by hand.
The app has no wtsexporter download for Linux on ARM.
- A cross with `<program> not used` and the reason.
For ffmpeg and ffprobe, the reason is that they were found in two places, one only on `PATH` and the other only in the Tools Directory.
For wtsexporter, the reason is that it has no permission to run.
- A download arrow with `Downloading <program>` and how much has arrived, such as `12 MB of 29 MB (41%)`.
The line updates each second until the download ends.
- A cross with `<program> download failed` and the reason: no connection to the download's server, the status the server answered, a checksum that didn't match, a file that couldn't be written to the Tools Directory, or a program in the Tools Directory that doesn't run on this computer.
A failed download is tried again the next time the app starts.
- A cross with `<program> download failed` and the reason: no connection to the download's server, the status the server answered, a checksum that didn't match, a file that couldn't be written to the Tools Directory, a download that was interrupted or couldn't be started, or a program in the Tools Directory that doesn't run on this computer.
A failed download is tried again the next time the app starts, or at once with **Try again** on the Import form.
The line ends with a link to the program's section in [Troubleshooting](/docs/user/features/owner/troubleshooting/#import).
A program that doesn't run is not downloaded again, because the download would be the same file.
For ffmpeg and ffprobe, the reason says to install them with a package manager, because the app uses the copy on `PATH`.
While an older copy is in place, the line shows that copy as found instead, because the old copy is still the one used.
Expand Down
2 changes: 2 additions & 0 deletions docs/src/content/docs/docs/user/import-sources/whatsapp.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Two things make it hard:
The desktop app runs `wtsexporter`, from Message Crate's fork of the open-source [WhatsApp Chat Exporter](https://github.com/KnugiHK/WhatsApp-Chat-Exporter), [messagecrate/WhatsApp-Chat-Exporter](https://github.com/messagecrate/WhatsApp-Chat-Exporter), because only the fork records the ids a quoted reply is linked by.
Each time it starts, the desktop app downloads the pinned release into its Tools Directory when it isn't there, in the background, and [**Settings → System**](/docs/user/features/settings/system/#media) shows the program or how its download stands.
The app runs `wtsexporter` only from the Tools Directory, never from `PATH`.
A WhatsApp import can't start without it, and the Import form names the reason and offers **Try again**.
An import started while `wtsexporter` is still downloading waits for the download, and its progress line says so.
A program in the Tools Directory whose checksum is the pinned program's is kept, whoever put it there, and anything else under its name is replaced by the pinned program.

## WhatsApp on Android
Expand Down
9 changes: 9 additions & 0 deletions src-tauri/src/commands/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use message_crate_core::{IssueSink, ProgressEvent, ProgressSink, RunIssue, Write
use serde::Serialize;
use tauri::{AppHandle, Emitter};

use crate::tool_downloads::Program;

/// One log line for the UI's log panel. Payload: `String`.
pub const LOG: &str = "extract:log";
/// Progress-bar numbers. Payload: [`ExtractProgressEvent`].
Expand Down Expand Up @@ -92,6 +94,12 @@ pub struct ExtractProgressEvent {
/// Extra step status the UI shows. On `setup` it is the step's label.
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
/// The program the run waits for while it downloads (#1053). The event
/// then has no counts, `bytes_done` is how much has arrived and
/// `bytes_total` the file's size when the server said, and the window
/// says "Waiting for the <program> download" with them.
#[serde(skip_serializing_if = "Option::is_none")]
pub waiting: Option<Program>,
}

impl ExtractProgressEvent {
Expand All @@ -104,6 +112,7 @@ impl ExtractProgressEvent {
bytes_done: None,
bytes_total: None,
status: None,
waiting: None,
}
}
}
Expand Down
27 changes: 26 additions & 1 deletion src-tauri/src/commands/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ use super::paths::{logs_dir, scratch_dir};
use crate::app_directories::RunLog;
use crate::run_directories::RunDirectories;
use crate::state::{AppState, JobName};
use crate::tool_downloads::{Program, ToolDownloads};

/// Ask this process to stop the job that is running. Does nothing when no
/// job runs.
Expand Down Expand Up @@ -160,6 +161,7 @@ pub struct ExtractArgs {
pub fn extract(
state: tauri::State<'_, Arc<Mutex<AppState>>>,
directories: tauri::State<'_, RunDirectories>,
downloads: tauri::State<'_, ToolDownloads>,
app: tauri::AppHandle,
args: ExtractArgs,
) -> Result<(), String> {
Expand Down Expand Up @@ -201,7 +203,9 @@ pub fn extract(
let job = start_job(&state, JobName::Staging)?;

let app_handle = app.clone();
config.cancel = Some(job.cancel_flag());
let cancel = job.cancel_flag();
config.cancel = Some(cancel.clone());
let downloads = downloads.inner().clone();
// Two channels, two jobs: log lines are for the person reading the log
// panel, progress events are for the bar. Nothing reads counts out of
// the prose.
Expand All @@ -217,6 +221,16 @@ pub fn extract(
config.issues = Some(events::run_issue_sink(&app_handle, &run_log));

spawn_job(app, job, move || {
// A program still downloading is waited for, and the wait is the
// Staging row's progress line (#1053).
super::tools::wait_for_downloads(
&app_handle,
&downloads,
programs_run_by(&config.source),
&cancel,
"setup",
)
.inspect_err(|error| run_log.error(error))?;
let run_result = run_staging(&config, &output_dir, &media_settings)
.inspect_err(|error| run_log.error(error))?;
let payload = finished_payload(&run_result);
Expand Down Expand Up @@ -494,6 +508,17 @@ fn build_exporter_config(
.map_err(|errors| errors.join("; "))
}

/// The programs the Tools Directory holds that `source`'s exporter runs: a
/// WhatsApp import runs wtsexporter. Staging copies attachments as they are
/// whatever the person chose, so it never runs ffmpeg; the Media stage does
/// (`transcode_staging`).
fn programs_run_by(source: &SourceConfig) -> &'static [Program] {
match source {
SourceConfig::Whatsapp(_) => &[Program::Wtsexporter],
_ => &[],
}
}

/// Call the exporter that matches `config.source`.
///
/// # Errors
Expand Down
40 changes: 40 additions & 0 deletions src-tauri/src/commands/extract/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -742,3 +742,43 @@ fn a_staged_jsonl_attachment_that_is_not_utf8_does_not_fail_the_count() {

assert_eq!(counts, (1, 1));
}

/// A WhatsApp import waits for wtsexporter's download, and no other source
/// waits for a program: Staging runs ffmpeg for none of them (#1053).
#[test]
fn only_a_whatsapp_import_waits_for_a_download() {
use message_crate_core::{
AppleConfig, FormatConfig, GoSmsProConfig, ImazingConfig, OpenExtractConfig,
SmsBackupPlusConfig, SmsBackupRestoreConfig, WhatsappConfig,
};
let none: &[Program] = &[];
let sources = [
SourceConfig::GoSmsPro(GoSmsProConfig {
owner_phones: Vec::new(),
}),
SourceConfig::SmsBackupRestore(SmsBackupRestoreConfig {
owner_phones: Vec::new(),
}),
SourceConfig::SmsBackupPlus(SmsBackupPlusConfig {
owner_phones: Vec::new(),
owner_emails: Vec::new(),
phone_country: None,
verbose: false,
include_summary: false,
}),
SourceConfig::OpenExtract(OpenExtractConfig::default()),
SourceConfig::Imazing(ImazingConfig::default()),
SourceConfig::Apple(AppleConfig::default()),
SourceConfig::Format(FormatConfig::default()),
];
for source in &sources {
assert_eq!(programs_run_by(source), none, "{source:?}");
}
for platform in [WhatsappPlatform::Android, WhatsappPlatform::Ios] {
let source = SourceConfig::Whatsapp(WhatsappConfig {
platform: Some(platform),
..WhatsappConfig::default()
});
assert_eq!(programs_run_by(&source), [Program::Wtsexporter]);
}
}
15 changes: 15 additions & 0 deletions src-tauri/src/commands/staging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ use super::jobs::{spawn_job, start_job};
use crate::app_directories::RunLog;
use crate::run_directories::{self, RunDirectories, StagingRoot};
use crate::state::{AppState, JobName};
use crate::tool_downloads::{Program, ToolDownloads};

/// The directory `summarize_staging`, `transcode_staging`, `delete_run_dir`
/// and the Import Run record commands act on.
Expand Down Expand Up @@ -158,6 +159,7 @@ pub async fn summarize_staging(
bytes_done: None,
bytes_total: None,
status: None,
waiting: None,
},
);
})
Expand Down Expand Up @@ -241,6 +243,7 @@ fn transcode_summary(report: &TranscodeReport) -> String {
pub fn transcode_staging(
state: tauri::State<'_, Arc<Mutex<AppState>>>,
directories: tauri::State<'_, RunDirectories>,
downloads: tauri::State<'_, ToolDownloads>,
app: tauri::AppHandle,
args: RunDirArgs,
) -> Result<(), String> {
Expand All @@ -255,8 +258,19 @@ pub fn transcode_staging(

let app_handle = app.clone();
let issues = events::run_issue_sink(&app, &run_log);
let downloads = downloads.inner().clone();
spawn_job(app, job, move || {
if media_stage_converts {
// ffmpeg or ffprobe still downloading is waited for, and the
// wait is the Media row's progress line (#1053).
super::tools::wait_for_downloads(
&app_handle,
&downloads,
&[Program::Ffmpeg, Program::Ffprobe],
&cancel,
"media",
)
.inspect_err(|error| run_log.error(error))?;
events::log_to_run(
&app_handle,
&run_log,
Expand Down Expand Up @@ -284,6 +298,7 @@ pub fn transcode_staging(
bytes_done: None,
bytes_total: None,
status: None,
waiting: None,
},
);
},
Expand Down
Loading
Loading