mirror of
https://github.com/beetbox/beets.git
synced 2026-02-09 00:41:57 +01:00
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:
parent
91d827fc91
commit
8aa96e8f2e
8 changed files with 492 additions and 492 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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']}")
|
||||
|
|
@ -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``.
|
||||
|
|
|
|||
|
|
@ -117,12 +117,12 @@ databases. They share the following configuration options:
|
|||
random
|
||||
replace
|
||||
replaygain
|
||||
report
|
||||
rewrite
|
||||
scrub
|
||||
smartplaylist
|
||||
sonosupdate
|
||||
spotify
|
||||
stats
|
||||
subsonicplaylist
|
||||
subsonicupdate
|
||||
substitute
|
||||
|
|
|
|||
|
|
@ -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
107
docs/plugins/stats.rst
Normal 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.
|
||||
|
|
@ -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
203
test/test_stats_overview.py
Normal 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
|
||||
Loading…
Reference in a new issue