kalamishere commited on
Commit
b7e632e
Β·
verified Β·
1 Parent(s): 1a05ceb

prototype: Within reach tab (78-93 band dive-in)

Browse files
Files changed (2) hide show
  1. app.py +70 -0
  2. reachview.py +544 -0
app.py CHANGED
@@ -62,6 +62,7 @@ import clap_embed
62
  import howitworks
63
  import livematch
64
  import lookup as lookupmod
 
65
  import render
66
  import savedview
67
  import store
@@ -1262,6 +1263,75 @@ with gr.Blocks(title="marathonmvp β€” live matcher") as demo:
1262
  outputs=[saved_out, saved_nonce],
1263
  api_visibility="private", **LANE)
1264
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1265
  with gr.Tab("How it works", id="how"):
1266
  gr.HTML(howitworks.render(CORPUS))
1267
 
 
62
  import howitworks
63
  import livematch
64
  import lookup as lookupmod
65
+ import reachview
66
  import render
67
  import savedview
68
  import store
 
1263
  outputs=[saved_out, saved_nonce],
1264
  api_visibility="private", **LANE)
1265
 
1266
+ # PROTOTYPE. The dive-in layer for the 78–93 band of one saved
1267
+ # analysis: everything it draws lives in `reachview.py`, and the tab
1268
+ # is two pickers and two blocks of HTML. Kept whole in one place so
1269
+ # that dropping the idea is dropping this block and one import.
1270
+ with gr.Tab("Within reach", id="reach"):
1271
+
1272
+ def reach_open(passphrase, aid):
1273
+ """A saved analysis's band. At most ten Deezer lookups."""
1274
+ blank = gr.update(choices=[], value=None, visible=False)
1275
+ if not _ok(passphrase):
1276
+ return (_notice(NEED_PASS if PASSPHRASE else NO_SECRET),
1277
+ blank, "")
1278
+ if not aid:
1279
+ return "", blank, ""
1280
+ res = store.result(aid)
1281
+ if not res:
1282
+ return (_notice("That analysis could not be read back."),
1283
+ blank, "")
1284
+ label = (store.meta(aid) or {}).get("label") or aid
1285
+ picks = reachview.market_choices(res)
1286
+ return (reachview.render_band(res, label),
1287
+ gr.update(choices=picks, value=None,
1288
+ visible=bool(picks)), "")
1289
+
1290
+ def reach_market(passphrase, aid, iso):
1291
+ """One market of that band, with its own players. Three
1292
+ lookups, paid only when a reader asks for them."""
1293
+ if not _ok(passphrase) or not aid or not iso:
1294
+ return ""
1295
+ res = store.result(aid)
1296
+ if not res:
1297
+ return _notice("That analysis could not be read back.")
1298
+ r = (res.get("regions") or {}).get(iso) or {}
1299
+ ids, _ = reachview.preview_ids(
1300
+ [(iso, m.get("deezer_id"))
1301
+ for m in (r.get("top") or [])[:reachview.
1302
+ MARKET_PREVIEW_CAP]],
1303
+ reachview.MARKET_PREVIEW_CAP)
1304
+ previews = render.deezer_previews(ids) if ids else {}
1305
+ return reachview.market_card(res, iso, previews)
1306
+
1307
+ gr.HTML(reachview.opening())
1308
+
1309
+ @gr.render(inputs=[pw], show_progress="hidden")
1310
+ def reach_body(passphrase):
1311
+ if not _ok(passphrase):
1312
+ gr.HTML(_notice(NEED_PASS if PASSPHRASE else NO_SECRET))
1313
+ return
1314
+ try:
1315
+ rows = _rows()
1316
+ except store.StoreUnavailable as exc:
1317
+ print(f"[store] index unreadable: {exc}", flush=True)
1318
+ gr.HTML(savedview.unreachable())
1319
+ return
1320
+ reach_pick = gr.Dropdown(
1321
+ choices=reachview.saved_choices(rows), value=None,
1322
+ label="Which track", interactive=True)
1323
+ reach_out = gr.HTML(elem_id="reachout")
1324
+ reach_one_pick = gr.Dropdown(
1325
+ choices=[], value=None, visible=False, interactive=True,
1326
+ label="Open one of these markets in full")
1327
+ reach_one = gr.HTML(elem_id="reachone")
1328
+ reach_pick.change(
1329
+ reach_open, inputs=[pw, reach_pick],
1330
+ outputs=[reach_out, reach_one_pick, reach_one], **PRIV)
1331
+ reach_one_pick.change(
1332
+ reach_market, inputs=[pw, reach_pick, reach_one_pick],
1333
+ outputs=[reach_one], **PRIV)
1334
+
1335
  with gr.Tab("How it works", id="how"):
