Spaces:
Running
Running
| """The Tracks list, the resolved-match card, and the batch summary. | |
| Kept out of `render.py` so the report renderer stays one thing. Everything | |
| here borrows `render.CSS` rather than defining a second visual language, so | |
| a saved row and a market card are recognisably the same product. | |
| """ | |
| from __future__ import annotations | |
| import datetime as dt | |
| import livematch | |
| import render | |
| import worldmap | |
| ESC = render.ESC | |
| def _when(iso: str | None) -> str: | |
| """"3 days ago" reads faster than a timestamp on a list someone scans. | |
| The exact date follows in the same line for anyone who wants it.""" | |
| if not iso: | |
| return "" | |
| try: | |
| t = dt.datetime.fromisoformat(iso.replace("Z", "+00:00")) | |
| except ValueError: | |
| return ESC(iso) | |
| # Everything this app writes carries a zone. Anything hand-written into | |
| # the index, or written by an older build, may not — and a whole list | |
| # that fails to draw because one row is missing a "+00:00" is not a | |
| # trade worth making. | |
| if t.tzinfo is None: | |
| t = t.replace(tzinfo=dt.timezone.utc) | |
| days = (dt.datetime.now(dt.timezone.utc) - t).days | |
| when = ("today" if days <= 0 else "yesterday" if days == 1 | |
| else f"{days} days ago") | |
| return f"{when} · {t:%-d %b}" | |
| week_span = render.week_span | |
| # "23 Aug". One author of the day a reader sees, in `render`, so the list, | |
| # the banner, the step bar and the report's own kicker cannot drift apart. | |
| _daystamp = render.day_stamp | |
| def _state(m: dict) -> str: | |
| """The one word this market is in, for a saved row. | |
| A meta written before "self" was a state carries `band: None` on a | |
| self-match, so the number decides when the word is missing. Everything | |
| else falls to flat, which is the sheet's word for "no ranking to read". | |
| """ | |
| band = m.get("band") | |
| if band in ("place", "derive", "pass", "self"): | |
| return band | |
| if float(m.get("best") or 0.0) >= worldmap.SAME_RECORDING: | |
| return "self" | |
| return "flat" | |
| def _band_class(band: str | None) -> str: | |
| return band if band in ("place", "derive", "pass", "self") else "flat" | |
| def _markets_strip(top: list[dict], flat: bool = False) -> str: | |
| """The top two or three markets, each in its own verdict colour. | |
| This is what makes the list answer "which track goes where" before | |
| anything is opened. Colouring only the leading market — the first | |
| version — meant a track placing in Kenya and passing everywhere else | |
| looked identical to one placing in all three. | |
| A flat reading gets no colours and a line saying why. The report for | |
| that track opens "No market stands out" and refuses to rank; a row | |
| printing three coloured percentages under a MAKE chip said the opposite | |
| about the same track on the same day (23 Aug audit). | |
| """ | |
| if not top: | |
| return "" | |
| if flat: | |
| names = [ESC(worldmap.market_name(m.get("iso", ""), m.get("name"))) | |
| for m in top[:3]] | |
| listed = (names[0] if len(names) == 1 | |
| else " and ".join([", ".join(names[:-1]), names[-1]]) | |
| if len(names) > 2 else " and ".join(names)) | |
| return (f'<p class="den" style="margin:10px 0 0">Reads much the same ' | |
| f'in every market measured, so there is no ranking to read. ' | |
| f'{listed} came top by a margin too small to act on.</p>') | |
| cells = [] | |
| for m in top[:3]: | |
| b = _state(m) | |
| colour = f"var(--{b})" | |
| # A self-match prints no percentage anywhere, here included: the | |
| # number is the record recognising itself, and 100% next to a market | |
| # name reads as the best market on the list. | |
| value = render.CHIP["self"] if b == "self" \ | |
| else livematch.pct(m.get("best", 0)) | |
| cells.append( | |
| f'<span style="display:inline-flex;align-items:baseline;gap:5px;' | |
| f'white-space:nowrap">' | |
| f'<b style="font-weight:800;font-size:15px;color:{colour};' | |
| f'font-variant-numeric:tabular-nums">{value}</b>' | |
| f'<span style="font-size:13px">' | |
| f'{ESC(worldmap.market_name(m.get("iso", ""), m.get("name")))}' | |
| f'</span></span>') | |
| return ('<div style="display:flex;flex-wrap:wrap;gap:12px;' | |
| 'margin:10px 0 0">' + "".join(cells) + "</div>") | |
| def _sort_key(r: dict) -> tuple: | |
| """Artist, then newest first inside an artist. | |
| A label's list is usually several tracks by one or two acts, so grouping | |
| by artist puts the comparison the reader is actually making — how do this | |
| artist's records sit against each other — next to each other with no | |
| filter to operate. Tracks with no artist sort last rather than first, so | |
| a stray upload does not head the list. | |
| """ | |
| artist = (r.get("artist") or "").strip() | |
| return (artist == "", artist.lower(), _neg_time(r.get("saved_at"))) | |
| def _neg_time(iso: str | None) -> str: | |
| """Descending sort on an ISO timestamp without parsing it: invert each | |
| digit. Newest first inside a group, with no date arithmetic.""" | |
| s = iso or "" | |
| return "".join(chr(ord("9") - (ord(c) - ord("0"))) if c.isdigit() else c | |
| for c in s) | |
| def header(rows: list[dict], corpus_week: str) -> str: | |
| """The Tracks list's opening block: how many tracks, and whether they are | |
| all read against the week the app has loaded. | |
| Carries the stylesheet for the whole tab. The cards below are separate | |
| HTML blocks so each can have its own button, and one copy of the CSS in | |
| the page serves all of them. | |
| """ | |
| out = [render.CSS, '<div class="ml">'] | |
| if not rows: | |
| out.append( | |
| '<div class="card flat"><p class="why"><b>Nothing saved yet.' | |
| '</b> Every track you analyse is kept here automatically, with ' | |
| 'its fingerprint. When next Monday\'s charts land you can ' | |
| 're-read the whole list against them without finding the files ' | |
| 'again.</p></div></div>') | |
| return "".join(out) | |
| stale = sum(1 for r in rows if str(r.get("corpus_week") or "") | |
| != str(corpus_week)) | |
| out.append(f'<div class="kicker">Tracks</div>' | |
| f'<h1 class="hdr">{len(rows)} ' | |
| f'{"track" if len(rows) == 1 else "tracks"}</h1>') | |
| if stale: | |
| out.append( | |
| f'<p class="sub">{stale} of them were last read against an ' | |
| f'earlier week. <b>Re-score all</b> brings the list up to the ' | |
| f'latest charts ({ESC(week_span(str(corpus_week)))}).</p>') | |
| else: | |
| out.append(f'<p class="sub">All read against the latest charts ' | |
| f'({ESC(week_span(str(corpus_week)))}).</p>') | |
| out.append('<p class="foot" style="margin:12px 0 0">Tap a track to open ' | |
| 'its full report.</p>') | |
| out.append("</div>") | |
| return "".join(out) | |
| def unreachable() -> str: | |
| """The Tracks tab when the archive cannot be read. | |
| Not the empty state. "Nothing saved yet" told a team whose forty | |
| analyses were behind a bad minute that they had never saved one — and | |
| the first thing anyone does about that is analyse everything again (23 | |
| Aug audit). Nothing here says what went wrong, because the reader cannot | |
| act on that; it says what is true and what to do. | |
| """ | |
| return (render.CSS + '<div class="ml"><div class="card flat">' | |
| '<p class="why"><b>The saved list could not be read just now.' | |
| '</b> This is the list not loading — nothing has been lost, and ' | |
| 'nothing has been deleted. Try again in a minute.</p>' | |
| '</div></div>') | |
| def card(row: dict, corpus_week: str, artist_heading: str | None = None, | |
| artist_count: int = 1) -> str: | |
| """One saved track, as the thing you tap. | |
| The picker at the foot of the tab was the only way into a saved report, | |
| and nobody found it — the cards looked like a read-only summary. The card | |
| is now the control: it sits under a transparent button covering the whole | |
| of it, and it says so, with the chevron every list on a phone uses to | |
| mean "this opens". | |
| Everything drawn here comes off the index, so a list of fifty costs one | |
| small download rather than fifty. | |
| """ | |
| out = ['<div class="ml">'] | |
| if artist_heading is not None: | |
| out.append(f'<h2 style="margin-top:0"><span>' | |
| f'{ESC(artist_heading) if artist_heading else "Other tracks"}' | |
| f'</span>' | |
| + (f'<span class="tag">{artist_count} tracks</span>' | |
| if artist_count > 1 else "") | |
| + '</h2>') | |
| top = row.get("top_markets") or [] | |
| lead = top[0] if top else {} | |
| # Flatness is a fact about the whole field, written down at save time — | |
| # `store._flat_field`. It outranks the leading market's own band, because | |
| # a field with no ranking has no leading market. A row saved before the | |
| # key existed carries no `flat` and reads as it did before. | |
| flat = bool(row.get("flat")) | |
| cls = "flat" if flat else _state(lead) | |
| artist = (row.get("artist") or "").strip() | |
| row_stale = str(row.get("corpus_week") or "") != str(corpus_week) | |
| # The title alone under an artist heading; the full label when there is | |
| # no artist to head it. | |
| name = (row.get("title") or row.get("label") or "untitled") if artist \ | |
| else (row.get("label") or "untitled") | |
| out.append(f'<div class="card tap {cls}">') | |
| out.append(f'<div class="mkt"><div class="name">{ESC(name)}</div>') | |
| if top: | |
| out.append(render.chip(cls)) | |
| out.append('<span class="chev">›</span>') | |
| out.append("</div>") | |
| out.append(_markets_strip(top, flat=flat)) | |
| meta_bits = [_when(row.get("saved_at"))] | |
| if row.get("source") == "lookup": | |
| meta_bits.append("30s preview, found by name") | |
| elif row.get("duration_s"): | |
| # "full track, 0 min" for anything under half a minute, and a | |
| # 20-second sketch is a real thing to drop in (23 Aug audit). | |
| secs = float(row["duration_s"]) | |
| meta_bits.append("full track, under a minute" if secs < 60 | |
| else f'full track, {secs / 60:.0f} min') | |
| week_bit = (f'charts of ' | |
| f'{ESC(week_span(str(row.get("corpus_week") or "?")))}') | |
| meta_bits.append(week_bit + (" — earlier than this one" if row_stale | |
| else "")) | |
| if int(row.get("runs", 1)) > 1: | |
| meta_bits.append(f'read {row["runs"]} times') | |
| out.append(f'<p class="den" style="margin:10px 0 0">' | |
| f'{" · ".join(b for b in meta_bits if b)}</p>') | |
| out.append("</div></div>") | |
| return "".join(out) | |
| def cards(rows: list[dict], corpus_week: str) -> list[tuple[dict, str]]: | |
| """(row, html) in the order the list draws them, with an artist heading | |
| on the first card of each artist's run. | |
| The Tracks tab builds one button per entry from this, so the order the | |
| buttons are created in and the order the cards read in are the same list. | |
| """ | |
| out = [] | |
| current = object() | |
| for r in sorted(rows, key=_sort_key): | |
| artist = (r.get("artist") or "").strip() | |
| heading, n = None, 1 | |
| if artist != current: | |
| current = artist | |
| heading = artist | |
| n = sum(1 for x in rows | |
| if (x.get("artist") or "").strip() == artist) | |
| out.append((r, card(r, corpus_week, artist_heading=heading, | |
| artist_count=n))) | |
| return out | |
| def saved_list(rows: list[dict], corpus_week: str) -> str: | |
| """The whole list as one block of HTML — the header and every card. | |
| One renderer, two callers: this is what a test and any non-interactive | |
| read of the list see, while the tab itself lays the same cards out one | |
| per button. | |
| Every card carries the same four things in the same places — name, the | |
| verdict chip, the top three markets with their own colours, and the | |
| provenance line — so two cards read against each other down a phone | |
| screen without a compare mode. That is the cheap 80% of comparison; | |
| what a real compare view would add is noted in design/ONE_APP.md §5. | |
| Everything drawn here comes off the index, so a list of fifty costs one | |
| small download rather than fifty. | |
| """ | |
| out = [header(rows, corpus_week)] | |
| for _row, html in cards(rows, corpus_week): | |
| out.append(html) | |
| return "".join(out) | |
| def match_card(match: dict, preview_url: str | None = None) -> str: | |
| """What the name lookup resolved, shown BEFORE any verdict. | |
| Deezer's search is fuzzy and its ranking put "Sugarcane (Remix)" above | |
| "Sugarcane" on a search for Sugarcane. A wrong match produces a perfectly | |
| confident analysis of the wrong record, so the reader sees what was found | |
| and can hear it before reading a single number. | |
| """ | |
| src = {"deezer": "Deezer", "itunes": "Apple Music"}.get( | |
| match.get("source", ""), match.get("source", "")) | |
| out = [render.CSS, '<div class="ml"><div class="card flat">', | |
| '<div class="lab">FOUND</div>', | |
| f'<div class="mkt"><div class="name">' | |
| f'{ESC(match.get("artist", ""))} — ' | |
| f'{ESC(match.get("title", ""))}</div></div>'] | |
| bits = [f'official 30-second preview from {ESC(src)}'] | |
| if match.get("album"): | |
| bits.append(ESC(match["album"])) | |
| out.append(f'<p class="den">{" · ".join(bits)}</p>') | |
| if preview_url: | |
| out.append(f'<div class="player"><span class="plab">PREVIEW</span>' | |
| f'<audio controls preload="none" src="{ESC(preview_url)}">' | |
| f'</audio></div>') | |
| alts = match.get("alternatives") or [] | |
| if alts: | |
| names = "; ".join(f'{ESC(a.get("artist", ""))} — ' | |
| f'{ESC(a.get("title", ""))}' for a in alts) | |
| out.append(f'<p class="why">Other versions with the same name: ' | |
| f'{names}. If one of those is the record you meant, type ' | |
| f'its full title and search again.</p>') | |
| out.append( | |
| '<p class="why"><b>This reading is like-for-like.</b> Every sound ' | |
| 'the track is compared against is measured from a 30-second preview ' | |
| 'as well, so the percentages here are better calibrated than they ' | |
| 'are for a full-length upload.</p>') | |
| out.append('</div></div>') | |
| return "".join(out) | |
| PREVIEW_NO_SNIPPETS = ( | |
| '<div class="note"><div class="lab">NO CLIP SHORTLIST</div>' | |
| '<p class="why">A preview is 30 seconds, which is one window, so there ' | |
| 'is nothing to choose between. Upload the full track to get the clip ' | |
| 'shortlist and the section map.</p></div>') | |
| def _names_said(names: list[str]) -> str: | |
| """"Kenya", "Kenya and Ghana", "Kenya, Ghana and Peru".""" | |
| if len(names) <= 1: | |
| return names[0] if names else "" | |
| return ", ".join(names[:-1]) + f" and {names[-1]}" | |
| def _answer_since(was: dict, now: dict, previous: dict, current: dict) -> str: | |
| """Whether this reading's answer is the one before it. | |
| "Held" and "moved" are the only two words here. A track reading Nigeria | |
| one week and Argentina the next has not risen, fallen or gained momentum: | |
| two readings of one record were taken and they said different things. | |
| That is the whole of the claim, and there is no other one to make from two | |
| readings of a single track. | |
| """ | |
| was_flat, now_flat = bool(previous.get("flat")), bool(current.get("flat")) | |
| if was_flat and now_flat: | |
| return ("the field was flat in the last chart read, and it is flat " | |
| "in this one.") | |
| if was_flat: | |
| return (f"the field was flat in the last chart read; this one ranks " | |
| f"{now['lead']}.") | |
| if now_flat: | |
| return f"the answer was {was['lead']}; this reading's field is flat." | |
| if was["text"] == now["text"]: | |
| return f"the answer held — {now['text']}." | |
| return f"the answer moved — was {was['text']}, now {now['text']}." | |
| def _markets_since(previous: dict, current: dict) -> str: | |
| """Which markets joined the top of the ranking and which dropped out of | |
| it. | |
| Both readings wrote down the three markets their row prints, and those | |
| three are what is compared — so the sentence says "the top markets" and | |
| not "the ranked list", which would claim a market had left the report | |
| altogether. A reading missing that list says nothing here. | |
| A flat reading on either side says nothing here either. Flatness is a | |
| refusal to rank, and a market cannot enter or leave an order that the | |
| report on the same page says does not exist — the same rule the saved row | |
| and the batch summary already follow. | |
| """ | |
| if previous.get("flat") or current.get("flat"): | |
| return "" | |
| was = list(previous.get("top_markets") or []) | |
| now = list(current.get("top_markets") or []) | |
| if not was or not now: | |
| return "" | |
| def named(m: dict) -> str: | |
| return ESC(worldmap.market_name(m.get("iso", ""), m.get("name"))) | |
| old_iso = {str(m.get("iso") or "") for m in was} | |
| new_iso = {str(m.get("iso") or "") for m in now} | |
| entered = [named(m) for m in now if str(m.get("iso") or "") not in old_iso] | |
| left = [named(m) for m in was if str(m.get("iso") or "") not in new_iso] | |
| if entered and left: | |
| return (f"{_names_said(entered)} entered the top markets; " | |
| f"{_names_said(left)} left them.") | |
| if entered: | |
| return f"{_names_said(entered)} entered the top markets." | |
| if left: | |
| return f"{_names_said(left)} left the top markets." | |
| return "" | |
| def _windows_since(previous: dict, current: dict) -> str: | |
| """How much of the clip shortlist this reading picked for the first time. | |
| Counted by window start, which is what identifies a window. Either | |
| reading missing its windows says nothing — a reading whose file could not | |
| be opened has no shortlist anybody has read, and a track too short to hold | |
| a 30-second window has none to compare. | |
| """ | |
| was = [round(float(w), 2) for w in (previous.get("windows") or [])] | |
| now = [round(float(w), 2) for w in (current.get("windows") or [])] | |
| if not was or not now: | |
| return "" | |
| fresh = [w for w in now if w not in set(was)] | |
| n, total = len(fresh), len(now) | |
| if not n: | |
| return (f"The same {total} snippet window" | |
| f"{' is' if total == 1 else 's are'} picked.") | |
| if n == total: | |
| return ("The one snippet window is new." if total == 1 | |
| else f"All {total} snippet windows are new.") | |
| return f"{n} of {total} snippet windows {'is' if n == 1 else 'are'} new." | |
| def since_last(current: dict, previous: dict | None) -> str: | |
| """What changed between the previous chart read and this one. | |
| One short paragraph of plain HTML with no wrapper around it: the app puts | |
| it in the SAVED READING banner and the share page draws its own note, and | |
| both get the same sentences from here. | |
| "Chart read" is the name the whole page uses — the line over the title | |
| says "chart read 25 Aug" — and it is the thing being compared. Two | |
| readings taken against one evening's charts are one chart read, and | |
| `store.prior` hands in the reading before that instead of the button | |
| press before this. | |
| `current` and `previous` are what `store.summarise` returns — | |
| `top_markets`, `flat`, `windows` — with the kept reading's entry folded | |
| into `previous`, which is where its date comes from. Every clause is | |
| printed only when the facts behind it are on both sides: a previous | |
| reading whose file would not open has no windows and possibly no answer, | |
| and what cannot be read is not written about. No earlier chart read at | |
| all is silence, not a line saying there is nothing to compare. | |
| """ | |
| if not previous: | |
| return "" | |
| was, now = verdict(previous), verdict(current) | |
| said = [] | |
| if was and now: | |
| said.append(_answer_since(was, now, previous, current)) | |
| said.append(_markets_since(previous, current)) | |
| said.append(_windows_since(previous, current)) | |
| body = " ".join(s for s in said if s) | |
| if not body: | |
| return "" | |
| day = _daystamp(previous.get("stamped_at")) | |
| head = (f"Since the last chart read (read {ESC(day)})" if day | |
| else "Since the last chart read") | |
| return f'<p class="why"><b>{head}:</b> {body}</p>' | |
| def since_note(line: str) -> str: | |
| """The same line on a page that has no SAVED READING banner to put it in — | |
| the share page. Nothing at all when there is nothing to say. | |
| No uppercase label over it. The sentence opens with "Since the last | |
| chart read" in bold, and a SINCE THE LAST CHART READ kicker above it | |
| printed the same words twice in two type sizes. The banner in the app | |
| keeps its own label because that one says something else — how many | |
| readings are kept. | |
| """ | |
| if not line: | |
| return "" | |
| return f'<div class="note">{line}</div>' | |
| def reopened_note(row: dict, corpus_week: str, since: str = "") -> str: | |
| """Sits above a reopened report. Its numbers may be from an older week, | |
| and a reader who assumes otherwise would compare two different worlds. | |
| When the track has been read before, the line says how many earlier | |
| readings are kept — the way into them is below the report, because the | |
| reader came here to read this week's answer first. | |
| And it says where they are. Below the report is five screens down on a | |
| desktop and eight on a phone (measured on staging, 25 Aug), so a count | |
| with no direction is a count of something the reader has no reason to | |
| believe is on the page at all. | |
| `since` is `since_last`'s paragraph, and it goes here because this is the | |
| one block on the page that is already about the track's history. A caller | |
| with no earlier reading to compare against passes nothing and the banner | |
| is what it was. | |
| """ | |
| week = str(row.get("corpus_week") or "?") | |
| if week == str(corpus_week): | |
| # No week named here. This track is read against the charts the app | |
| # has loaded, which is the whole of what the reader needs to know; | |
| # the span in brackets was a second date beside a read-date and read | |
| # as a contradiction of it. Which charts those are is on the | |
| # provenance line at the foot of the report. | |
| body = (f'Saved {_when(row.get("saved_at"))}, read against the ' | |
| 'charts loaded now.') | |
| else: | |
| body = (f'Saved {_when(row.get("saved_at"))}, read against the ' | |
| f'charts of {ESC(week_span(week))}. The charts loaded ' | |
| f'now cover {ESC(week_span(str(corpus_week)))}. ' | |
| f'<b>Re-run</b> to read it against them.') | |
| n = len(row.get("readings") or []) | |
| lab = "SAVED READING" | |
| if n: | |
| lab += f' · {n} earlier reading{"" if n == 1 else "s"}' | |
| body += (f' {n} earlier reading{"" if n == 1 else "s"} of it ' | |
| f'{"is" if n == 1 else "are"} kept, at the foot of this ' | |
| f'report.') | |
| return (render.CSS + '<div class="ml"><div class="note">' | |
| f'<div class="lab">{ESC(lab)}</div>' | |
| f'<p class="why">{body}</p>{since}</div></div>') | |
| def verdict(entry: dict) -> dict | None: | |
| """What one kept reading answered, in the words a row prints. | |
| "Argentina 95% · push" is what tells a reader whether to open a reading, | |
| so it stands on the row itself rather than inside it — beside the days | |
| the group was read, which are what tell one group of readings from the | |
| next. | |
| Returns None when the answer is not known — a reading filed before the | |
| entry carried one, whose file could not be opened. A row with no verdict | |
| says the week and the date and stops there. Nothing here guesses: a flat | |
| field says flat, and everything else is read off the markets that were | |
| written down. | |
| """ | |
| if entry.get("flat"): | |
| return {"band": "flat", "lead": "Field was flat", | |
| "text": "Field was flat · no ranking to read"} | |
| top = entry.get("top_markets") or [] | |
| if not top: | |
| return None | |
| m = top[0] or {} | |
| band = _state(m) | |
| name = ESC(worldmap.market_name(m.get("iso", ""), m.get("name"))) | |
| if band == "self": | |
| # A record meeting its own chart entry prints no percentage, here as | |
| # everywhere else: the number is the record recognising itself. | |
| return {"band": band, "lead": name, | |
| "text": f"{name} · already charting there"} | |
| lead = f"{name} {livematch.pct(m.get('best', 0))}" | |
| return {"band": band, "lead": lead, | |
| "text": f"{lead} · {render.CHIP[band].lower()}"} | |
| def _verdict_bits(said: dict | None) -> str: | |
| """The verdict half of a week row: the answer, then its chip.""" | |
| if not said: | |
| return '<div class="rverd"></div>' | |
| return (f'<div class="rverd">' | |
| f'<span class="rlead {said["band"]}">{said["lead"]}</span>' | |
| f'<span class="rchip {said["band"]}">' | |
| f'{render.CHIP[said["band"]]}</span></div>') | |
| def _days_span(first: str, last: str) -> str: | |
| """"24–25 Aug" from two day stamps, "24 Aug – 2 Sep" across a month end. | |
| The same shape `render.week_span` gives a chart week, so a span of days | |
| reads as one span wherever it appears rather than as two dates that | |
| happen to be next to each other. One day, or the same day twice, is | |
| itself. | |
| """ | |
| if not first or not last or first == last: | |
| return first or last | |
| a, b = first.rsplit(" ", 1), last.rsplit(" ", 1) | |
| if len(a) == 2 and len(b) == 2 and a[1] == b[1]: | |
| return f"{a[0]}–{b[0]} {b[1]}" | |
| return f"{first} – {last}" | |
| def _span(readings: list[dict]) -> str: | |
| """"24–25 Aug" for a group read on more than one day, "4 Aug" for a group | |
| read on one. Oldest first: the rows underneath list newest first, and a | |
| date range still has to run forward to read as a range — the mockup's | |
| newest-first span ("25 Aug – 24 Aug") read as broken dates. Kalam, | |
| 25 Aug.""" | |
| stamped = sorted(str(r.get("stamped_at") or "") for r in readings | |
| if r.get("stamped_at")) | |
| days = [d for d in (_daystamp(s) for s in stamped) if d] | |
| if not days: | |
| return "" | |
| return _days_span(days[0], days[-1]) | |
| def _movement(readings: list[dict], short: bool = False) -> str: | |
| """How many readings this week holds, and whether the answer moved. | |
| Counted from the answers themselves. A week holding a reading whose | |
| answer is not known says how many it holds and nothing more — "the same | |
| answer each time" about readings that were never compared would be the | |
| kind of claim this app does not make. | |
| """ | |
| n = len(readings) | |
| if n == 1: | |
| return "1 reading" | |
| said = [verdict(r) for r in readings] | |
| if any(s is None for s in said): | |
| return f"{n} readings" | |
| changes = sum(1 for a, b in zip(said, said[1:]) | |
| if a["text"] != b["text"]) | |
| if not changes: | |
| return f"{n} readings · same answer" + ("" if short else " each time") | |
| if changes == 1: | |
| return f"{n} readings · answer changed once" | |
| return f"{n} readings · answer changed {changes} times" | |
| def week_groups(kept: list[dict]) -> list[dict]: | |
| """The kept readings grouped by the chart week they were read against. | |
| The list grows by week rather than by reading, which is what makes it | |
| hold: a year of Monday re-scores is 52 rows, and a team re-scoring twice | |
| a week is still 52. | |
| `n` on each reading is its place in the order they were made, counting | |
| from the oldest, so reading 1 stays reading 1 when a fifth is added under | |
| it. Weeks come back most-recently-read first, and so do the readings | |
| inside each one. | |
| """ | |
| groups: dict[str, list[dict]] = {} | |
| for n, e in enumerate(kept, 1): | |
| week = str(e.get("week") or "").strip() or "an earlier week" | |
| groups.setdefault(week, []).append(dict(e, n=n)) | |
| out = [{"week": w, "readings": list(reversed(rs))} | |
| for w, rs in groups.items()] | |
| out.sort(key=lambda g: -max(r["n"] for r in g["readings"])) | |
| return out | |
| # How many weeks stand open at the foot of a report before the rest fold into | |
| # one row. Three is what fits on a phone screen under the report without the | |
| # list becoming the page. | |
| OPEN_WEEKS = 3 | |
| _COUNT_WORDS = {2: "two", 3: "three", 4: "four", 5: "five", 6: "six", | |
| 7: "seven", 8: "eight", 9: "nine", 10: "ten"} | |
| def _weeks_said(n: int) -> str: | |
| return f'{_COUNT_WORDS.get(n, n)} week{"" if n == 1 else "s"}' | |
| def _pick(file: str) -> str: | |
| return ESC(str(file or "")) | |
| def _reading_row(r: dict) -> str: | |
| """One reading, inside its week.""" | |
| said = verdict(r) | |
| day = _daystamp(r.get("stamped_at")) | |
| return (f'<button type="button" class="rrow" data-pick="' | |
| f'{_pick(r.get("file"))}">' | |
| f'<span class="rno">Reading {r["n"]}' | |
| + (f' <span class="rday">· read {ESC(day)}</span>' if day else "") | |
| + '</span>' | |
| f'<span class="rsaid">{said["text"] if said else ""}</span>' | |
| f'<span class="ropen">Open →</span></button>') | |
| def _group_title(group: dict) -> str: | |
| """What the row is called: "Readings · 24–25 Aug". | |
| The days these readings were made, not the week they were read against. | |
| "Week 2026-08-17" was the first thing Kalam read on the first live | |
| re-score and the one thing he could not place — the chart week's start | |
| date reads as the past to anyone looking today, while the day a reading | |
| was made is the day they looked. A group whose readings carry no date | |
| says "Readings" and stops there. | |
| """ | |
| span = _span(group["readings"]) | |
| return f"Readings · {span}" if span else "Readings" | |
| def _group_note(group: dict, short: bool = False) -> str: | |
| """The line under the title: how many readings, whether the answer moved, | |
| and quietly which charts they were read against. | |
| The chart week is still the thing that groups these rows, so it is still | |
| written down — one step below the title, where it tells two groups apart | |
| without leading the row. | |
| """ | |
| said = _movement(group["readings"], short=short) | |
| week = week_span(group["week"]) | |
| return f"{said} · charts of {ESC(week)}" if week else said | |
| def _week_row(group: dict, short: bool = False) -> str: | |
| """A whole week as one row, for a week read once and for the weeks folded | |
| away below. It opens that week's latest reading.""" | |
| rs = group["readings"] | |
| return (f'<button type="button" class="rrow rwrow" data-pick="' | |
| f'{_pick(rs[0].get("file"))}">' | |
| f'<div class="rwk"><b>{ESC(_group_title(group))}</b>' | |
| f'<span class="rnote">{_group_note(group, short=short)}</span>' | |
| f'</div>' | |
| + _verdict_bits(verdict(rs[0])) | |
| + f'<span class="ropen">Open →</span></button>') | |
| def _week_details(group: dict, open_: bool) -> str: | |
| """A week read more than once: the week's own line, and every reading of | |
| it one click inside.""" | |
| rs = group["readings"] | |
| return (f'<details class="rweek"{" open" if open_ else ""}>' | |
| f'<summary>' | |
| f'<div class="rwk"><b>{ESC(_group_title(group))}</b>' | |
| f'<span class="rnote">{_group_note(group)}</span></div>' | |
| + _verdict_bits(verdict(rs[0])) | |
| + f'<span class="rchev">▼</span></summary>' | |
| f'<div class="rrows">' | |
| + "".join(_reading_row(r) for r in rs) | |
| + '</div></details>') | |
| def readings_list(kept: list[dict]) -> str: | |
| """Every earlier reading of this track, as a list of chart weeks. | |
| Under the report and under what to do about it: this week's answer is | |
| what the reader came for, and what the charts said before it is the | |
| second question. | |
| The newest weeks stand open; everything older folds into one row that | |
| names how much it holds and the span it covers, so a track read a hundred | |
| times is still a list somebody can look down. | |
| """ | |
| groups = week_groups(kept) | |
| if not groups: | |
| return "" | |
| n = len(kept) | |
| out = [render.CSS, '<div class="ml">', | |
| '<div class="rhead"><div class="lab">EARLIER READINGS</div>', | |
| f'<div class="rkept">{n} kept · ' | |
| f'{len(groups)} chart week{"" if len(groups) == 1 else "s"}</div>' | |
| '</div>', | |
| '<p class="den rwhy">Every re-read keeps the answer it replaced, ' | |
| 'grouped by the chart week it was read against. Opening one ' | |
| 'changes nothing.</p>', | |
| '<div class="rlist">'] | |
| for i, g in enumerate(groups[:OPEN_WEEKS]): | |
| out.append(_week_details(g, open_=(i == 0)) | |
| if len(g["readings"]) > 1 else _week_row(g)) | |
| older = groups[OPEN_WEEKS:] | |
| if older: | |
| held = sum(len(g["readings"]) for g in older) | |
| days = [d for d in (_daystamp(r.get("stamped_at")) | |
| for g in older for r in g["readings"]) if d] | |
| # Oldest first, the same way a group's own title runs. | |
| span = f", {_days_span(days[-1], days[0])}" if days else "" | |
| out.append(f'<details class="rfold"><summary>' | |
| f'<span class="rfoldlab">{held} more reading' | |
| f'{"" if held == 1 else "s"} · {_weeks_said(len(older))}' | |
| f'{ESC(span)}</span>' | |
| f'<span class="rchev">▼</span></summary>' | |
| f'<div class="rrows">' | |
| + "".join(_week_row(g, short=True) for g in older) | |
| + '</div></details>') | |
| out.append('</div></div>') | |
| return "".join(out) | |
| def earlier_reading_note(entry: dict, current_week: str, | |
| n: int | None = None, | |
| total: int | None = None) -> str: | |
| """Sits above an earlier reading, and says that is what it is. | |
| Nothing on the report below it says which week it was read against — the | |
| page looks exactly like the current one, because it IS a report, drawn | |
| the same way. So this line carries both weeks: the one these numbers were | |
| read against, and the one the track's current reading is from. | |
| When both weeks are the same the two halves printed the same date twice | |
| and the line said nothing at all, so that case says the thing that is | |
| actually true about it: this track was read against those charts more | |
| than once. The date it was read carries the difference either way, and | |
| the number matches the button that was pressed to get here. | |
| """ | |
| week = str(entry.get("week") or "").strip() or "an earlier week" | |
| cur = str(current_week or "").strip() | |
| day = _daystamp(entry.get("stamped_at")) | |
| who = (f"Reading {n} of {total}" if n and total | |
| else f"Reading {n}" if n else "An earlier reading") | |
| when = f"read on {ESC(day)} against" if day else "read against" | |
| if cur and cur == week: | |
| tail = ('The current reading is from the same charts — this ' | |
| 'track was read against them more than once.') | |
| elif cur and cur > week: | |
| # Only said when it is true. The comparison is between two week | |
| # stamps of the same shape, and anything else falls to the plain | |
| # line below rather than to a claim about which came first. | |
| tail = (f'The current reading is against the charts of ' | |
| f'{ESC(week_span(cur))}, a later week than this one.') | |
| else: | |
| tail = (f'The current reading is from the charts of ' | |
| f'{ESC(week_span(cur)) if cur else "?"}.') | |
| return (render.CSS + '<div class="ml"><div class="note">' | |
| '<div class="lab">AN EARLIER READING</div>' | |
| f'<p class="why"><b>{ESC(who)}</b> — {when} the charts ' | |
| f'of {ESC(week_span(week))}. {tail}</p></div></div>') | |
| def reading_day(entry: dict) -> str: | |
| """"12 Aug" — the day one kept reading was made, or nothing when the | |
| entry carries no date.""" | |
| return _daystamp(entry.get("stamped_at")) | |
| def reading_stamp(entry: dict, n: int | None) -> str: | |
| """The line every card on a kept reading closes with. | |
| Uppercased by the stylesheet. It exists so that a screenshot taken from | |
| the middle of the page cannot pass for this week's answer: the banner is | |
| at the top, and nobody screenshots the top. | |
| """ | |
| day = _daystamp(entry.get("stamped_at")) | |
| bits = [f"reading {n}" if n else "an earlier reading"] | |
| if day: | |
| bits.append(f"read {day.lower()}") | |
| bits.append("not this week's answer") | |
| return " · ".join(bits) | |
| def step_bar(entry: dict, n: int, total: int, | |
| older: str | None, newer: str | None) -> str: | |
| """Which reading this is, and the step to the one either side of it. | |
| One reading at a time: inside an open reading there is no list, because | |
| the list is the thing the reader just came out of. | |
| """ | |
| week = str(entry.get("week") or "").strip() | |
| day = _daystamp(entry.get("stamped_at")) | |
| who = f"Reading {n} of {total}" | |
| # The day this reading was made, which is what tells one record on this | |
| # track apart from the next: two readings a day apart can be read against | |
| # the same charts. The chart week stands in only for a reading filed | |
| # before the day was written down. | |
| tail = f"read {day}" if day else week_span(week) if week else "" | |
| if tail: | |
| # Dropped on a phone, where the bar is three controls across 390px | |
| # and this line is the one that can be shorter. | |
| who += f'<span class="rbarwk"> · {ESC(tail)}</span>' | |
| def pill(file: str | None, label: str) -> str: | |
| if not file: | |
| return f'<span class="rstep off">{label}</span>' | |
| return (f'<button type="button" class="rstep" ' | |
| f'data-pick="{_pick(file)}">{label}</button>') | |
| return (f'<div class="rbar"><span class="rbarwho">{who}</span>' | |
| f'<span class="rsteps">{pill(older, "‹ older")}' | |
| f'{pill(newer, "newer ›")}</span></div>') | |
| def reading_foot(entry: dict) -> str: | |
| """The line the page closes on. Says what this page is and what it is | |
| not for — a reader who has scrolled a kept reading to the bottom is one | |
| press from a Re-run that is not on this page at all.""" | |
| day = _daystamp(entry.get("stamped_at")) | |
| from_when = f" from {ESC(day)}" if day else "" | |
| return (render.CSS + '<div class="ml"><p class="rfoot">You are reading a ' | |
| f'record{from_when}. It cannot be re-run or deleted from here, ' | |
| f'and nothing on this page has changed by being opened.</p>' | |
| '</div>') | |
| def archive_page(parts: list[str], stamp: str) -> str: | |
| """A kept reading, on its own paper. | |
| The tone and the stamp are carried by one wrapper so that every block | |
| inside it — the banner, the report, the closing line — is on the same | |
| page rather than three blocks that happen to sit near each other. | |
| """ | |
| said = str(stamp or "").replace('"', "") | |
| return (f'<div class="arch" style=\'--stamp:"{ESC(said)}"\'>' | |
| + "".join(parts) + '</div>') | |
| def batch_summary(done: list[dict], failed: list[dict]) -> str: | |
| out = [render.CSS, '<div class="ml">', | |
| f'<div class="kicker">Batch</div>' | |
| f'<h1 class="hdr">{len(done)} of {len(done) + len(failed)} ' | |
| f'analysed</h1>', | |
| '<p class="sub">Each one is saved. Open any of them in ' | |
| '<b>Tracks</b>.</p>'] | |
| for d in done: | |
| lead = (d.get("top_markets") or [{}])[0] | |
| flat = bool(d.get("flat")) | |
| cls = "flat" if flat else _state(lead) | |
| where = ESC(worldmap.market_name(lead.get("iso", ""), lead.get("name"))) | |
| out.append(f'<div class="card {cls}"><div class="mkt">' | |
| f'<div class="name">{ESC(d.get("label", ""))}</div>') | |
| if lead: | |
| out.append(render.chip(cls)) | |
| out.append("</div>") | |
| if lead and flat: | |
| # Same rule as the Saved card: a field with no ranking has no | |
| # closest market, and naming one would invent the finding the | |
| # report refuses to make. | |
| out.append('<p class="why">Reads much the same in every market ' | |
| 'measured — no ranking to read.</p>') | |
| elif lead and cls == "self": | |
| out.append(f'<p class="why">Already charting in {where} — the ' | |
| f'reading there is the record meeting itself.</p>') | |
| elif lead: | |
| out.append(f'<p class="why">Closest to {where} at ' | |
| f'{livematch.pct(lead.get("best", 0))}</p>') | |
| out.append("</div>") | |
| for f in failed: | |
| out.append(f'<div class="card flat"><div class="mkt">' | |
| f'<div class="name">{ESC(f.get("label", ""))}</div>' | |
| f'</div><p class="why">Could not be read: ' | |
| f'{ESC(str(f.get("error", "")))}</p></div>') | |
| out.append("</div>") | |
| return "".join(out) | |
| def rescore_summary(out: dict) -> str: | |
| n, total = len(out.get("rescored", [])), out.get("total", 0) | |
| body = [render.CSS, '<div class="ml"><div class="card flat">', | |
| f'<div class="lab">RE-SCORED</div>' | |
| f'<p class="why"><b>{n} of {total}</b> saved tracks read ' | |
| f'against the charts of ' | |
| f'{ESC(week_span(str(out.get("week", ""))))}.</p>'] | |
| # A re-score can promote a window nobody had cut, and cuts it. Both lines | |
| # are printed only when the number behind them is real: a sweep where | |
| # every card already had its audio says neither. | |
| cut = int(out.get("clips_cut") or 0) | |
| missed = int(out.get("clips_missed") or 0) | |
| if cut: | |
| body.append(f'<p class="why"><b>{render._plural(cut, "clip")}</b> cut ' | |
| f'for windows these readings picked and no clip existed ' | |
| f'for.</p>') | |
| if missed: | |
| body.append(f'<p class="den">{render._plural(missed, "clip")} could ' | |
| f'not be cut. Those cards say so, and the readings ' | |
| f'themselves are complete.</p>') | |
| for f in out.get("failed", []): | |
| body.append(f'<p class="den">{ESC(str(f.get("id", "")))}: ' | |
| f'{ESC(str(f.get("error", "")))}</p>') | |
| body.append("</div></div>") | |
| return "".join(body) | |
| def bands_note() -> str: | |
| return (f'{livematch.PLACE_THRESHOLD:.0%} and above is a ' | |
| f'{render.CHIP["place"]}, {livematch.DERIVE_THRESHOLD:.0%} to ' | |
| f'{livematch.PLACE_THRESHOLD:.0%} is a {render.CHIP["derive"]}.') | |