mirror of
https://github.com/beetbox/beets.git
synced 2026-02-09 00:41:57 +01:00
Addreseed reported issues: guard longest track, consolidate metadate, improve test
This commit is contained in:
parent
99a95889c7
commit
edc09b4eab
2 changed files with 157 additions and 88 deletions
|
|
@ -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']}")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue