Skip to content
Open
177 changes: 176 additions & 1 deletion beets/ui/commands/stats.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -49,8 +51,175 @@ def show_stats(lib, query, exact):
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(
Expand All @@ -59,4 +228,10 @@ def stats_func(lib, opts, args):
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
5 changes: 5 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ been dropped.

New features:

- :doc:`plugins/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``.
Expand Down
1 change: 1 addition & 0 deletions docs/plugins/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ databases. They share the following configuration options:
smartplaylist
sonosupdate
spotify
stats
subsonicplaylist
subsonicupdate
substitute
Expand Down
107 changes: 107 additions & 0 deletions docs/plugins/stats.rst
Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading