diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3f9d16d..2d3e973 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -38,6 +38,15 @@ phase plan these entries follow.
and why — with three commands for checking a finished recording, because the failure this
catches produces a file that plays perfectly and measures wrong.
+- **The Metadata page now shows what a file's own bitrate and codec actually are, not just its
+ tags.** Repairing a library's metadata never said anything about the audio underneath it — a
+ file downloaded at 96 kbps and a lossless rip looked identical on the one page built to review a
+ folder of them. Each row now carries a line under its name — `MP3 · 320 kbps`, `FLAC ·
+ Lossless` — read straight from the file's own container properties, so it costs one more file
+ open per row and nothing else: no ffprobe, no decoding, no spectral analysis, no guessing beyond
+ what the header already claims. Only the one tier worth a second look, a lossy file under 128
+ kbps, gets the caution colour; everything else is background information.
+
### Fixed
- **One notch of the wheel jumped three tracks in the session list.** The list of what has been
diff --git a/src/Offstream.App/Resources/Strings.fr.resx b/src/Offstream.App/Resources/Strings.fr.resx
index e347c72..3be534d 100644
--- a/src/Offstream.App/Resources/Strings.fr.resx
+++ b/src/Offstream.App/Resources/Strings.fr.resx
@@ -562,6 +562,12 @@
Sera modifié
+
+ {0} · Sans perte
+
+
+ {0} · {1} kbit/s
+
Titre
diff --git a/src/Offstream.App/Resources/Strings.resx b/src/Offstream.App/Resources/Strings.resx
index 6d17934..48395f5 100644
--- a/src/Offstream.App/Resources/Strings.resx
+++ b/src/Offstream.App/Resources/Strings.resx
@@ -718,6 +718,14 @@
Will change
Marks a row whose save would alter the file, so those rows can be found without opening each one.
+
+ {0} · Lossless
+ {0} is the codec name, e.g. "FLAC". Shown under a row for a lossless file.
+
+
+ {0} · {1} kbps
+ {0} is the codec name, e.g. "MP3"; {1} is the bitrate the file reports.
+
Title
diff --git a/src/Offstream.App/Services/AppServices.cs b/src/Offstream.App/Services/AppServices.cs
index ab7c66b..643613f 100644
--- a/src/Offstream.App/Services/AppServices.cs
+++ b/src/Offstream.App/Services/AppServices.cs
@@ -84,6 +84,7 @@ public static IServiceCollection AddOffstream(
// independent of the recording pipeline's: a scanner over the file system, a TagLib#
// store, and a provider chain rebuilt per run so signing in takes effect immediately.
services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
diff --git a/src/Offstream.App/ViewModels/LibraryTrackViewModel.cs b/src/Offstream.App/ViewModels/LibraryTrackViewModel.cs
index dc69c98..4aa3892 100644
--- a/src/Offstream.App/ViewModels/LibraryTrackViewModel.cs
+++ b/src/Offstream.App/ViewModels/LibraryTrackViewModel.cs
@@ -2,6 +2,7 @@
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
+using System.Text;
using System.Windows.Media.Imaging;
using CommunityToolkit.Mvvm.ComponentModel;
using Offstream.App.Resources;
@@ -27,6 +28,12 @@ namespace Offstream.App.ViewModels;
///
public sealed partial class LibraryTrackViewModel : ObservableValidator
{
+ private static readonly CompositeFormat QualityLosslessFormat =
+ CompositeFormat.Parse(Strings.MetadataQualityLossless);
+
+ private static readonly CompositeFormat QualityBitrateFormat =
+ CompositeFormat.Parse(Strings.MetadataQualityBitrate);
+
private readonly LibraryTrack _track;
private readonly string? _existingTitle;
private readonly string? _existingArtist;
@@ -87,6 +94,9 @@ public LibraryTrackViewModel(LibraryTrack track)
CoverArt = LoadCoverArt(track.Existing.AlbumArtImage);
ExistingCoverArt = CoverArt;
+
+ QualityLabel = FormatQuality(track.Quality, FileName);
+ IsQualityLow = track.Quality.Tier == AudioQualityTier.Low;
}
/// The file's own name, which is what identifies the row.
@@ -133,6 +143,21 @@ public LibraryTrackViewModel(LibraryTrack track)
///
public string CurrentCopyright { get; }
+ ///
+ /// What the file's own audio properties say, e.g. "MP3 · 128 kbps" or "FLAC · Lossless", or
+ /// null when the file reported nothing usable.
+ ///
+ public string? QualityLabel { get; }
+
+ /// Whether has anything to show.
+ public bool HasQuality => QualityLabel is not null;
+
+ ///
+ /// Whether the file is lossy at under 128 kbps — audibly compressed on most material, and the
+ /// one tier worth calling out rather than just stating.
+ ///
+ public bool IsQualityLow { get; }
+
/// The scanned track this row edits.
internal LibraryTrack Track => _track;
@@ -538,6 +563,41 @@ public void SeedMatchQuery()
MatchQuery = string.Join(' ', new[] { Artist, Title }.Where(part => !string.IsNullOrWhiteSpace(part)));
}
+ ///
+ /// Turns a container-property read into the one line the row shows. Deliberately just a
+ /// codec name and a number — no spectral analysis, no transcode detection, just what the
+ /// file's own header already says.
+ ///
+ private static string? FormatQuality(AudioQuality quality, string fileName)
+ {
+ var codec = CodecName(System.IO.Path.GetExtension(fileName), quality.Tier);
+
+ if (quality.Tier == AudioQualityTier.Lossless)
+ {
+ return string.Format(CultureInfo.CurrentCulture, QualityLosslessFormat, codec);
+ }
+
+ return quality.BitrateKbps is > 0
+ ? string.Format(CultureInfo.CurrentCulture, QualityBitrateFormat, codec, quality.BitrateKbps)
+ : null;
+ }
+
+ /// A short codec name from the file's extension, which the scanner already filtered on.
+ ///
+ /// .m4a is ambiguous by extension alone — Offstream only ever writes AAC into it, but a
+ /// file from elsewhere can be ALAC. already tells the two
+ /// apart, so a lossless .m4a is labelled ALAC rather than guessed wrong.
+ ///
+ private static string CodecName(string extension, AudioQualityTier tier) => extension.ToUpperInvariant() switch
+ {
+ ".M4A" => tier == AudioQualityTier.Lossless ? "ALAC" : "AAC",
+ ".MP3" => "MP3",
+ ".FLAC" => "FLAC",
+ ".OPUS" => "Opus",
+ ".OGG" => "Vorbis",
+ _ => extension.TrimStart('.').ToUpperInvariant(),
+ };
+
private static string Or(string? value, string fallback) =>
string.IsNullOrWhiteSpace(value) ? fallback : value;
diff --git a/src/Offstream.App/Views/Pages/MetadataPage.xaml b/src/Offstream.App/Views/Pages/MetadataPage.xaml
index b8f8b43..df9409c 100644
--- a/src/Offstream.App/Views/Pages/MetadataPage.xaml
+++ b/src/Offstream.App/Views/Pages/MetadataPage.xaml
@@ -107,6 +107,28 @@
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
+
+
+
+
+
+
+