Skip to content

feat(metadata): advertise Length / Bitrate / Codec for shared media files via ffprobe - #280

Merged
got3nks merged 7 commits into
amule-org:masterfrom
got3nks:feat/media-metadata-probe
Jul 3, 2026
Merged

feat(metadata): advertise Length / Bitrate / Codec for shared media files via ffprobe#280
got3nks merged 7 commits into
amule-org:masterfrom
got3nks:feat/media-metadata-probe

Conversation

@got3nks

@got3nks got3nks commented Jul 3, 2026

Copy link
Copy Markdown

Closes #140.

Phase B of the media-metadata work — aMule now extracts Length / Bitrate / Codec from local shared audio and video files with ffprobe and advertises them via FT_MEDIA_LENGTH / FT_MEDIA_BITRATE / FT_MEDIA_CODEC to eD2K servers and Kad, so other clients see populated columns when they search and find our files. Phase A (consumer-side rendering) already landed in #139.

Why this shape

The issue laid out four backend options. Ships with ffprobe subprocess as recommended:

  • Zero link-time dep — nothing changes at CMake configure / package time.
  • Clean license boundary — ffmpeg's LGPL / GPL license stays out-of-process.
  • User opts in via a preference + supplies (or auto-detects) the binary path. Feature is off by default; no existing install changes behaviour after upgrade.

Two longstanding stubs un-stubbed