1336
  gr.HTML(howitworks.render(CORPUS))
1337
 
reachview.py ADDED
@@ -0,0 +1,544 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Within reach β€” the way into the 78–93 band of one saved analysis.
2
+
3
+ **Prototype.** Kalam asked for a dive-in layer and said he may keep it or
4
+ drop it, so it is a module of its own with a tab of its own: nothing in
5
+ `render.py`, `savedview.py`, `worldmap.py` or `store.py` is touched by it,
6
+ and deleting this file plus its tab removes it completely.
7
+
8
+ What it is for. A saved report shows the eight closest markets and paints
9
+ the rest of the world orange on the map with no way in. Orange is the
10
+ derive band β€” 78 to 93 β€” the markets where a different cut, edit or version
11
+ is what would close the gap. On a real reading that is dozens of markets,
12
+ and the report gives the reader no way to see any of them. This is that way
13
+ in.
14
+
15
+ Three rules it inherits from every other surface, and one of its own:
16
+
17
+ * the cut-offs are quoted wherever a number sits near one, because 89% means
18
+ nothing without the 93 line beside it;
19
+ * every percentage carries the denominator it was measured over, so
20
+ "closest record" is read against how much of that chart could be measured;
21
+ * a market the track already charts in is named as our own release and never
22
+ given a percentage, and a flat reading is said to be flat and stopped
23
+ there;
24
+ * and the band itself is not a verdict. It is defined as *not close enough
25
+ to act on from numbers alone* β€” so every line here is a candidate for
26
+ somebody's ears, not a finding.
27
+
28
+ Deezer. The 30-second players are the expensive part: one lookup per
29
+ distinct record. The opening view asks for at most `PREVIEW_CAP` of them and
30
+ says on screen how many it got; one market opened by itself asks for at most
31
+ `MARKET_PREVIEW_CAP`. `render.deezer_previews` does the work β€” its keeping,
32
+ its throttling and its record of what came back empty are shared with the
33
+ rest of the app, so this view costs the quota nothing the report would not
34
+ have cost anyway.
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import livematch
40
+ import render
41
+ import worldmap
42
+
43
+ ESC = render.ESC
44
+
45
+ # Inside the band, the split between "a near miss" and "a longer bridge".
46
+ # It is not a cut-off the app decides anything on β€” both halves are the same
47
+ # derive band β€” it is where the list stops being worth a player each.
48
+ NEAR_LINE = 0.88
49
+ # How many near misses get a card. Ten is the shortlist a person can listen
50
+ # through in one sitting; the rest are one line each, and any of them opens
51
+ # in full from the picker below.
52
+ NEAR_CAP = 10
53
+ # Distinct records looked up on the opening view, and on one market opened
54
+ # by itself.
55
+ PREVIEW_CAP = 10
56
+ MARKET_PREVIEW_CAP = 3
57
+
58
+ PROTOTYPE = "Prototype β€” this view may change or go away."
59
+
60
+
61
+ # --------------------------------------------------------------------------
62
+ # reading the stored result
63
+
64
+
65
+ def in_band(res: dict) -> list[tuple[str, dict]]:
66
+ """Every market in the derive band, strongest first.
67
+
68
+ The band is read off the same numbers `livematch.band` reads, so a market
69
+ that is orange on the report's map is a market on this list. A market at
70
+ or above the same-recording line is the track meeting its own chart entry
71
+ and is never in the band β€” it is above it β€” but it is excluded by name
72
+ rather than by arithmetic, so a change to either line cannot quietly let
73
+ one through.
74
+ """
75
+ regions = res.get("regions") or {}
76
+ rows = [(iso, r) for iso, r in regions.items()
77
+ if isinstance(r, dict)
78
+ and float(r.get("best") or 0) < worldmap.SAME_RECORDING
79
+ and livematch.band(float(r.get("best") or 0)) == "derive"]
80
+ rows.sort(key=lambda kv: -float(kv[1].get("best") or 0))
81
+ return rows
82
+
83
+
84
+ def self_markets(res: dict) -> list[tuple[str, dict]]:
85
+ """Markets where the reading is the record meeting its own chart entry."""
86
+ regions = res.get("regions") or {}
87
+ rows = [(iso, r) for iso, r in regions.items()
88
+ if isinstance(r, dict)
89
+ and float(r.get("best") or 0) >= worldmap.SAME_RECORDING]
90
+ rows.sort(key=lambda kv: -float(kv[1].get("best") or 0))
91
+ return rows
92
+
93
+
94
+ def _name(iso: str, r: dict) -> str:
95
+ return worldmap.market_name(iso, r.get("name"))
96
+
97
+
98
+ def _plural(n: int, unit: str) -> str:
99
+ """"1 market", "5 markets". A count that reads "1 markets" tells the
100
+ reader the page cannot count β€” the same rule `render._plural` follows."""
101
+ return f'{n} {unit if n == 1 else unit + "s"}'
102
+
103
+
104
+ def _nearest(r: dict) -> dict | None:
105
+ top = r.get("top") or []
106
+ return top[0] if top else None
107
+
108
+
109
+ def _record(m: dict | None) -> str:
110
+ if not m:
111
+ return ""
112
+ return f'{m.get("artist") or "β€”"} β€” {m.get("title") or "β€”"}'
113
+
114
+
115
+ def saved_choices(rows: list[dict]) -> list[tuple[str, str]]:
116
+ """The picker's list: what a reader would call the track, and its id."""
117
+ out = []
118
+ for r in rows:
119
+ artist = (r.get("artist") or "").strip()
120
+ title = (r.get("title") or "").strip()
121
+ name = f"{artist} β€” {title}" if artist and title else (
122
+ r.get("label") or r.get("id") or "")
123
+ week = str(r.get("corpus_week") or "")
124
+ out.append((f"{name} Β· week {week}" if week else name, r.get("id")))
125
+ return out
126
+
127
+
128
+ def market_choices(res: dict) -> list[tuple[str, str]]:
129
+ """Every market in the band, for the one-at-a-time picker."""
130
+ return [(f'{_name(iso, r)} Β· {livematch.pct(float(r["best"]))}', iso)
131
+ for iso, r in in_band(res)]
132
+
133
+
134
+ def preview_ids(pairs: list[tuple[str, int | None]], cap: int
135
+ ) -> tuple[list[int], set]:
136
+ """The distinct records to look up, and which markets that covers.
137
+
138
+ Deduped by record rather than by market: one record can be the nearest
139
+ thing charting in four countries, and looking it up four times would
140
+ spend four of the ten on one player.
141
+ """
142
+ ids: list[int] = []
143
+ seen: dict = {}
144
+ covered: set = set()
145
+ for iso, did in pairs:
146
+ if not did:
147
+ continue
148
+ if did in seen:
149
+ covered.add(iso)
150
+ continue
151
+ if len(ids) >= cap:
152
+ continue
153
+ seen[did] = True
154
+ ids.append(int(did))
155
+ covered.add(iso)
156
+ return ids, covered
157
+
158
+
159
+ # --------------------------------------------------------------------------
160
+ # the one fact that separates a market, where there is one to state
161
+
162
+
163
+ def _genre_form(res: dict) -> str | None:
164
+ f = ((res.get("tags") or {}).get("fields") or {}).get("main_genre") or {}
165
+ return f.get("form")
166
+
167
+
168
+ def _sharing(band: list[tuple[str, dict]], iso: str) -> list[str]:
169
+ """Every market in the band whose nearest record is this market's, in
170
+ band order and including this one.
171
+
172
+ On the real YO YO reading one record β€” Ozuna, Omar Courtz β€” "ZIZI" β€” is
173
+ the nearest thing charting in five of the ten closest markets. Five cards
174
+ each saying "the same record carries another market" is the same sentence
175
+ five times; the fact is that five markets are one conversation, and it is
176
+ worth saying once, where the record first appears.
177
+ """
178
+ mine = _nearest(dict(band).get(iso) or {})
179
+ if not mine:
180
+ return []
181
+ key = (mine.get("artist"), mine.get("title"))
182
+ out = []
183
+ for other, r in band:
184
+ top = _nearest(r)
185
+ if top and (top.get("artist"), top.get("title")) == key:
186
+ out.append(other)
187
+ return out
188
+
189
+
190
+ def _names(band: list[tuple[str, dict]], isos: list[str]) -> str:
191
+ by = dict(band)
192
+ named = [_name(i, by[i]) for i in isos if i in by]
193
+ if len(named) <= 3:
194
+ return (named[0] if len(named) == 1
195
+ else ", ".join(named[:-1]) + " and " + named[-1])
196
+ return ", ".join(named[:3]) + f" and {len(named) - 3} more"
197
+
198
+
199
+ def separator(res: dict, band: list[tuple[str, dict]], iso: str) -> str:
200
+ """One measurable thing to say about this market, beyond its number.
201
+
202
+ Everything printed here comes from what is written down: which other
203
+ markets rest on the same record, and the labels iTunes or Deezer gave it.
204
+ There is no tempo and no vocal reading kept against a charting record, so
205
+ a tempo gap or a voice difference is not a comparison this app can make,
206
+ and it says nothing rather than estimating one.
207
+ """
208
+ shared = _sharing(band, iso)
209
+ if len(shared) > 1 and shared[0] != iso:
210
+ first = _names(band, shared[:1])
211
+ return f'Same record as {first} above.'
212
+ if len(shared) > 1:
213
+ return (f'This record is also the nearest thing charting in '
214
+ f'{ESC(_names(band, shared[1:]))} β€” {len(shared)} markets in '
215
+ f'this band on one record, so one different version could '
216
+ f'answer all of them.')
217
+ r = dict(band).get(iso) or {}
218
+ top = _nearest(r) or {}
219
+ labels = [g for g in (top.get("genres") or []) if g]
220
+ mine = _genre_form(res)
221
+ if labels and mine:
222
+ if {g.lower() for g in labels} & {mine.lower()}:
223
+ return (f'That record carries the same label this track\'s genre '
224
+ f'form does: {ESC(mine)}.')
225
+ return (f'That record is labelled {ESC(", ".join(labels))}; this '
226
+ f'track\'s genre form reads {ESC(mine)}.')
227
+ if labels:
228
+ # The label is the useful half and it is printed; what is missing is
229
+ # the other side of the comparison, and that is said rather than
230
+ # filled in from the words the neighbouring records happen to carry.
231
+ return (f'That record is labelled {ESC(", ".join(labels))}. This '
232
+ f'analysis carries no genre of its own, so there is nothing '
233
+ f'to line it up against.')
234
+ if "genres" in top:
235
+ return ('That record carries no label at iTunes or Deezer, so there '
236
+ 'is nothing to compare it on beyond the sound.')
237
+ return ''
238
+
239
+
240
+ # --------------------------------------------------------------------------
241
+ # the blocks
242
+
243
+
244
+ def opening() -> str:
245
+ """What sits above the picker: the prototype line and one sentence."""
246
+ return (render.CSS + '<div class="ml">'
247
+ f'<div class="kicker">{ESC(PROTOTYPE)}</div>'
248
+ '<div class="hdr">Within reach</div>'
249
+ '<div class="sub">The markets a different cut, edit or version '
250
+ 'could bridge to β€” the ones a report paints orange and gives you '
251
+ 'no way into. Choose a track you have already analysed.</div>'
252
+ '</div>')
253
+
254
+
255
+ def flat(res: dict, label: str) -> str:
256
+ """A track that reads level everywhere has no shortlist here."""
257
+ f = worldmap.field(res)
258
+ n = f["n"]
259
+ return (render.CSS + '<div class="ml">'
260
+ f'<div class="hdr">{ESC(label)}</div>'
261
+ '<div class="card flat">'
262
+ f'<div class="lab">nothing to dive into</div>'
263
+ f'<p class="why"><b>This track reads level across all {n} markets '
264
+ f'measured.</b> The strongest of them sits '
265
+ f'{livematch.pct(f["hi"])} and the middle of them '
266
+ f'{livematch.pct(f["median"])}, which is too close together to '
267
+ f'call one market nearer than another. So the 78–93 band here is '
268
+ f'not a shortlist β€” it is most of the world at the same distance, '
269
+ f'and picking ten of them would be picking at random. The saved '
270
+ f'report says the same thing at the top of it.</p>'
271
+ '</div></div>')
272
+
273
+
274
+ def _self_line(res: dict) -> str:
275
+ rows = self_markets(res)
276
+ if not rows:
277
+ return ''
278
+ names = ", ".join(_name(iso, r) for iso, r in rows)
279
+ return (f'<div class="note"><p class="why"><b>Already charting in '
280
+ f'{ESC(names)}.</b> That is our own release meeting its own chart '
281
+ f'entry, so no percentage is read there and those markets are not '
282
+ f'in the band below.</p></div>')
283
+
284
+
285
+ def _line_note(best: float) -> str:
286
+ """"89% β€” four below the 93 line that reads as a direct fit." """
287
+ p = livematch.whole_pct(float(best))
288
+ if float(best) >= NEAR_LINE:
289
+ return (f'{p}% β€” {93 - p} below the 93 line that reads as a direct '
290
+ f'fit, and above the 78 line the band starts at.')
291
+ if p == 78:
292
+ return (f'{p}% β€” exactly on the 78 line the band starts at, '
293
+ f'{93 - p} below the 93 line that reads as a direct fit.')
294
+ return (f'{p}% β€” {p - 78} above the 78 line the band starts at, '
295
+ f'{93 - p} below the 93 line that reads as a direct fit.')
296
+
297
+
298
+ def _rank(m: dict | None) -> str:
299
+ return f'#{int(m["rank"])}' if m and m.get("rank") else ''
300
+
301
+
302
+ def near_card(res: dict, band: list[tuple[str, dict]], iso: str, r: dict,
303
+ previews: dict) -> str:
304
+ """One near miss, with the nearest charting record playable if we have it.
305
+ """
306
+ top = _nearest(r)
307
+ url = previews.get((top or {}).get("deezer_id"))
308
+ out = [f'<div class="card derive" id="market-{ESC(iso)}">',
309
+ f'<div class="mkt"><span class="name">{ESC(_name(iso, r))}</span>'
310
+ f'<span class="iso">{ESC(iso)}</span>{render.chip("derive")}</div>',
311
+ '<div class="sim"><div class="simtop"><div>'
312
+ f'<div class="simpct" style="color:var(--derive)">'
313
+ f'{livematch.whole_pct(r["best"])}<small>%</small></div>']
314
+ if top:
315
+ out.append(f'<div class="simto">{ESC(_record(top))}</div>')
316
+ out.append(f'</div><div class="simcaption">{render.CAPTION["derive"]}'
317
+ f'</div></div>')
318
+ out.append(render._scale(float(r["best"]) * 100, "derive") + '</div>')
319
+ out.append(f'<div class="den">{_line_note(r["best"])} '
320
+ f'{render._pool_line(r)}.</div>')
321
+ if top:
322
+ # One record, once. The number above is its number, so repeating the
323
+ # title in a row under it would print the same record twice on a card
324
+ # that only shows one.
325
+ rank = _rank(top)
326
+ out.append(f'<div class="den">That record is'
327
+ + (f' at {ESC(rank)} there this week.' if rank
328
+ else ' on the chart there this week.') + '</div>')
329
+ if url:
330
+ out.append('<div class="player">'
331
+ '<span class="plab">NEAREST RECORD</span>'
332
+ f'<audio controls preload="none" src="{ESC(url)}">'
333
+ '</audio></div>')
334
+ else:
335
+ out.append('<div class="den">No 30-second preview came back for '
336
+ 'that record, so there is nothing to play here.</div>')
337
+ sep = separator(res, band, iso)
338
+ if sep:
339
+ out.append(f'<div class="den">{sep}</div>')
340
+ out.append('</div>')
341
+ return "".join(out)
342
+
343
+
344
+ def bridge_row(iso: str, r: dict) -> str:
345
+ """One longer bridge: a line, no player."""
346
+ top = _nearest(r)
347
+ bits = [f'<div class="nrow" id="market-{ESC(iso)}"><div class="ntop">'
348
+ f'<span class="pct" style="color:var(--derive)">'
349
+ f'{livematch.pct(r["best"])}</span>'
350
+ f'<span class="lbl">{ESC(_name(iso, r))}</span></div>']
351
+ line = []
352
+ if top:
353
+ rank = _rank(top)
354
+ line.append(f'nearest there: {ESC(_record(top))}'
355
+ + (f' Β· {ESC(rank)}' if rank else ''))
356
+ line.append(f'{int(r.get("matched") or 0)} of the '
357
+ f'{int(r.get("pool") or 0)} sounds charting there measured')
358
+ bits.append(f'<div class="nwhere">{" Β· ".join(line)}</div></div>')
359
+ return "".join(bits)
360
+
361
+
362
+ def band_map(res: dict, band: list[tuple[str, dict]]) -> str:
363
+ """The world with only the band markets coloured.
364
+
365
+ Built by handing `worldmap.svg` a reading that carries the band and
366
+ nothing else: every other country falls through to the map's uncoloured
367
+ tone, which is exactly "fade everything, colour the band". The map is not
368
+ re-drawn or re-coloured here β€” it is the same picture the report draws,
369
+ given a smaller reading.
370
+ """
371
+ if not band:
372
+ return ''
373
+ isos = [iso for iso, _ in band]
374
+ return worldmap.svg({"regions": {iso: r for iso, r in band}}, linked=isos)
375
+
376
+
377
+ def market_card(res: dict, iso: str, previews: dict) -> str:
378
+ """One market, in full, with its closest three records playable.
379
+
380
+ This is the on-demand half: the opening view spends ten lookups on ten
381
+ markets, and a reader who wants everything about one market pays three
382
+ more for that one.
383
+ """
384
+ band = in_band(res)
385
+ r = dict(band).get(iso)
386
+ if not r:
387
+ regions = res.get("regions") or {}
388
+ if iso in regions:
389
+ return (render.CSS + '<div class="ml"><div class="card flat">'
390
+ f'<div class="den">{ESC(_name(iso, regions[iso]))} is not '
391
+ f'in the 78–93 band for this track, so there is nothing '
392
+ f'to open here.</div></div></div>')
393
+ return ''
394
+ top = (r.get("top") or [])[:3]
395
+ out = [render.CSS, '<div class="ml">',
396
+ f'<div class="lab">one market in full</div>',
397
+ f'<div class="card derive" id="one-{ESC(iso)}">',
398
+ f'<div class="mkt"><span class="name">{ESC(_name(iso, r))}</span>'
399
+ f'<span class="iso">{ESC(iso)}</span>{render.chip("derive")}</div>',
400
+ '<div class="sim"><div class="simtop"><div>'
401
+ f'<div class="simpct" style="color:var(--derive)">'
402
+ f'{livematch.whole_pct(r["best"])}<small>%</small></div>']
403
+ if top:
404
+ out.append(f'<div class="simto">{ESC(_record(top[0]))}</div>')
405
+ out.append(f'</div><div class="simcaption">{render.CAPTION["derive"]}'
406
+ f'</div></div>')
407
+ out.append(render._scale(float(r["best"]) * 100, "derive") + '</div>')
408
+ out.append(f'<div class="den">{_line_note(r["best"])} '
409
+ f'{render._pool_line(r)}. Closest '
410
+ f'{"three" if len(top) >= 3 else len(top)}:</div>')
411
+ for m in top:
412
+ url = previews.get(m.get("deezer_id"))
413
+ player = (f'<audio controls preload="none" src="{ESC(url)}"></audio>'
414
+ if url else '')
415
+ out.append(
416
+ f'<div class="row"><span class="pct" '
417
+ f'style="color:var(--{livematch.band(m["similarity"])})">'
418
+ f'{livematch.pct(m["similarity"])}</span>'
419
+ f'<span class="lbl">{ESC(_record(m))}</span>'
420
+ f'<span class="rk">{ESC(_rank(m))}</span>{player}</div>')
421
+ sep = separator(res, band, iso)
422
+ if sep:
423
+ out.append(f'<div class="den">{sep}</div>')
424
+ out.append('<div class="den">Nothing here is a verdict. This market is in '
425
+ 'the band because it is not close enough to act on from the '
426
+ 'numbers alone β€” what decides it is hearing the two records '
427
+ 'next to each other.</div>')
428
+ out.append('</div></div>')
429
+ return "".join(out)
430
+
431
+
432
+ # --------------------------------------------------------------------------
433
+ # the whole view
434
+
435
+
436
+ def render_band(res: dict, label: str) -> str:
437
+ """The tab body for one saved analysis.
438
+
439
+ Costs at most `PREVIEW_CAP` Deezer lookups, deduped by record, and says
440
+ on screen how many players it got and why the rest have none.
441
+ """
442
+ if worldmap.field(res)["flat"]:
443
+ return flat(res, label)
444
+
445
+ band = in_band(res)
446
+ measured = len(res.get("regions") or {})
447
+ if not band:
448
+ return (render.CSS + '<div class="ml">'
449
+ f'<div class="hdr">{ESC(label)}</div>'
450
+ + _self_line(res) +
451
+ f'<div class="card flat"><div class="den">No market sits in '
452
+ f'the 78–93 band for this track this week, out of '
453
+ f'{_plural(measured, "market")} measured. The saved report '
454
+ f'has where it does sit.</div>'
455
+ '</div></div>')
456
+
457
+ # Near misses get a card each, capped at ten. Anything above 88 that the
458
+ # cap pushed out is not silently downgraded: it drops into the list below
459
+ # and that list says so, because a market reading 91% under a heading
460
+ # marked 78–88 would be the page mis-stating its own number.
461
+ near = [(iso, r) for iso, r in band if float(r["best"]) >= NEAR_LINE]
462
+ spill = near[NEAR_CAP:]
463
+ near = near[:NEAR_CAP]
464
+ picked = {iso for iso, _ in near}
465
+ rest = [(iso, r) for iso, r in band if iso not in picked]
466
+
467
+ ids, _covered = preview_ids(
468
+ [(iso, (_nearest(r) or {}).get("deezer_id")) for iso, r in near],
469
+ PREVIEW_CAP)
470
+ previews = render.deezer_previews(ids) if ids else {}
471
+ played = sum(1 for iso, r in near
472
+ if previews.get((_nearest(r) or {}).get("deezer_id")))
473
+
474
+ out = [render.CSS, '<div class="ml">',
475
+ f'<div class="hdr">{ESC(label)}</div>',
476
+ f'<div class="sub"><b>{len(band)} '
477
+ f'{"market sits" if len(band) == 1 else "markets sit"} in the '
478
+ f'78–93 band for this track</b> β€” close enough that a different '
479
+ f'version could bridge the gap. Nobody has listened for you; this '
480
+ f'is the shortlist worth an ear.</div>',
481
+ f'<div class="den">{len(band)} of '
482
+ f'{_plural(measured, "market")} measured this '
483
+ f'week. Nothing here is a verdict: the band is defined as not '
484
+ f'close enough to act on from the numbers alone.</div>',
485
+ _self_line(res)]
486
+
487
+ # -- near misses -------------------------------------------------------
488
+ if near:
489
+ out.append('<h2><span>Near misses</span>'
490
+ f'<span class="tag">{len(near)} of {len(band)} Β· 88–93'
491
+ '</span></h2>')
492
+ out.append(f'<div class="sub">The closest end of the band. Players on '
493
+ f'the {played} nearest; the rest are listed without audio, '
494
+ f'and any market opens in full further down.</div>')
495
+ for iso, r in near:
496
+ out.append(near_card(res, band, iso, r, previews))
497
+ out.append('<div class="den">The charting records are held as sound '
498
+ 'and as the labels iTunes or Deezer gave them β€” no tempo '
499
+ 'and no vocal reading is kept against a chart entry β€” so '
500
+ 'the line under each market compares what is written down '
501
+ 'and stops there.</div>')
502
+ else:
503
+ out.append('<h2><span>Near misses</span>'
504
+ '<span class="tag">none</span></h2>')
505
+ out.append(f'<div class="sub">No market in this band reads 88 or '
506
+ f'above, so there is no closest end to lead with. All '
507
+ f'{len(band)} of them are listed below, and any of them '
508
+ f'opens in full with its players.</div>')
509
+
510
+ # -- longer bridges ----------------------------------------------------
511
+ if rest:
512
+ lo = livematch.whole_pct(min(float(r["best"]) for _, r in rest))
513
+ hi = livematch.whole_pct(max(float(r["best"]) for _, r in rest))
514
+ out.append('<h2><span>Longer bridges</span>'
515
+ f'<span class="tag">{_plural(len(rest), "market")} Β· '
516
+ f'{lo}–{hi}</span>'
517
+ '</h2>')
518
+ line = ('Same band, further out: a bigger change to the record before '
519
+ 'it reads as a fit. No players here β€” open one below to hear '
520
+ 'it.')
521
+ if spill:
522
+ line = (f'The first {len(spill)} of these read 88 or above β€” the '
523
+ f'same near-miss range as the cards above, which stop at '
524
+ f'{NEAR_CAP} markets. Below those, a bigger change to the '
525
+ f'record before it reads as a fit. No players here β€” open '
526
+ f'one below to hear it.')
527
+ out.append(f'<div class="sub">{line}</div>')
528
+ out.append('<div class="card flat">')
529
+ for iso, r in rest:
530
+ out.append(bridge_row(iso, r))
531
+ out.append('</div>')
532
+
533
+ # -- the band on the map, under the lists it belongs to -----------------
534
+ picture = band_map(res, band)
535
+ if picture:
536
+ out.append('<h2><span>The band on the map</span></h2>')
537
+ out.append('<div class="card flat">' + picture
538
+ + '<div class="den">Only the markets in the band are '
539
+ 'coloured. Everything else is left blank β€” including the '
540
+ 'markets the track reads closest in, which the saved '
541
+ 'report has.</div></div>')
542
+
543
+ out.append('</div>')
544
+ return "".join(out)