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" /> + + + + + + +