Both publishers had TODO stubs that were never wired. The whole feature had to un-stub them:

  • Kad: Search.cpp:1422 gates the media-tag emit on file->GetMetaDataVer() > 0. GetMetaDataVer() was a hardcoded return 0 since forever (KnownFile.h:282, // TODO: This must be implemented if we ever want to have metadata.). Replaced with a derived getter that returns 1 when the file carries a non-zero FT_MEDIA_LENGTH — no separate persisted field, tag presence is the signal.
  • eD2K server: KnownFile.cpp:1242 had the comment // There, we could add MetaData info, if we ever get to have that. and built its outgoing tag list from hardcoded fields. Now appends FT_MEDIA_LENGTH / _BITRATE (VBT-encoded for capable clients / servers, fixed 32-bit otherwise) / _CODEC from m_taglist when present.

Threading + throttling

  • New CMediaProbeTask rides the existing CThreadScheduler at ETP_Low — hashing and file-completion tasks always preempt.
  • Task ctor snapshots the ffprobe path so the worker never touches thePrefs.
  • Result marshals back to the main thread via new MULE_EVT_MEDIA_PROBE — worker never touches CKnownFile state.
  • Extension gate: only ED2KFT_AUDIO / ED2KFT_VIDEO files (via GetED2KFileTypeID) get scheduled.
  • Retrofit gate: skip when FT_MEDIA_LENGTH > 0 — each file probed at most once per lifetime.

CSharedFileList::AddFile is the single choke point every shared file passes through. So the same code path handles both:

  • Runtime adds (new folder shared / watcher picks up a new file / partfile completes) → probe fires immediately.
  • Startup / Reload — walks known.met, each entry re-enters AddFile, eligible files get retrofitted in the background.

Startup is not deferred: AddTask is O(log N) enqueue; the actual ffprobe subprocess runs on the scheduler worker thread after OnInit returns. A 10 k-file library retrofit is ~100 ms of enqueue overhead on the main thread plus ~10-15 min of background probing at 50-100 ms per file. Tags persist to known.met per successful probe.

Preferences

New [MediaMetadata] section:

[MediaMetadata]
Enabled=0
FFProbePath=

UI panel added at the bottom of Preferences → Files: Enable checkbox, path field, Browse and Detect buttons. The whole block hides in amulegui (probing runs daemon-side).

AutoDetectPath() is a two-step probe: ffprobe -version on PATH first, then a per-platform well-known-paths fallback (Homebrew + MacPorts on macOS; Chocolatey + Scoop + C:\ffmpeg\bin on Windows; distro prefixes + /snap/bin on Linux + OpenBSD). Falls back to well-known paths because GUI-launched processes on macOS lose the shell PATH.

Verification

Smoke-tested end-to-end on macOS: dropped a 5-second 96 kbps sine-wave MP3 into the shared Incoming dir, SharedDirWatcher picked it up → hashed → AddFile scheduled probe → ffprobe returned duration + bit_rate + codec → main-thread handler attached tags. Read back byte-for-byte from known.met:

  • FT_MEDIA_LENGTH = 5 seconds
  • FT_MEDIA_BITRATE = 97 kbps
  • FT_MEDIA_CODEC = "mp3"

Wire format is standard ed2k tag encoding (UINT32 for length/bitrate, STRING for codec), matches the format Phase A already reads.

Notes for reviewers

  • Off by default — an upgraded install with the feature disabled runs identically to before.
  • Preserving tags across known.met load works via the existing m_taglist load/save — no known.met format bump needed.

@got3nks got3nks added this to the 3.1.0 milestone Jul 3, 2026
got3nks added a commit that referenced this pull request Jul 3, 2026
Clicking any Browse button inside Preferences on macOS (video player
picker on IDC_BROWSEV, browser picker on IDC_SELBROWSER, and — once
#280 lands — the ffprobe path picker on IDC_MEDIAMETA_FFPROBEBROWSE)
visually closes the whole Preferences dialog on both Open and Cancel.

Debug-tracing the dialog state showed it was still logically alive
after wxFileSelector returned (IsShown() == true, IsBeingDeleted()
== false, no wxEVT_CLOSE_WINDOW fired), so the disappearance wasn't
a close event we were mishandling. The modal NSOpenPanel that wxCocoa
opens steals key-window status while shown; when it dismisses, Cocoa
returns focus + Z-order to whichever aMule window was active before
Preferences opened, not to Preferences itself. The dialog sits alive
but ordered behind the main aMule window — Cmd+Tab back to aMule
brings it right up.

Fix: call Raise() on Preferences after wxFileSelector returns on
macOS. Wrapped in #ifdef __WXMAC__ so wxGTK / wxMSW pay nothing
(they don't reproduce the bug — the Windows CommonDialog is a real
top-level and wxGTK's file dialog doesn't reshuffle Z-order the same
way).
got3nks added 6 commits July 3, 2026 16:52
Standalone module that fronts an ffprobe subprocess to extract length
/ bitrate / codec from local shared files. Wiring into share-add and
Preferences follows in subsequent commits.

MediaProbe::AutoDetectPath() locates a usable binary in two steps:
first a bare `ffprobe -version` invocation to catch the PATH-installed
case, then a per-platform well-known-paths scan (Homebrew + MacPorts
on macOS; Chocolatey + Scoop + WinGet layouts on Windows; distro-
standard prefixes plus /snap/bin on Linux + OpenBSD). Fallback matters
because GUI-launched processes get a minimal PATH on macOS (launchd
default lacks /opt/homebrew) and unreliable PATH on Windows (system-
level updates from Chocolatey install don't always propagate to
running processes).

MediaProbe::Probe() forks ffprobe with `-of default=nk=0:nw=1` so the
output is a bare `key=value` stream — no JSON parser dependency needed
(the tree has none). Length rounds to whole seconds (FT_MEDIA_LENGTH's
wire format is uint32), bit_rate is converted from bps to kbps
(FT_MEDIA_BITRATE), and the first stream's codec_name wins (video for
video containers, audio for audio-only). Failures emit debug log
lines but never surface user-visible errors — a file the probe can't
read still shares fine, just without media tags.

Boolean return with out-param instead of std::optional so the module
matches the existing tree's C++ style. Callers MUST run Probe() off
the main thread; a subsequent commit extends SharedFileList's batch
path to do so.

Prep for amule-project#140 Phase B — advertising media metadata from own shared
files to ed2k servers + Kad.
The Kad publisher at kademlia/kademlia/Search.cpp:1422 already
iterates the file's media tags — but gates the whole emit on
`file->GetMetaDataVer() > 0`. GetMetaDataVer had been a longstanding
stub returning a hardcoded 0 with a TODO comment, so the code path
was dead: no ed2k client has ever seen media metadata from an
aMule-shared file. The ed2k server publish path
(CreateOfferedFilePacket) had a matching stub — a comment reading
"There, we could add MetaData info, if we ever get to have that."
that built its outgoing tag list without touching m_taglist for
media entries.

Un-stub both:

- GetMetaDataVer now derives from FT_MEDIA_LENGTH tag presence. No
  new persisted field: MediaProbe is the only source of the tag,
  and the tag itself already rides through known.met via the
  existing m_taglist load/save. Non-zero length is the "we've
  probed and have data worth publishing" signal, which is exactly
  what Kad's gate needs.

- CreateOfferedFilePacket appends FT_MEDIA_LENGTH / FT_MEDIA_BITRATE
  as CTagVarInt (VBT-encoded for capable eMule clients + TYPETAG-
  INTEGER-capable servers, fixed 32-bit otherwise) and FT_MEDIA_CODEC
  as CTagString when the corresponding tag is present and non-empty
  / non-zero. Each tag is optional per file; a file with only length
  publishes only length.

Prep for amule-project#140 Phase B — once MediaProbe wiring lands in
SharedFileList's share-add path, tags flow to peers automatically.
No change to known.met format (m_taglist already covers the
persistence layer).
Preferences plumbing for amule-project#140's ffprobe subprocess. Cfg items live
in a fresh /MediaMetadata/ config namespace (won't clash with any
/eMule/ key) with Enabled default false — an upgraded install won't
kick off background probing until the user opts in from Preferences
-> Files.

UI panel goes at the bottom of PreferencesFilesTab: an Enable
checkbox plus a "Path to ffprobe:" row with text field, Browse and
Detect buttons. Browse routes through the existing
OnButtonBrowseApplication switch (same file-selector infra the
video-player / browser fields use). Detect fires
MediaProbe::AutoDetectPath() and either populates the field or
shows a friendly "install ffmpeg or Browse manually" info dialog.

The whole box hides in amulegui via PrefsUnifiedDlg's
amuledOnlyPrefs[] — probing runs daemon-side, the remote GUI has
no business setting the path.

IDC IDs sit in the 10420-10423 functional band and 10370 orphan-
label slot, chosen to leave a 10-ID cushion above bind-to-
interface's IDC_INTERFACE = 10410 so parallel branches can grow
without collision. MediaProbe moves to COMMON_SOURCES so the
Detect button's handler still links in remote-GUI builds (the
button is hidden there but the event-table binding still needs
the symbol).

