From edc09b4eabc3833237b10e74b03a5ddc4c985ce9 Mon Sep 17 00:00:00 2001 From: MatMacinf Date: Mon, 5 Jan 2026 13:27:48 +0100 Subject: [PATCH] Addreseed reported issues: guard longest track, consolidate metadate, improve test --- beetsplug/report.py | 196 ++++++++++++++++++++++++++------------------ test/test_report.py | 49 +++++++++-- 2 files changed, 157 insertions(+), 88 deletions(-) diff --git a/beetsplug/report.py b/beetsplug/report.py index 9466c76a8..a63e9bfe1 100644 --- a/beetsplug/report.py +++ b/beetsplug/report.py @@ -32,73 +32,105 @@ class ReportPlugin(BeetsPlugin): return [report_cmd] def _run_report(self, lib, opts, args): - """Collect statistics and print a report about the library.""" - items = list(lib.items()) - total_tracks = len(items) + """Run the plugin: generate summary and print report.""" + summary = self.generate_summary(lib) + self.print_report(summary) - if total_tracks == 0: + def generate_summary(self, lib): + """Generate all summary statistics for the library.""" + items = list(lib.items()) + summary = { + "total_tracks": len(items), + "artists": Counter(), + "albums": set(), + "genres": Counter(), + "years": Counter(), + "formats": Counter(), + "lengths": [], + "bitrates": [], + "items": items, + } + + for i in items: + if i.artist: + summary["artists"][i.artist] += 1 + if i.album: + summary["albums"].add(i.album) + if i.genre: + summary["genres"][i.genre] += 1 + if isinstance(i.year, int) and i.year > 0: + summary["years"][i.year] += 1 + if i.format: + summary["formats"][i.format] += 1 + if i.length: + summary["lengths"].append(i.length) + if i.bitrate: + summary["bitrates"].append(i.bitrate) + + # Safe longest/shortest track + tracks_with_length = [i for i in items if i.length] + summary["longest_track"] = max( + tracks_with_length, key=lambda i: i.length, default=None + ) + summary["shortest_track"] = min( + tracks_with_length, key=lambda i: i.length, default=None + ) + + # Missing metadata + summary["missing_genre"] = sum(1 for i in items if not i.genre) + summary["missing_year"] = sum( + 1 for i in items if not isinstance(i.year, int) or i.year <= 0 + ) + + # Time and bitrate + lengths = summary["lengths"] + summary["total_length"] = sum(lengths) if lengths else 0 + summary["avg_length"] = sum(lengths) / len(lengths) if lengths else 0 + + bitrates = summary["bitrates"] + summary["avg_bitrate"] = ( + sum(bitrates) // len(bitrates) if bitrates else None + ) + if summary["avg_bitrate"]: + if summary["avg_bitrate"] >= 900: + summary["quality"] = "Hi-Fi" + elif summary["avg_bitrate"] >= 320: + summary["quality"] = "High quality" + else: + summary["quality"] = "Standard quality" + else: + summary["quality"] = None + + # Decades + current_year = datetime.datetime.now().year + decades = [ + (y // 10) * 10 + for y in summary["years"].keys() + if 1900 <= y <= current_year + ] + summary["decade_counter"] = Counter(decades) + + return summary + + def print_report(self, summary): + """Print the library report based on precomputed summary statistics.""" + items = summary["items"] + + if summary["total_tracks"] == 0: print_("Your Beets library is empty.") return - # --- Collect metadata --- - artists = [i.artist for i in items if i.artist] - albums = [i.album for i in items if i.album] - genres = [i.genre for i in items if i.genre] - years = [ - i.year for i in items if isinstance(i.year, int) and i.year > 0 - ] - formats = [i.format for i in items if i.format] - lengths = [i.length for i in items if i.length] - bitrates = [i.bitrate for i in items if i.bitrate] - - # --- Counters --- - artist_counter = Counter(artists) - genre_counter = Counter(genres) - format_counter = Counter(formats) - year_counter = Counter(years) - - # --- Time calculations --- - total_length = sum(lengths) if lengths else 0 - avg_length = total_length / len(lengths) if lengths else 0 - def fmt_time(seconds): return str(datetime.timedelta(seconds=int(seconds))) - # --- Decades --- - current_year = datetime.datetime.now().year - decades = [(y // 10) * 10 for y in years if 1900 <= y <= current_year] - decade_counter = Counter(decades) - def decade_label(d): return f"{str(d)[-2:]}s" - # --- Wrapped insights --- - top_artist = artist_counter.most_common(1) - top_genre = genre_counter.most_common(1) - top_decade = decade_counter.most_common(1) - top_year = year_counter.most_common(1) - - longest_track = max(items, key=lambda i: i.length or 0) - shortest_track = min( - (i for i in items if i.length), key=lambda i: i.length, default=None - ) - - missing_genre = sum(1 for i in items if not i.genre) - missing_year = sum( - 1 for i in items if not isinstance(i.year, int) or i.year <= 0 - ) - - recent_tracks = sum(1 for y in years if y >= 2015) - older_tracks = len(years) - recent_tracks - - avg_bitrate = sum(bitrates) // len(bitrates) if bitrates else None - quality = ( - "Hi-Fi" - if avg_bitrate and avg_bitrate >= 900 - else "High quality" - if avg_bitrate and avg_bitrate >= 320 - else "Standard quality" - ) + # --- Top items --- + top_artist = summary["artists"].most_common(1) + top_genre = summary["genres"].most_common(1) + top_decade = summary["decade_counter"].most_common(1) + top_year = summary["years"].most_common(1) # ===================== REPORT ===================== print_("Beets Library Report") @@ -107,10 +139,11 @@ class ReportPlugin(BeetsPlugin): # --- Overview --- print_("Overview") - print_(f" Tracks: {total_tracks}") - print_(f" Albums: {len(set(albums))}") - print_(f" Artists: {len(set(artists))}") - print_(f" Genres: {len(set(genres))}") + print_(f" Tracks: {summary['total_tracks']}") + print_(f" Albums: {len(summary['albums'])}") + print_(f" Artists: {len(summary['artists'])}") + print_(f" Genres: {len(summary['genres'])}") + years = list(summary["years"].keys()) print_( f" Years: {min(years)} – {max(years)}" if years @@ -120,21 +153,23 @@ class ReportPlugin(BeetsPlugin): # --- Duration & quality --- print_("Listening time & quality") - print_(f" Total playtime: {fmt_time(total_length)}") - print_(f" Avg track length: {fmt_time(avg_length)}") - if avg_bitrate: - print_(f" Avg bitrate: {avg_bitrate} kbps ({quality})") - if format_counter: + print_(f" Total playtime: {fmt_time(summary['total_length'])}") + print_(f" Avg track length: {fmt_time(summary['avg_length'])}") + if summary["avg_bitrate"] is not None: + print_( + f" Avg bitrate: {summary['avg_bitrate']} kbps ({summary['quality']})" + ) + if summary["formats"]: print_( - f" Primary format: {format_counter.most_common(1)[0][0]}" + f" Primary format: {summary['formats'].most_common(1)[0][0]}" ) print_("-" * 60) # --- Decade distribution --- print_("Favorite musical decades") - if decade_counter: - total_decade_tracks = sum(decade_counter.values()) - for d, c in decade_counter.most_common(): + if summary["decade_counter"]: + total_decade_tracks = sum(summary["decade_counter"].values()) + for d, c in summary["decade_counter"].most_common(): pct = (c / total_decade_tracks) * 100 print_( f" {decade_label(d):>4} ({d}-{d + 9}): {c:>5} tracks ({pct:4.1f}%)" @@ -162,18 +197,21 @@ class ReportPlugin(BeetsPlugin): y, c = top_year[0] print_(f" Top year: {y} ({c} tracks)") - print_( - f" Longest track: {longest_track.artist} – {longest_track.title} ({fmt_time(longest_track.length)})" - ) - if shortest_track: + if summary["longest_track"]: + lt = summary["longest_track"] print_( - f" Shortest track: {shortest_track.artist} – {shortest_track.title} ({fmt_time(shortest_track.length)})" + f" Longest track: {lt.artist} – {lt.title} ({fmt_time(lt.length)})" + ) + if summary["shortest_track"]: + st = summary["shortest_track"] + print_( + f" Shortest track: {st.artist} – {st.title} ({fmt_time(st.length)})" ) + recent_tracks = sum(1 for y in summary["years"].keys() if y >= 2015) + older_tracks = len(summary["years"]) - recent_tracks print_(f" New music (2015+): {recent_tracks}") print_(f" Older music: {older_tracks}") - print_(f" Missing genre tags: {missing_genre}") - print_(f" Missing year tags: {missing_year}") - - print_("\nReport complete.") + print_(f" Missing genre tags: {summary['missing_genre']}") + print_(f" Missing year tags: {summary['missing_year']}") diff --git a/test/test_report.py b/test/test_report.py index 7597952c2..5753f197a 100644 --- a/test/test_report.py +++ b/test/test_report.py @@ -23,7 +23,8 @@ def add_item( genre="Genre", year=2000, length=180, - bitrate=320, + bitrate=320000, # bitrate in bits per second + format="MP3", ): """Add a single Item to the test library.""" item = Item( @@ -35,6 +36,7 @@ def add_item( year=year, length=length, bitrate=bitrate, + format=format, ) lib.add(item) @@ -51,13 +53,16 @@ def test_empty_library(capsys, library): def test_single_item(capsys, library): - """Test library with a single track.""" + """Test library with a single track, including bitrate/quality and format.""" add_item( library, title="Single Track", artist="Solo Artist", genre="Indie", year=2019, + length=240, + bitrate=256000, # 256 kbps + format="MP3", ) plugin = ReportPlugin() plugin._run_report(library, None, []) @@ -79,6 +84,27 @@ def test_single_item(capsys, library): assert "Top year:" in captured.out assert "2019" in captured.out + # --- Bitrate / quality --- + avg_bitrate_lines = [ + line + for line in captured.out.splitlines() + if line.strip().startswith("Avg bitrate:") + ] + assert avg_bitrate_lines, "Expected an 'Avg bitrate:' line in output" + avg_line = avg_bitrate_lines[0] + assert "kbps" in avg_line + assert "(" in avg_line and ")" in avg_line # Quality label + + # --- Primary format --- + primary_format_lines = [ + line + for line in captured.out.splitlines() + if line.strip().startswith("Primary format:") + ] + assert primary_format_lines, "Expected a 'Primary format:' line in output" + primary_line = primary_format_lines[0] + assert "MP3" in primary_line + def test_multiple_items(capsys, library): """Test library with multiple tracks from different decades and genres.""" @@ -116,10 +142,10 @@ def test_missing_metadata(capsys, library): "Track1", "Artist", "Album", - None, + None, # missing genre 2000, length=200, - bitrate=256, + bitrate=256000, ) add_item( library, @@ -127,7 +153,7 @@ def test_missing_metadata(capsys, library): "Artist", "Album", "Rock", - None, + None, # missing year length=180, bitrate=None, ) @@ -137,7 +163,12 @@ def test_missing_metadata(capsys, library): captured = capsys.readouterr() # --- Check missing metadata counts --- - assert "Missing genre" in captured.out - assert "1" in captured.out # At least one missing genre - assert "Missing year" in captured.out - assert "1" in captured.out # At least one missing year + # Use 'in' check instead of exact string match + assert any( + "Missing genre tags:" in line and "1" in line + for line in captured.out.splitlines() + ) + assert any( + "Missing year tags:" in line and "1" in line + for line in captured.out.splitlines() + )