Spaces:
Running
Running
prototype: Within reach tab (78-93 band dive-in)
Browse files- app.py +70 -0
- 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)
|