Follow-ups on the same branch: SharedFileList wiring (probe on
share-add + known.met load), extension gate, background-probe
throttling.
Closes the amule-project#140 wiring loop: shared audio / video files now get
probed with ffprobe, and the resulting FT_MEDIA_LENGTH / _BITRATE /
_CODEC tags are attached to the CKnownFile so the earlier ed2k +
Kad publisher un-stubs pick them up.

Threading + throttling:

- New CMediaProbeTask (ETP_Low) rides on the existing
  CThreadScheduler queue. That queue serialises tasks — a large
  library at first-boot-after-upgrade retrofits one file at a time,
  never stepping on hashing or completion which run at higher
  priority. No custom thread pool.

- Task ctor snapshots the ffprobe path so the worker never touches
  thePrefs, and remembers just the file's CMD4Hash + CPath. Result
  marshals back via CMediaProbeEvent (new MULE_EVT_MEDIA_PROBE),
  main thread resolves the hash to a live CKnownFile via
  CKnownFileList::FindKnownFileByID (the file may have been
  unshared while we were probing) and calls AddTagUnique for each
  populated field, MarkECChanged, and knownfiles->Save so the tags
  survive a crash.

Gating:

- Preference /MediaMetadata/Enabled must be true AND FFProbePath
  non-empty.
- Extension gate: only ED2KFT_AUDIO or ED2KFT_VIDEO files (from
  GetED2KFileTypeID) — skips the mass of docs / archives / images
  a typical share tree carries.
- Already-probed gate: skip when FT_MEDIA_LENGTH > 0. This is the
  retrofit cache — once a file has been probed successfully its
  tag rides through known.met and future launches skip re-probing.

Hook site is CSharedFileList::AddFile — the single choke point for
both runtime share-adds (user picks a new folder / drops a file in
Incoming) AND Reload()'s known.met walk, so an upgraded install
retrofits every existing shared file on the first launch after the
user enables the feature. No separate load-vs-add code path.

Event-table bindings for MULE_EVT_MEDIA_PROBE landed in both
amuled.cpp (daemon) and amule-gui.cpp (monolithic / remote GUI) so
the handler fires regardless of which app the scheduler dispatched
from.
Rerun of scripts/update-po.sh on top of the post-amule-project#281 (bind-to-
interface) master so the Media metadata strings coexist with the
new bind-to-interface strings. Replaces the earlier standalone
regen that was skipped during the rebase.
@got3nks
got3nks force-pushed the feat/media-metadata-probe branch from 94f7e18 to ddd4e21 Compare July 3, 2026 14:56
The [MediaMetadata] Enabled checkbox now drives the enabled state of
IDC_MEDIAMETA_FFPROBEPATHTEXT / _FFPROBEPATH / _FFPROBEBROWSE /
_FFPROBEDETECT via the existing OnCheckBoxChange handler, and the
initial state is applied at dialog open. Previously the path input,
Browse and Detect buttons stayed live-looking with the feature
disabled, which read as "I can configure ffprobe here" while
nothing downstream would ever run.
@got3nks

