Moved logic to stats.py plugin as alternative usage with -o --overviwe flag. Change test file name to test_stats_overview.py changed entries in chanelog and creating new stats.rst file

This commit is contained in:
MatMacinf 2026-01-05 20:24:04 +01:00
parent 91d827fc91
commit 8aa96e8f2e
8 changed files with 492 additions and 492 deletions

View file

@ -1,6 +1,8 @@
"""The 'stats' command: show library statistics."""
import datetime
import os
from collections import Counter
from beets import logging, ui
from beets.util import syspath
@ -49,8 +51,175 @@ Albums: {len(albums)}
Album artists: {len(album_artists)}""")
def show_overview_report(lib, query):
"""Show overview-style library report."""
items = list(lib.items(query))
if not items:
ui.print_("Your Beets library is empty.")
return
# Collect statistics in a single pass
artists = Counter()
albums = set()
genres = Counter()
years = Counter()
formats = Counter()
lengths = []
bitrates = []
items_with_length = []
for item in items:
if item.artist:
artists[item.artist] += 1
if item.album:
albums.add(item.album)
if item.genre:
genres[item.genre] += 1
if isinstance(item.year, int) and item.year > 0:
years[item.year] += 1
if item.format:
formats[item.format] += 1
if item.length:
lengths.append(item.length)
items_with_length.append(item)
if item.bitrate:
bitrates.append(item.bitrate)
# Helper functions
def fmt_time(seconds):
return str(datetime.timedelta(seconds=int(seconds)))
def decade_label(d):
return f"{str(d)[-2:]}s"
# Calculate averages
total_length = sum(lengths) if lengths else 0
avg_length = sum(lengths) / len(lengths) if lengths else 0
avg_bitrate = sum(bitrates) // len(bitrates) if bitrates else None
# Determine quality label
if avg_bitrate:
if avg_bitrate >= 900:
quality = "Hi-Fi"
elif avg_bitrate >= 320:
quality = "High quality"
else:
quality = "Standard quality"
else:
quality = None
# Calculate decades
current_year = datetime.datetime.now().year
decades = [
(y // 10) * 10 for y in years.keys() if 1900 <= y <= current_year
]
decade_counter = Counter(decades)
# Get top items
top_artist = artists.most_common(1)
top_genre = genres.most_common(1)
top_decade = decade_counter.most_common(1)
top_year = years.most_common(1)
# Longest/shortest tracks
longest_track = (
max(items_with_length, key=lambda i: i.length)
if items_with_length
else None
)
shortest_track = (
min(items_with_length, key=lambda i: i.length)
if items_with_length
else None
)
# Missing metadata
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
)
# ===================== REPORT =====================
ui.print_("Beets Library Report")
ui.print_(f"Generated: {datetime.datetime.now():%Y-%m-%d %H:%M:%S}")
ui.print_("=" * 60)
# --- Overview ---
ui.print_("Overview")
ui.print_(f" Tracks: {len(items)}")
ui.print_(f" Albums: {len(albums)}")
ui.print_(f" Artists: {len(artists)}")
ui.print_(f" Genres: {len(genres)}")
if years:
ui.print_(f" Years: {min(years)} {max(years)}")
else:
ui.print_(" Years: n/a")
ui.print_("-" * 60)
# --- Duration & quality ---
ui.print_("Listening time & quality")
ui.print_(f" Total playtime: {fmt_time(total_length)}")
ui.print_(f" Avg track length: {fmt_time(avg_length)}")
if avg_bitrate is not None:
ui.print_(f" Avg bitrate: {avg_bitrate} kbps ({quality})")
if formats:
ui.print_(f" Primary format: {formats.most_common(1)[0][0]}")
ui.print_("-" * 60)
# --- Decade distribution ---
ui.print_("Favorite musical decades")
if decade_counter:
total_decade_tracks = sum(decade_counter.values())
for d, c in decade_counter.most_common():
pct = (c / total_decade_tracks) * 100
ui.print_(
f" {decade_label(d):>4} ({d}-{d + 9}): "
f"{c:>5} tracks ({pct:4.1f}%)"
)
else:
ui.print_(" n/a")
ui.print_("-" * 60)
# --- Wrapped summary ---
ui.print_("Your Music Wrapped")
if top_artist:
ui.print_(
f" Top artist: {top_artist[0][0]} ({top_artist[0][1]} tracks)"
)
if top_genre:
ui.print_(
f" Top genre: {top_genre[0][0]} ({top_genre[0][1]} tracks)"
)
if top_decade:
d, c = top_decade[0]
ui.print_(
f" Top decade: {decade_label(d)} ({d}-{d + 9}, {c} tracks)"
)
if top_year:
y, c = top_year[0]
ui.print_(f" Top year: {y} ({c} tracks)")
if longest_track:
ui.print_(
f" Longest track: {longest_track.artist} "
f"{longest_track.title} ({fmt_time(longest_track.length)})"
)
if shortest_track:
ui.print_(
f" Shortest track: {shortest_track.artist} "
f"{shortest_track.title} ({fmt_time(shortest_track.length)})"
)
ui.print_(f" Missing genre tags: {missing_genre}")
ui.print_(f" Missing year tags: {missing_year}")
def stats_func(lib, opts, args):
show_stats(lib, args, opts.exact)
if opts.overview:
show_overview_report(lib, args)
else:
show_stats(lib, args, opts.exact)
stats_cmd = ui.Subcommand(
@ -59,4 +228,10 @@ stats_cmd = ui.Subcommand(
stats_cmd.parser.add_option(
"-e", "--exact", action="store_true", help="exact size and time"
)
stats_cmd.parser.add_option(
"-o",
"--overview",
action="store_true",
help="show overview-style comprehensive library report",
)
stats_cmd.func = stats_func

View file

@ -1,220 +0,0 @@
# This file is part of beets.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Report plugin for Beets: generate statistical summaries of your music library."""
import datetime
from collections import Counter
from beets.plugins import BeetsPlugin
from beets.ui import Subcommand, print_
class ReportPlugin(BeetsPlugin):
"""A Beets plugin that generates a library report with Wrapped-style insights."""
def commands(self):
report_cmd = Subcommand(
"report",
help="Generate a statistical report of your music library.",
)
report_cmd.func = self._run_report
return [report_cmd]
def _run_report(self, lib, opts, args):
"""Run the plugin: generate summary and print report."""
summary = self.generate_summary(lib)
self.print_report(summary)
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."""
if summary["total_tracks"] == 0:
print_("Your Beets library is empty.")
return
def fmt_time(seconds):
return str(datetime.timedelta(seconds=int(seconds)))
def decade_label(d):
return f"{str(d)[-2:]}s"
# --- 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")
print_(f"Generated: {datetime.datetime.now():%Y-%m-%d %H:%M:%S}")
print_("=" * 60)
# --- Overview ---
print_("Overview")
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())
if years:
print_(f" Years: {min(years)} {max(years)}")
else:
print_(" Years: n/a")
print_("-" * 60)
# --- Duration & quality ---
print_("Listening time & quality")
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 "
f"({summary['quality']})"
)
if summary["formats"]:
print_(
f" Primary format: "
f"{summary['formats'].most_common(1)[0][0]}"
)
print_("-" * 60)
# --- Decade distribution ---
print_("Favorite musical decades")
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}): "
f"{c:>5} tracks ({pct:4.1f}%)"
)
else:
print_(" n/a")
print_("-" * 60)
# --- Wrapped summary ---
print_("Your Music Wrapped")
if top_artist:
print_(
f" Top artist: {top_artist[0][0]} "
f"({top_artist[0][1]} tracks)"
)
if top_genre:
print_(
f" Top genre: {top_genre[0][0]} ({top_genre[0][1]} tracks)"
)
if top_decade:
d, c = top_decade[0]
print_(
f" Top decade: {decade_label(d)} ({d}-{d + 9}, {c} tracks)"
)
if top_year:
y, c = top_year[0]
print_(f" Top year: {y} ({c} tracks)")
if summary["longest_track"]:
lt = summary["longest_track"]
print_(
f" Longest track: {lt.artist} {lt.title} "
f"({fmt_time(lt.length)})"
)
if summary["shortest_track"]:
st = summary["shortest_track"]
print_(
f" Shortest track: {st.artist} {st.title} "
f"({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: {summary['missing_genre']}")
print_(f" Missing year tags: {summary['missing_year']}")

View file

@ -12,10 +12,11 @@ been dropped.
New features:
- :doc:`plugins/report`: Added `report` plugin to generate a statistical summary
of your music library, including tracks, albums, artists, genres, years,
listening time, audio quality, decade distribution, top artist/genre/year,
longest/shortest tracks, and counts of missing metadata.
- :doc:`beets/ui/commands/stats`: Extended the ``stats`` command with an
overview-style report (``--overview``) that generates a detailed statistical
summary of the music library, including tracks, albums, artists, genres,
years, listening time, audio quality, decade distribution, top
artist/genre/year, longest/shortest tracks, and counts of missing metadata.
- :doc:`plugins/fetchart`: Added config setting for a fallback cover art image.
- :doc:`plugins/ftintitle`: Added argument for custom feat. words in ftintitle.
- :doc:`plugins/ftintitle`: Added album template value ``album_artist_no_feat``.

View file

@ -117,12 +117,12 @@ databases. They share the following configuration options:
random
replace
replaygain
report
rewrite
scrub
smartplaylist
sonosupdate
spotify
stats
subsonicplaylist
subsonicupdate
substitute

View file

@ -1,62 +0,0 @@
Report Plugin
=============
The ``report`` plugin provides a command that generates a detailed statistical
summary of your music library. It collects information about tracks, albums,
artists, genres, years, formats, and more, giving you insights similar to a
“Wrapped” summary of your listening habits.
First, enable the plugin named ``report`` (see :ref:`using-plugins`). You'll
then be able to use the ``beet report`` command:
::
$ beet report
Beets Library Report
Generated: 2026-01-05 12:34:56
============================================================
Overview
Tracks: 124
Albums: 30
Artists: 20
Genres: 12
Years: 1998 2022
------------------------------------------------------------
Listening time & quality
Total playtime: 12:34:56
Avg track length: 00:06:07
Avg bitrate: 320 kbps (High quality)
Primary format: mp3
------------------------------------------------------------
Favorite musical decades
90s (1990-1999): 35 tracks (28.2%)
00s (2000-2009): 40 tracks (32.3%)
10s (2010-2019): 49 tracks (39.5%)
------------------------------------------------------------
Your Music Wrapped
Top artist: Radiohead (15 tracks)
Top genre: Alternative (28 tracks)
Top decade: 10s (2010-2019, 49 tracks)
Top year: 2017 (12 tracks)
Longest track: Pink Floyd Echoes (23:31)
Shortest track: Daft Punk Nightvision (01:12)
New music (2015+): 60
Older music: 64
Missing genre tags: 3
Missing year tags: 2
The command takes no additional arguments. It scans your library and prints
statistics such as:
- Total number of tracks, albums, artists, and genres
- Range of years present in the library
- Total listening time and average track length
- Average bitrate and primary file format
- Distribution of tracks by decade
- Most common artist, genre, decade, and year
- Longest and shortest tracks
- Counts of new vs. older music (tracks since 2015)
- Number of tracks missing genre or year tags
This plugin is useful for analyzing your collection, identifying missing
metadata, and discovering trends in your listening habits.

107
docs/plugins/stats.rst Normal file
View file

@ -0,0 +1,107 @@
Stats Plugin
============
The ``stats`` plugin provides commands for displaying statistics about your
music library, such as the total number of tracks, artists, albums, and the
overall size and duration of your collection.
Basic Statistics
----------------
By default, the ``stats`` command prints a concise summary of the library or a
query result:
::
$ beet stats
This includes:
- Total number of matched tracks
- Total listening time
- Approximate total size of audio files
- Number of artists, albums, and album artists
Exact Mode
----------
The ``-e`` / ``--exact`` flag enables *exact* size and duration calculations:
::
$ beet stats --exact
When this flag is used, the command:
- Computes file sizes directly from the filesystem instead of estimating them
from bitrate and duration
- Prints both human-readable values and raw byte/second counts
- May be slower on large libraries, as it requires accessing every audio file
This mode is useful when precise storage or duration figures are required.
Overview Report
---------------
In addition to the standard output, the ``stats`` command supports an
*overview-style* report that generates a detailed, human-readable summary of
your library.
To generate this report, run:
::
$ beet stats --overview
This prints a comprehensive report including general library statistics,
listening time, audio quality information, decade distribution, and summary
highlights.
Example output:
::
Beets Library Report
Generated: 2026-01-05 12:34:56
============================================================
Overview
Tracks: 124
Albums: 30
Artists: 20
Genres: 12
Years: 1998 2022
------------------------------------------------------------
Listening time & quality
Total playtime: 12:34:56
Avg track length: 00:06:07
Avg bitrate: 320 kbps (High quality)
Primary format: MP3
------------------------------------------------------------
Favorite musical decades
90s (1990-1999): 35 tracks (28.2%)
00s (2000-2009): 40 tracks (32.3%)
10s (2010-2019): 49 tracks (39.5%)
------------------------------------------------------------
Your Music Wrapped
Top artist: Radiohead (15 tracks)
Top genre: Alternative (28 tracks)
Top decade: 10s (2010-2019, 49 tracks)
Top year: 2017 (12 tracks)
Longest track: Pink Floyd Echoes (23:31)
Shortest track: Daft Punk Nightvision (01:12)
Missing genre tags: 3
Missing year tags: 2
The overview report includes:
- Total number of tracks, albums, artists, and genres
- Range of years present in the library
- Total listening time and average track length
- Average bitrate and primary file format
- Distribution of tracks by decade
- Most common artist, genre, decade, and year
- Longest and shortest tracks
- Counts of tracks missing genre or year metadata
The ``--overview`` flag is mutually exclusive with ``--exact`` and always uses
estimated sizes and durations derived from metadata.

View file

@ -1,204 +0,0 @@
# This file is part of beets.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
import pytest
from beets.library import Item
from beetsplug.report import ReportPlugin
# --- Fixtures ---
@pytest.fixture
def library(tmp_path):
"""Create a temporary empty Beets library."""
from beets.library import Library
lib_path = tmp_path / "beets.db"
lib = Library(str(lib_path))
return lib
def add_item(
lib,
title="Test",
artist="Artist",
album="Album",
genre="Genre",
year=2000,
length=180,
bitrate=320000, # bitrate in bits per second
format="MP3",
):
"""Add a single Item to the test library."""
item = Item(
path=f"/tmp/{title}.mp3",
title=title,
artist=artist,
album=album,
genre=genre,
year=year,
length=length,
bitrate=bitrate,
format=format,
)
lib.add(item)
# --- Tests ---
def test_empty_library(capsys, library):
"""Test empty library: should output message without crashing."""
plugin = ReportPlugin()
plugin._run_report(library, None, [])
captured = capsys.readouterr()
assert "Your Beets library is empty." in captured.out
def test_single_item(capsys, library):
"""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, [])
captured = capsys.readouterr()
# --- Basic statistics ---
assert "Tracks:" in captured.out
assert "Albums:" in captured.out
assert "Artists:" in captured.out
assert "Genres:" in captured.out
# --- Wrapped-style insights ---
assert "Top artist:" in captured.out
assert "Solo Artist" in captured.out
assert "Top genre:" in captured.out
assert "Indie" in captured.out
assert "Top decade:" in captured.out
assert "10s" in captured.out
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
assert ")" 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."""
add_item(library, "Track1", "Artist A", "Album X", "Rock", 1995)
add_item(library, "Track2", "Artist A", "Album X", "Rock", 1995)
add_item(library, "Track3", "Artist B", "Album Y", "Pop", 2002)
add_item(library, "Track4", "Artist C", "Album Z", "Electronic", 2018)
plugin = ReportPlugin()
plugin._run_report(library, None, [])
captured = capsys.readouterr()
# --- Basic stats ---
assert "Tracks:" in captured.out
assert "4" in captured.out
assert "Albums:" in captured.out
assert "3" in captured.out
assert "Artists:" in captured.out
assert "3" in captured.out
assert "Genres:" in captured.out
assert "3" in captured.out
# --- Wrapped-style insights ---
assert "Top artist:" in captured.out
assert "Artist A" in captured.out
assert "Top genre:" in captured.out
assert "Rock" in captured.out
assert "Top decade:" in captured.out
assert "90s" in captured.out
assert "Top year:" in captured.out
assert "1995" in captured.out
# --- Decade distribution ---
assert "90s" in captured.out
assert "00s" in captured.out
assert "10s" in captured.out
def test_missing_metadata(capsys, library):
"""Test library with missing tags, length, and bitrate."""
add_item(
library,
"Track1",
"Artist",
"Album",
None, # missing genre
2000,
length=200,
bitrate=256000,
)
add_item(
library,
"Track2",
"Artist",
"Album",
"Rock",
None, # missing year
length=180,
bitrate=None,
)
plugin = ReportPlugin()
plugin._run_report(library, None, [])
captured = capsys.readouterr()
# --- Check missing metadata counts ---
lines = captured.out.splitlines()
# Check for missing genre
genre_found = False
for line in lines:
if "Missing genre tags:" in line:
assert "1" in line
genre_found = True
break
assert genre_found
# Check for missing year
year_found = False
for line in lines:
if "Missing year tags:" in line:
assert "1" in line
year_found = True
break
assert year_found

203
test/test_stats_overview.py Normal file
View file

@ -0,0 +1,203 @@
# This file is part of beets.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
import pytest
from beets.library import Item
from beets.ui.commands.stats import show_overview_report
# --- Fixtures ---
@pytest.fixture
def library(tmp_path):
"""Create a temporary empty Beets library."""
from beets.library import Library
lib_path = tmp_path / "beets.db"
lib = Library(str(lib_path))
return lib
def add_item(
lib,
title="Test",
artist="Artist",
album="Album",
genre="Genre",
year=2000,
length=180,
bitrate=320000,
format="MP3",
):
"""Add a single Item to the test library."""
item = Item(
path=f"/tmp/{title}.mp3",
title=title,
artist=artist,
album=album,
genre=genre,
year=year,
length=length,
bitrate=bitrate,
format=format,
)
lib.add(item)
# --- Tests ---
def test_empty_library_overview(capsys, library):
"""Test empty library with overview report."""
from beets.ui import _raw_main
# Simulate: beet stats --overview
_raw_main(["stats", "--overview"], library)
captured = capsys.readouterr()
assert "Your Beets library is empty." in captured.out
def test_single_item_overview(capsys, library):
"""Test library with a single track using overview report."""
add_item(
library,
title="Single Track",
artist="Solo Artist",
genre="Indie",
year=2019,
bitrate=256000,
)
from beets.ui import _raw_main
# Simulate: beet stats --overview
_raw_main(["stats", "--overview"], library)
captured = capsys.readouterr()
# --- Check basic statistics ---
# Format is "Tracks: X" (3 spaces after colon)
assert "Tracks: 1" in captured.out
assert "Albums: 1" in captured.out
assert "Artists: 1" in captured.out
assert "Genres: 1" in captured.out
# --- Wrapped-style insights ---
assert "Top artist: Solo Artist (1 tracks)" in captured.out
assert "Top genre: Indie (1 tracks)" in captured.out
# Decade format: "10s (2010-2019): 1 tracks (100.0%)"
assert "10s (2010-2019" in captured.out
assert "1 tracks" in captured.out
# Year format
assert "Top year: 2019 (1 tracks)" in captured.out
def test_multiple_items_overview(capsys, library):
"""Test library with multiple tracks using overview report."""
# 1995 2 tracks Rock
add_item(library, "Track1", "Artist A", "Album X", "Rock", 1995)
add_item(library, "Track2", "Artist A", "Album X", "Rock", 1995)
# 2002 1 track Pop
add_item(library, "Track3", "Artist B", "Album Y", "Pop", 2002)
# 2018 1 track Electronic
add_item(library, "Track4", "Artist C", "Album Z", "Electronic", 2018)
from beets.ui import _raw_main
# Simulate: beet stats --overview
_raw_main(["stats", "--overview"], library)
captured = capsys.readouterr()
# --- Basic stats ---
assert "Tracks: 4" in captured.out
assert "Albums: 3" in captured.out
assert "Artists: 3" in captured.out
assert "Genres: 3" in captured.out
# --- Wrapped insights ---
assert "Top artist: Artist A (2 tracks)" in captured.out
assert "Top genre: Rock (2 tracks)" in captured.out
# Decade format check
assert "90s (1990-1999" in captured.out
assert "2 tracks" in captured.out
# Year format
assert "Top year: 1995 (2 tracks)" in captured.out
# --- Decade distribution ---
assert "90s (1990-1999" in captured.out
assert "00s (2000-2009" in captured.out
assert "10s (2010-2019" in captured.out
def test_missing_metadata_overview(capsys, library):
"""Test library with missing tags using overview report."""
# Missing genre
add_item(
library,
"Track1",
"Artist",
"Album",
None,
2000,
length=200,
bitrate=256000,
)
# Missing year
add_item(
library,
"Track2",
"Artist",
"Album",
"Rock",
None,
length=180,
bitrate=256000,
)
from beets.ui import _raw_main
# Simulate: beet stats --overview
_raw_main(["stats", "--overview"], library)
captured = capsys.readouterr()
# Format has 2 spaces after colon for year tags
assert "Missing genre tags: 1" in captured.out
assert "Missing year tags: 1" in captured.out
def test_various_lengths_and_bitrates_overview(capsys, library):
"""Test track lengths and bitrate classification."""
add_item(library, "Short", "A", "X", "Pop", 2010, length=60, bitrate=128000)
add_item(
library, "Long", "B", "Y", "Rock", 2015, length=3600, bitrate=1024000
)
from beets.ui import _raw_main
# Simulate: beet stats --overview
_raw_main(["stats", "--overview"], library)
captured = capsys.readouterr()
# --- Check durations ---
assert "Total playtime:" in captured.out
assert "Avg track length:" in captured.out
# --- Check bitrate and quality classification ---
assert "Avg bitrate:" in captured.out
assert "kbps" in captured.out