Capstone — Library Dashboard
Every function in this section built one piece of Tunebox's library: the scale
planner, the playlist, the session finder, the song index. Now they run together.
The home screen shows a library dashboard — how big the library is, what's been
played, who you listen to most, and the longest session that fits your free time —
and one function, library_dashboard(), computes it on every refresh.
The catch: a real library is messy and a real play log is long. The dashboard must
stay O(t + p) for t tracks and p plays — a pass or two over each, no loop inside a
loop, no in on a list, no pop(0).
What you're building
library_dashboard(tracks, plays, budget)tracks— the library, a list of dicts like{"id": "t1", "title": "Glow", "artist": "Nova", "mins": 4}. A messy import can list the same id twice; the first copy is the real track.plays— the play log: track ids in the order they were played. Ids that aren't in the library (deleted tracks) are ignored.budget— a number of minutes.
The rules
Work in this order — each step feeds the next:
- Index the library.
id → track, keeping the first copy of each id. - Clean the log. Keep only plays whose id is in the index, in their original order.
- Count. Plays per track, and minutes played per artist.
- Pick the leaders.
top_trackis the title with the most plays — ties go to the track that comes first in the library.top_artisthas the most minutes played — ties go alphabetically. Both areNonewhen there are no valid plays. - Find the session. The longest run of consecutive valid plays whose minutes
add up to at most
budget, as a list of titles. Ties go to the earliest run; if no single play fits, it's[].
The exact shape to return
{
"tracks": 5, # unique ids in the library
"plays": 8, # valid plays
"minutes": 30, # total minutes of valid plays
"top_track": "Tide", # most plays (ties: library order)
"top_artist": "Nova", # most minutes (ties: alphabetical)
"session": ["Glow", "Tide", "Tide"], # longest run within budget, as titles
}Which piece does which job
| step | technique | from | cost |
|---|---|---|---|
| index, first copy wins | hash table with a not in check |
Hash tables | O(t) |
| clean the log | O(1) membership per play | Hash tables | O(p) |
| count plays and minutes | counting with dict.get |
Hash tables | O(p) |
| top track, library order | one walk over the index — dicts keep insertion order | Hash tables | O(t) |
| longest session | variable-size sliding window over the log | Two pointers | O(p) |
| session titles | one slice of the clean log | Arrays | O(p) |
Add them up: O(t + p). Each step is short; the bugs live in the details. Here they are one at a time.
Trap 1 — in on the wrong structure
Cleaning the log asks "is this id in the library?" once per play. Here's what that costs when the ids sit in a list:
library_ids = ["t1", "t2", "t3", "t4"]
plays = ["t2", "t9", "t4", "t2"]
checks = 0
for tid in plays:
for known in library_ids: # what `tid in library_ids` really does
checks += 1
if known == tid:
break
print(checks)Trap 2 — the duplicate id
A dict comprehension is the natural way to build an index, and it gets the duplicate rule backwards:
The first copy of a repeated id is the real track, so this should print Tide. It prints the duplicate instead.
Trap 3 — ties
max() and min() return the first best item they meet, so the order you walk in
is your tie-break rule:
counts = {"t3": 2, "t1": 2, "t2": 1}
library_order = ["t1", "t2", "t3"]
print(max(counts, key=counts.get), max(library_order, key=counts.get))So for top_track, walk the index — it's in library order — and replace the leader
only on strictly more plays:
"Most minutes, ties alphabetical" needs two rules at once. A tuple key does it: the
smallest (-minutes, name) has the most minutes, then the earliest name.
Trap 4 — the window runs over the clean log
The session is a window over the cleaned log, measured in minutes: a deleted track
has no minutes to count, and looking its id up in the index would raise KeyError.
Step through the window on the minutes of a clean log:
A strict > keeps the earliest of two equally long runs; >= would keep the latest.
Once you have best, the titles are one slice away: log[best[0]:best[1]].
Build it in pieces
Steps 1 and 2 on the sample library — run it, then carry on from here in the editor:
🎯 Your turn
Write library_dashboard(tracks, plays, budget) and return all six keys. Follow the
rules in order: index (first copy wins) → clean log → counts → leaders → session.
With the sample library and play log above, library_dashboard(tracks, plays, 10) →
{"tracks": 5, "plays": 8, "minutes": 30, "top_track": "Tide", "top_artist": "Nova", "session": ["Glow", "Tide", "Tide"]}
Hint — reuse the pieces you already built: the index loop from Trap 2, the tuple
key from Trap 3, and session_within's window from the two-pointers lesson.
Then press ▶ Run, tap the Live App try chips to call it with different inputs, and hit ✓ Check. All green = Tunebox's library is complete. 🎉