got3nks commented Jul 3, 2026

Copy link
Copy Markdown
Author

Testing summary before merge.

Platforms built + smoke-tested end-to-end (drop probed mp3 → parse known.met for FT_MEDIA_{LENGTH,BITRATE,CODEC}):

  • macOS 15 ARM64 (host, Homebrew ffprobe 8.x): OK — 7 s / 129 kbps / mp3 matches ffprobe truth byte-for-byte.
  • Ubuntu 26.04 ARM64 (amule-dev-vm, distro ffprobe 8.0.1): OK — same file, same hash, same tags.
  • Windows 11 ARM64 (amule-dev-windows, gyan.dev ffprobe): OK — same file, same hash, same tags. Auto-detect resolved via where.exe ffprobe.

Wire-side verification (temp debug branch, not in this PR):

  • ed2k: CKnownFile::CreateOfferedFilePacket emits the three FT_MEDIA_* tags into the OP_OFFERFILES packet the moment the daemon connects to a server. Verified on wire via DBG-MEDIA-ED2K offer 'probe-test.mp3' hash=7F837FB1 to=srv L=7 B=129 C='mp3'. ed2k publish is not firewall-gated (server accepts LowID OFFERFILES fine), so tag propagation to other peers via server-side search works from any node.
  • Kad: CSearch::PreparePacketForTags gates the FT_MEDIA_* emission on GetMetaDataVer() > 0, which we un-stubbed to return 1 when FT_MEDIA_LENGTH is present. Live Kad-publish log line requires !IsFirewalled || buddyConnected per CSharedFileList::Publish(), so needs an open UDP port or a buddy — the fire path itself is trivially verifiable-by-inspection and will emit a matching DBG-MEDIA-KAD publish-key … metaVer=1 L=… B=… C='…' line the moment the outer publisher fires. Testbed is firewalled with no buddy yet, so mainline log line is pending; will report if it materialises.

UX polish since the last review pass:

  • ffprobe path field + Browse + Detect + orphan label now gray out when the master [MediaMetadata]/Enabled checkbox is off, so the panel stops looking configurable when the feature is disabled.

Rebased on top of master (post #281 bind-to-interface), no IDC ID collisions (audited: 10370 label + 10420–10423 functional stay clear of bind-to-interface's 10362 + 10410 and everything else). Ready to merge on CI green.

@got3nks
got3nks merged commit ada17df into amule-org:master Jul 3, 2026
10 checks passed
@got3nks
got3nks deleted the feat/media-metadata-probe branch July 3, 2026 15:24
got3nks added a commit that referenced this pull request Jul 6, 2026
…CTag + codec fixes (#319)

* Fix CTag::operator= freeing the wrong union member

CTag holds an owned pointer (wxString* / CMD4Hash* / unsigned char*) in a
union that is only valid for string / hash / blob / bsob tags. operator=
freed that pointer keyed on the *right-hand side*'s type, so assigning a
string tag onto a slot that currently holds an int freed a garbage pointer
(the int's bits reinterpreted), corrupting the heap. std::vector<CTag>::erase()
hits this whenever it shifts a string tag over a non-string one -- observed as
a double free in a wxString destructor when a media tag set ([int length,
int bitrate, string codec]) is re-attached via AddTagUnique.

Free based on THIS tag's current type first (mirroring the destructor), then
copy from rhs (mirroring the copy constructor). Pre-existing since well before
3.0.1; independent of the media-probe change that exposes it.

* Isolate media probing on a dedicated bounded worker thread

ffprobe media-metadata extraction (#280) ran on the shared CThreadScheduler
alongside hashing and download completion. A slow or hung ffprobe -- readily
reproducible on a headless daemon -- blocked the scheduler, wedging every
download at PS_COMPLETING indefinitely and stalling shutdown.

Move probing onto a dedicated CMediaProbeThread (mirrors CPartFileHashThread),
so a stuck probe can only ever delay other probes, never completions or hashing.

Bound and make each probe killable: the ffprobe invocation is now a native
posix_spawn / CreateProcess child rather than wxExecute. wxExecute's synchronous
path is uncancellable, and its async path couples termination and pipe draining
to the main-thread event loop -- polling it from the worker is a use-after-free.
The child is waited on with a 30s wall-clock timeout and a cancel flag that
EndThread flips at shutdown; on either it is killed (by process group on POSIX,
so wrapper-style ffprobe from snap/flatpak is fully reaped) and the join returns
promptly.

Add a dedicated "Media Probe" (logMediaProbe) debug category; probe tracing
lives there rather than at normal log level.

* Prefer the video track's codec (then audio) in media probing

The probe reported the first stream's codec_name, which for a container whose
first stream is a subtitle or data track (common in mkv: a leading subrip
track) advertised e.g. "subrip" as the file's codec. Query codec_type
alongside codec_name and select the first video track's codec, falling back to
the first audio track's; subtitle / data streams never win.
got3nks added a commit that referenced this pull request Jul 6, 2026
…rit preview from search results (#321)

* Probe media files on download completion

Media-metadata probing (#280) only scheduled a probe when a file was first
inserted into the shared list. A download is shared as a partfile while
transferring, so on completion CPartFile::CompleteFileEnded -> SafeAddKFile
re-adds it, AddFile's insert no-ops (the hash is already present), and the
probe was never scheduled -- a downloaded media file only got its FT_MEDIA_*
tags on the next daemon restart.

Schedule the probe from the completion (alreadyCanonical) branch, now that the
file is complete on disk at its Incoming path. QueueProbe only enqueues, so it
never stalls completion. Force it (bypassing the already-has-FT_MEDIA gate via
a new bForceReprobe argument) so the authoritative local probe overwrites any
metadata inherited from the search result; startup rescans stay probe-once.

Also add two guards to MaybeScheduleMediaProbe:
 * Never probe an in-progress download: a partfile is shared while
   transferring, so this fires from AddFile() during the download when there is
   no complete file to read (the on-disk name is <hash>.part). Skip any
   non-forced probe of a partfile unconditionally -- the completion re-enters
   with bForceReprobe set. Metadata is derived exactly once, on completion.
 * Skip when the resolved path is not on disk, so a stale known.met record that
   outlived its deleted file never hands ffprobe a path that cannot succeed.

* Inherit advertised media metadata from search results as a preview

A search result from a source running #280 carries the file's FT_MEDIA_LENGTH
/ _BITRATE / _CODEC tags, but CPartFile(CSearchFile*) copied only FT_FILETYPE
/ FT_FILEFORMAT and dropped the rest. Add the media tags to the inherited set
so a download shows media metadata immediately while transferring, without a
local ffprobe. The authoritative probe on completion re-derives and overwrites
these values, so the inherited data is only a during-download preview.

Source-agnostic: ed2k and Kad use the same media tag IDs (TAG_MEDIA_* ==
FT_MEDIA_* == 0xD3/D4/D5) and share the CSearchFile construction path, so this
one change covers both.
Cflsft pushed a commit to Cflsft/amule that referenced this pull request Jul 6, 2026
…e-org#283)

Clicking any Browse button inside Preferences on macOS (video player
picker on IDC_BROWSEV, browser picker on IDC_SELBROWSER, and — once
amule-org#280 lands — the ffprobe path picker on IDC_MEDIAMETA_FFPROBEBROWSE)
visually closes the whole Preferences dialog on both Open and Cancel.

Debug-tracing the dialog state showed it was still logically alive
after wxFileSelector returned (IsShown() == true, IsBeingDeleted()
== false, no wxEVT_CLOSE_WINDOW fired), so the disappearance wasn't
a close event we were mishandling. The modal NSOpenPanel that wxCocoa
opens steals key-window status while shown; when it dismisses, Cocoa
returns focus + Z-order to whichever aMule window was active before
Preferences opened, not to Preferences itself. The dialog sits alive
but ordered behind the main aMule window — Cmd+Tab back to aMule
brings it right up.

Fix: call Raise() on Preferences after wxFileSelector returns on
macOS. Wrapped in #ifdef __WXMAC__ so wxGTK / wxMSW pay nothing
(they don't reproduce the bug — the Windows CommonDialog is a real
top-level and wxGTK's file dialog doesn't reshuffle Z-order the same
way).
Cflsft pushed a commit to Cflsft/amule that referenced this pull request Jul 6, 2026
…iles via ffprobe (amule-org#280)

* feat(mediaprobe): ffprobe subprocess module for local media metadata

Standalone module that fronts an ffprobe subprocess to extract length
/ bitrate / codec from local shared files. Wiring into share-add and
Preferences follows in subsequent commits.

MediaProbe::AutoDetectPath() locates a usable binary in two steps:
first a bare `ffprobe -version` invocation to catch the PATH-installed
case, then a per-platform well-known-paths scan (Homebrew + MacPorts
on macOS; Chocolatey + Scoop + WinGet layouts on Windows; distro-
standard prefixes plus /snap/bin on Linux + OpenBSD). Fallback matters
because GUI-launched processes get a minimal PATH on macOS (launchd
default lacks /opt/homebrew) and unreliable PATH on Windows (system-
level updates from Chocolatey install don't always propagate to
running processes).

MediaProbe::Probe() forks ffprobe with `-of default=nk=0:nw=1` so the
output is a bare `key=value` stream — no JSON parser dependency needed
(the tree has none). Length rounds to whole seconds (FT_MEDIA_LENGTH's
wire format is uint32), bit_rate is converted from bps to kbps
(FT_MEDIA_BITRATE), and the first stream's codec_name wins (video for
video containers, audio for audio-only). Failures emit debug log
lines but never surface user-visible errors — a file the probe can't
read still shares fine, just without media tags.

Boolean return with out-param instead of std::optional so the module
matches the existing tree's C++ style. Callers MUST run Probe() off
the main thread; a subsequent commit extends SharedFileList's batch
path to do so.

Prep for amule-org#140 Phase B — advertising media metadata from own shared
files to ed2k servers + Kad.

* feat(sharing): publish FT_MEDIA_{LENGTH,BITRATE,CODEC} to ed2k + Kad

The Kad publisher at kademlia/kademlia/Search.cpp:1422 already
iterates the file's media tags — but gates the whole emit on
`file->GetMetaDataVer() > 0`. GetMetaDataVer had been a longstanding
stub returning a hardcoded 0 with a TODO comment, so the code path
was dead: no ed2k client has ever seen media metadata from an
aMule-shared file. The ed2k server publish path
(CreateOfferedFilePacket) had a matching stub — a comment reading
"There, we could add MetaData info, if we ever get to have that."
that built its outgoing tag list without touching m_taglist for
media entries.

Un-stub both:

- GetMetaDataVer now derives from FT_MEDIA_LENGTH tag presence. No
  new persisted field: MediaProbe is the only source of the tag,
  and the tag itself already rides through known.met via the
  existing m_taglist load/save. Non-zero length is the "we've
  probed and have data worth publishing" signal, which is exactly
  what Kad's gate needs.

- CreateOfferedFilePacket appends FT_MEDIA_LENGTH / FT_MEDIA_BITRATE
  as CTagVarInt (VBT-encoded for capable eMule clients + TYPETAG-
  INTEGER-capable servers, fixed 32-bit otherwise) and FT_MEDIA_CODEC
  as CTagString when the corresponding tag is present and non-empty
  / non-zero. Each tag is optional per file; a file with only length
  publishes only length.

Prep for amule-org#140 Phase B — once MediaProbe wiring lands in
SharedFileList's share-add path, tags flow to peers automatically.
No change to known.met format (m_taglist already covers the
persistence layer).

* feat(prefs): [MediaMetadata]/Enabled + FFProbePath + UI panel

Preferences plumbing for amule-org#140's ffprobe subprocess. Cfg items live
in a fresh /MediaMetadata/ config namespace (won't clash with any
/eMule/ key) with Enabled default false — an upgraded install won't
kick off background probing until the user opts in from Preferences
-> Files.

UI panel goes at the bottom of PreferencesFilesTab: an Enable
checkbox plus a "Path to ffprobe:" row with text field, Browse and
Detect buttons. Browse routes through the existing
OnButtonBrowseApplication switch (same file-selector infra the
video-player / browser fields use). Detect fires
MediaProbe::AutoDetectPath() and either populates the field or
shows a friendly "install ffmpeg or Browse manually" info dialog.

The whole box hides in amulegui via PrefsUnifiedDlg's
amuledOnlyPrefs[] — probing runs daemon-side, the remote GUI has
no business setting the path.

IDC IDs sit in the 10420-10423 functional band and 10370 orphan-
label slot, chosen to leave a 10-ID cushion above bind-to-
interface's IDC_INTERFACE = 10410 so parallel branches can grow
without collision. MediaProbe moves to COMMON_SOURCES so the
Detect button's handler still links in remote-GUI builds (the
button is hidden there but the event-table binding still needs
the symbol).

Follow-ups on the same branch: SharedFileList wiring (probe on
share-add + known.met load), extension gate, background-probe
throttling.

* feat(shared): probe media metadata off-main + attach to CKnownFile

Closes the amule-org#140 wiring loop: shared audio / video files now get
probed with ffprobe, and the resulting FT_MEDIA_LENGTH / _BITRATE /
_CODEC tags are attached to the CKnownFile so the earlier ed2k +
Kad publisher un-stubs pick them up.

Threading + throttling:

- New CMediaProbeTask (ETP_Low) rides on the existing
  CThreadScheduler queue. That queue serialises tasks — a large
  library at first-boot-after-upgrade retrofits one file at a time,
  never stepping on hashing or completion which run at higher
  priority. No custom thread pool.

- Task ctor snapshots the ffprobe path so the worker never touches
  thePrefs, and remembers just the file's CMD4Hash + CPath. Result
  marshals back via CMediaProbeEvent (new MULE_EVT_MEDIA_PROBE),
  main thread resolves the hash to a live CKnownFile via
  CKnownFileList::FindKnownFileByID (the file may have been
  unshared while we were probing) and calls AddTagUnique for each
  populated field, MarkECChanged, and knownfiles->Save so the tags
  survive a crash.

Gating:

- Preference /MediaMetadata/Enabled must be true AND FFProbePath
  non-empty.
- Extension gate: only ED2KFT_AUDIO or ED2KFT_VIDEO files (from
  GetED2KFileTypeID) — skips the mass of docs / archives / images
  a typical share tree carries.
- Already-probed gate: skip when FT_MEDIA_LENGTH > 0. This is the
  retrofit cache — once a file has been probed successfully its
  tag rides through known.met and future launches skip re-probing.

Hook site is CSharedFileList::AddFile — the single choke point for
both runtime share-adds (user picks a new folder / drops a file in
Incoming) AND Reload()'s known.met walk, so an upgraded install
retrofits every existing shared file on the first launch after the
user enables the feature. No separate load-vs-add code path.

Event-table bindings for MULE_EVT_MEDIA_PROBE landed in both
amuled.cpp (daemon) and amule-gui.cpp (monolithic / remote GUI) so
the handler fires regardless of which app the scheduler dispatched
from.

* chore(clang-format): apply clang-format-18 to amule-org#140 files

* i18n(pot): regenerate catalogs for amule-org#140 UI additions

Rerun of scripts/update-po.sh on top of the post-amule-org#281 (bind-to-
interface) master so the Media metadata strings coexist with the
new bind-to-interface strings. Replaces the earlier standalone
regen that was skipped during the rebase.

* feat(prefs): gray out ffprobe path controls when Media metadata is off

The [MediaMetadata] Enabled checkbox now drives the enabled state of
IDC_MEDIAMETA_FFPROBEPATHTEXT / _FFPROBEPATH / _FFPROBEBROWSE /
_FFPROBEDETECT via the existing OnCheckBoxChange handler, and the
initial state is applied at dialog open. Previously the path input,
Browse and Detect buttons stayed live-looking with the feature
disabled, which read as "I can configure ffprobe here" while
nothing downstream would ever run.
got3nks added a commit that referenced this pull request Aug 7, 2026
…els (#847)

* feat(gui): show free disk space on the Downloads and Shared Files panels

The Downloads panel now reports the free space on the filesystem holding
the part files beside the queue size, and turns red once that space no
longer covers what is left to download. The Shared Files panel reports
the free space where finished downloads land. Requested in #757.

Which filesystem, and what to compare against, both follow from how the
directories actually work. There is one temp directory for every
category -- a category chooses where a file lands when it finishes, not
where it downloads -- so the figure is global and the warning is
measured against the whole queue rather than the category on screen; a
per-category comparison would only fire once it was already too late.
The threshold uses the bytes still to download, not the total queue
size: the bytes a part file already holds are off the free-space figure
already, so what is left is exactly what the disk still has to find room
for. Incoming, by contrast, is per category, and the Shared Files panel
has no category selector, so it reports the default category's -- and
carries no threshold, since nothing there stops when the disk fills.

Only the core can answer either question: the GUI may be on another
machine entirely, and even where it mounts the same share it can see a
different size or quota. Two new stats tags carry the figures over EC.
An absent tag means unknown rather than zero, so a daemon older than
these tags leaves the field empty instead of reporting a full disk.

Both getters are cache-backed, re-sampling at most every ten seconds.
statvfs()/GetDiskFreeSpaceEx() blocks on the directory, and on a network
mount -- temp and incoming commonly are one -- a slow or stale server
blocks it for as long as the mount's timeout. The callers are a
per-second GUI refresh and every stats poll from every connected client,
so an uncached read would multiply that exposure by the poll rate. A
path that cannot be queried at all reports FREE_SPACE_UNKNOWN, which
empties the label rather than printing "0 bytes" and never colours it
red.

The GUI side is gated on what is actually on screen: the timer refreshes
only the panel currently displayed, and nothing at all while the window
is hidden to the tray or minimized. That matters because the Downloads
refresh walks the whole queue to decide whether to warn -- skipped
outright when there is no figure to compare against -- and because each
label is resolved by name, which is now done once and cached rather than
per tick.

The Shared Files statistics box grows from four columns to five: the
collapse/expand button keeps a narrow column of its own and the size
figures get a column beside it, one figure per row, so the total size
lines up with the counters and the free space with the gauges.

One new translatable string, "Free space: %s", shared by both panels;
catalogs regenerated. The separator between the queue size and the free
space is built in code so translators are given the figure alone.

Builds clean on macOS (amule + amulegui + amuled); clang-format and both
clang-tidy tiers clean over the diff.

* perf(core): probe free space on its own thread, off the main loop

CStatistics::GetTempFreeSpace() / GetIncomingFreeSpace() called
CPath::GetFreeSpaceAt() inline, cached for ten seconds. Both callers are
latency critical: the GUI timer on the main thread, and the EC stats
reply on amuled's core thread.

statvfs() / GetDiskFreeSpaceEx() blocks on the directory it is asked
about. Measured read-only on a live host, 50 samples per path: ext4 0.002
ms median, mergerfs 0.678 ms, nfs4 0.204-0.222 ms, worst case 1.765 ms.
So the healthy cost was never the problem, and the interval was not
either. What those numbers do not cover is the pathological case: a cold
autofs mount takes tens to hundreds of milliseconds, and an unreachable
server blocks for timeo x retrans on a soft mount and indefinitely on a
hard one. Either directory is commonly a network mount, and that stall
landed on the main loop -- freezing the GUI and stalling EC replies.

Nothing else in the core reaches those filesystems from the main loop:
downloads write through CPartFileWriteThread, uploads read through
CUploadDiskIOThread. This probe was the one exception.

CFreeSpaceThread is a dedicated joinable worker rather than a task on
CThreadScheduler, for the reason CMediaProbeThread was split out (#280):
the scheduler runs one task at a time and owns completion, allocation,
hashing, verification and IP filtering, so a probe blocked on a hung
mount would not delay that queue but stop it -- and would couple
unrelated filesystems, a hung incoming mount blocking the hashing of part
files on a healthy local disk. Here a hung mount can only ever delay the
next sample: the figure goes stale, the label empties, nothing else
notices.

The two figures become std::atomic<sint64>, written by the worker and
read by the main and EC threads; the getters are plain relaxed loads that
cannot block, so no caller changed. The worker wakes every second and
samples each path only when its own interval has elapsed -- separately
timed, so a slow incoming cannot hold temp back -- and the wake interval
is shorter than the sample interval so shutdown does not wait out a
sleeping thread.

The worker reads no preferences. The temp and incoming paths can change
under the preferences dialog, no worker in the tree touches thePrefs, and
a wxString read while another thread assigns it is a race whatever the
value; so CamuleApp hands the worker mutex-guarded copies, at
construction and on each core tick. That also removes any question of
teardown ordering against thePrefs.

Constructed alongside mediaProbeThread, which is post-fork so the POSIX
threads belong to the daemon child (#849), and torn down beside it in
OnExit().

The panel-visibility gate stays, but it now skips pointless work rather
than a blocking call: CDownloadListCtrl::UpdateFreeSpace() still walks
the queue to decide whether to warn. Refreshing when a panel becomes
active as well as on the timer means switching to a panel no longer
shows the figure it had when it was last visible until the next tick.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(metadata): advertise media metadata (bitrate / length / codec) for shared files

1 participant