<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://interfaces3.hunter.cryptograss.live/index.php?action=history&amp;feed=atom&amp;title=Emergent%3ABlu%2FPatches%2Fsideman-bandcamp</id>
	<title>Emergent:Blu/Patches/sideman-bandcamp - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://interfaces3.hunter.cryptograss.live/index.php?action=history&amp;feed=atom&amp;title=Emergent%3ABlu%2FPatches%2Fsideman-bandcamp"/>
	<link rel="alternate" type="text/html" href="https://interfaces3.hunter.cryptograss.live/index.php?title=Emergent:Blu/Patches/sideman-bandcamp&amp;action=history"/>
	<updated>2026-11-15T09:44:26Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.43.6</generator>
	<entry>
		<id>https://interfaces3.hunter.cryptograss.live/index.php?title=Emergent:Blu/Patches/sideman-bandcamp&amp;diff=14993&amp;oldid=prev</id>
		<title>Blu: Bandcamp folded into Sideman --album; replaces Emergent:Blu/Patches/bandcamp (#178) (via create-page on MediaWiki MCP Server)</title>
		<link rel="alternate" type="text/html" href="https://interfaces3.hunter.cryptograss.live/index.php?title=Emergent:Blu/Patches/sideman-bandcamp&amp;diff=14993&amp;oldid=prev"/>
		<updated>2026-11-07T19:34:10Z</updated>

		<summary type="html">&lt;p&gt;Bandcamp folded into Sideman --album; replaces Emergent:Blu/Patches/bandcamp (#178) (via create-page on MediaWiki MCP Server)&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;Replaces [[Emergent:Blu/Patches/bandcamp]] (pickipedia#178): the Bandcamp reader folded into Sideman as a third reading in &amp;lt;code&amp;gt;--album&amp;lt;/code&amp;gt;, not a tool of its own (Justin, 7 Nov, block 26,365,089). One commit, on top of [[Emergent:Blu/Patches/ensemble-pipeline]] (#177). sha256 of the file: 10e6bedba35a29058e407dd2286500b2bc5bcd67566f8b3183a2e7966c409187. Round-trip checked on production f44da0e + the Sideman fixes + #177: 79 tests pass. tools/ only, nothing to redeploy. For magent: &amp;lt;code&amp;gt;git am&amp;lt;/code&amp;gt; onto #177&amp;#039;s branch, and close #178.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&amp;lt;nowiki&amp;gt;&lt;br /&gt;
From da23678c6a1242c63459e100520a43bc84504e83 Mon Sep 17 00:00:00 2001&lt;br /&gt;
From: blu &amp;lt;blu@cryptograss.live&amp;gt;&lt;br /&gt;
Date: Sat, 7 Nov 2026 19:33:26 +0000&lt;br /&gt;
Subject: [PATCH] Sideman --album --bandcamp: a record&amp;#039;s Bandcamp page as a&lt;br /&gt;
 third reading&lt;br /&gt;
&lt;br /&gt;
Folds the Bandcamp reader into Sideman instead of a tool of its own&lt;br /&gt;
(Justin: why a new tool?). Bandcamp is one more source answering &amp;quot;who is&lt;br /&gt;
on this record&amp;quot;, so it is one more reading in --album.&lt;br /&gt;
&lt;br /&gt;
Bandcamp&amp;#039;s credits are prose, so the reading carries them word for word&lt;br /&gt;
(credits_text, about_text) and no people: splitting a line into a person&lt;br /&gt;
and an instrument is for the person who writes it up. Bandcamp challenges&lt;br /&gt;
searches, so the page is a URL a person hands in. Fetched with curl, since&lt;br /&gt;
Bandcamp challenges Python&amp;#039;s urllib whatever its headers; an argument list,&lt;br /&gt;
no shell, one request every 1.5 s. main() moves to argparse for the new&lt;br /&gt;
flag. 79 tests.&lt;br /&gt;
---&lt;br /&gt;
 tools/recording_credits.py      | 142 ++++++++++++++++++++++++++++----&lt;br /&gt;
 tools/test_recording_credits.py |  74 ++++++++++++++++-&lt;br /&gt;
 2 files changed, 200 insertions(+), 16 deletions(-)&lt;br /&gt;
&lt;br /&gt;
diff --git a/tools/recording_credits.py b/tools/recording_credits.py&lt;br /&gt;
index 9cc25c9..2c0358f 100644&lt;br /&gt;
--- a/tools/recording_credits.py&lt;br /&gt;
+++ b/tools/recording_credits.py&lt;br /&gt;
@@ -31,13 +31,27 @@ answer is printed under that source, every person with the roles and the&lt;br /&gt;
 scope that source gives them. Nobody is merged across sources; a reader&lt;br /&gt;
 sees two readings of the same record and decides. This is what Rambler&lt;br /&gt;
 reads, and what a person verifies.&lt;br /&gt;
+&lt;br /&gt;
+--bandcamp URL adds a third reading: the record&amp;#039;s Bandcamp page, where small&lt;br /&gt;
+bands put the credits MusicBrainz and Discogs never got. Bandcamp&amp;#039;s credits&lt;br /&gt;
+are prose (&amp;quot;Joe Biglin - keys&amp;quot;, &amp;quot;Recorded at 25 Murray Studios&amp;quot;) with no&lt;br /&gt;
+fields for who or what, so Sideman copies them word for word and parses&lt;br /&gt;
+nothing: splitting a line into a person and an instrument is the judgement&lt;br /&gt;
+of the person who writes it up, with the text in front of them. Bandcamp&lt;br /&gt;
+challenges searches, so the page is a URL a person hands in.&lt;br /&gt;
+&lt;br /&gt;
+    python3 tools/recording_credits.py --album &amp;quot;Milkweed&amp;quot; &amp;quot;Milkweed EP&amp;quot; \&lt;br /&gt;
+        --bandcamp https://milkweed.bandcamp.com/album/milkweed-ep&lt;br /&gt;
 &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
 &lt;br /&gt;
+import argparse&lt;br /&gt;
+import html&lt;br /&gt;
 import json&lt;br /&gt;
 import re&lt;br /&gt;
+import subprocess&lt;br /&gt;
 import sys&lt;br /&gt;
 import time&lt;br /&gt;
-from urllib.parse import urlencode&lt;br /&gt;
+from urllib.parse import urlencode, urlparse&lt;br /&gt;
 from urllib.error import HTTPError&lt;br /&gt;
 from urllib.request import Request, urlopen&lt;br /&gt;
 &lt;br /&gt;
@@ -409,7 +423,76 @@ def discogs_find_releases(artist, title):&lt;br /&gt;
     return _one_per_group_earliest(hits, lambda r: r.get(&amp;quot;master_id&amp;quot;) or r[&amp;quot;id&amp;quot;])&lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;
-def album(artist, title):&lt;br /&gt;
+# ---------------------------------------------------------------------------&lt;br /&gt;
+# Bandcamp: credits copied, never parsed&lt;br /&gt;
+# ---------------------------------------------------------------------------&lt;br /&gt;
+&lt;br /&gt;
+BANDCAMP_UA = &amp;quot;pickipedia-recording-credits/0.1 (https://pickipedia.xyz; Artificial Dumb)&amp;quot;&lt;br /&gt;
+BANDCAMP_INTERVAL = 1.5&lt;br /&gt;
+LD_JSON = re.compile(r&amp;#039;&amp;lt;script type=&amp;quot;application/ld\+json&amp;quot;&amp;gt;\s*(.*?)\s*&amp;lt;/script&amp;gt;&amp;#039;, re.S)&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+class NotABandcampRecord(Exception):&lt;br /&gt;
+    pass&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def is_bandcamp(url):&lt;br /&gt;
+    host = urlparse(url).hostname or &amp;quot;&amp;quot;&lt;br /&gt;
+    return host == &amp;quot;bandcamp.com&amp;quot; or host.endswith(&amp;quot;.bandcamp.com&amp;quot;)&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def bandcamp_get(url):&lt;br /&gt;
+    &amp;quot;&amp;quot;&amp;quot;(final URL, page), fetched with curl, not urllib.&lt;br /&gt;
+&lt;br /&gt;
+    Bandcamp answers Python&amp;#039;s own HTTPS client with a JavaScript challenge&lt;br /&gt;
+    page whatever its headers say, and answers curl with the album (tested&lt;br /&gt;
+    7 Nov 2026, same User-Agent both ways). Same honest User-Agent; an&lt;br /&gt;
+    argument list, no shell.&lt;br /&gt;
+    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
+    wait = BANDCAMP_INTERVAL - (time.monotonic() - _last_call.get(&amp;quot;bandcamp&amp;quot;, 0.0))&lt;br /&gt;
+    if wait &amp;gt; 0:&lt;br /&gt;
+        time.sleep(wait)&lt;br /&gt;
+    try:&lt;br /&gt;
+        out = subprocess.run(&lt;br /&gt;
+            [&amp;quot;curl&amp;quot;, &amp;quot;-sSL&amp;quot;, &amp;quot;--max-time&amp;quot;, &amp;quot;30&amp;quot;, &amp;quot;-A&amp;quot;, BANDCAMP_UA,&lt;br /&gt;
+             &amp;quot;-w&amp;quot;, &amp;quot;\n%{url_effective}&amp;quot;, &amp;quot;--&amp;quot;, url],&lt;br /&gt;
+            capture_output=True, text=True, check=True).stdout&lt;br /&gt;
+    finally:&lt;br /&gt;
+        _last_call[&amp;quot;bandcamp&amp;quot;] = time.monotonic()&lt;br /&gt;
+    page, _, final = out.rpartition(&amp;quot;\n&amp;quot;)&lt;br /&gt;
+    return final, page&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def _bc_text(value):&lt;br /&gt;
+    &amp;quot;&amp;quot;&amp;quot;Bandcamp&amp;#039;s prose as written: entities decoded, line ends made plain.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
+    return html.unescape(value).replace(&amp;quot;\r\n&amp;quot;, &amp;quot;\n&amp;quot;).strip() if value else &amp;quot;&amp;quot;&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def bandcamp_reading(url, get=bandcamp_get):&lt;br /&gt;
+    &amp;quot;&amp;quot;&amp;quot;A Bandcamp album or track page, from the schema.org JSON-LD it carries:&lt;br /&gt;
+    the credits and about text verbatim, and nothing parsed out of them.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
+    final, page = get(url)&lt;br /&gt;
+    m = LD_JSON.search(page)&lt;br /&gt;
+    d = json.loads(m.group(1)) if m else {}&lt;br /&gt;
+    if d.get(&amp;quot;@type&amp;quot;) not in (&amp;quot;MusicAlbum&amp;quot;, &amp;quot;MusicRecording&amp;quot;):&lt;br /&gt;
+        raise NotABandcampRecord(f&amp;quot;{url}: no album or track data on the page &amp;quot;&lt;br /&gt;
+                                 &amp;quot;(an artist page, or Bandcamp&amp;#039;s challenge page)&amp;quot;)&lt;br /&gt;
+    return {&lt;br /&gt;
+        &amp;quot;source&amp;quot;: &amp;quot;Bandcamp&amp;quot;,&lt;br /&gt;
+        &amp;quot;url&amp;quot;: final,&lt;br /&gt;
+        &amp;quot;id&amp;quot;: final,&lt;br /&gt;
+        &amp;quot;group&amp;quot;: f&amp;quot;bandcamp-{final}&amp;quot;,&lt;br /&gt;
+        &amp;quot;artist&amp;quot;: _bc_text((d.get(&amp;quot;byArtist&amp;quot;) or {}).get(&amp;quot;name&amp;quot;)),&lt;br /&gt;
+        &amp;quot;title&amp;quot;: _bc_text(d.get(&amp;quot;name&amp;quot;)),&lt;br /&gt;
+        &amp;quot;date&amp;quot;: re.sub(r&amp;quot;\s+00:00:00 GMT$&amp;quot;, &amp;quot;&amp;quot;, d.get(&amp;quot;datePublished&amp;quot;) or &amp;quot;&amp;quot;),&lt;br /&gt;
+        &amp;quot;tracks&amp;quot;: len((d.get(&amp;quot;track&amp;quot;) or {}).get(&amp;quot;itemListElement&amp;quot;, [])),&lt;br /&gt;
+        &amp;quot;people&amp;quot;: {},&lt;br /&gt;
+        &amp;quot;credits_text&amp;quot;: _bc_text(d.get(&amp;quot;creditText&amp;quot;)),&lt;br /&gt;
+        &amp;quot;about_text&amp;quot;: _bc_text(d.get(&amp;quot;description&amp;quot;)),&lt;br /&gt;
+    }&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def album(artist, title, bandcamp=None, bandcamp_get=bandcamp_get):&lt;br /&gt;
     &amp;quot;&amp;quot;&amp;quot;What each source says about who is on a record, source by source.&lt;br /&gt;
 &lt;br /&gt;
     MusicBrainz and Discogs are searched independently, so a record one has&lt;br /&gt;
@@ -417,6 +500,7 @@ def album(artist, title):&lt;br /&gt;
     release, that Discogs release is read under the MusicBrainz entry rather&lt;br /&gt;
     than found again. Nothing is merged across sources: the same person&lt;br /&gt;
     appears once per source that names them, with what that source says.&lt;br /&gt;
+    A Bandcamp URL, if given, is read last; its credits stay text.&lt;br /&gt;
     &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
     readings = []&lt;br /&gt;
     linked_discogs = set()&lt;br /&gt;
@@ -441,6 +525,8 @@ def album(artist, title):&lt;br /&gt;
     for hit in discogs_find_releases(artist, title):&lt;br /&gt;
         if hit[&amp;quot;id&amp;quot;] not in linked_discogs:&lt;br /&gt;
             readings.append(_discogs_reading(discogs_get(f&amp;quot;releases/{hit[&amp;#039;id&amp;#039;]}&amp;quot;), hit[&amp;quot;id&amp;quot;]))&lt;br /&gt;
+    if bandcamp:&lt;br /&gt;
+        readings.append(bandcamp_reading(bandcamp, get=bandcamp_get))&lt;br /&gt;
     return readings&lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;
@@ -464,6 +550,16 @@ def render_album(artist, title, readings):&lt;br /&gt;
     if not readings:&lt;br /&gt;
         lines.append(&amp;quot;Neither MusicBrainz nor Discogs has a release with this title and artist.&amp;quot;)&lt;br /&gt;
     for r in readings:&lt;br /&gt;
+        if r[&amp;quot;source&amp;quot;] == &amp;quot;Bandcamp&amp;quot;:&lt;br /&gt;
+            lines.append(f&amp;#039;Bandcamp: {r[&amp;quot;artist&amp;quot;]}: {r[&amp;quot;title&amp;quot;]} ({r[&amp;quot;date&amp;quot;] or &amp;quot;undated&amp;quot;}), {r[&amp;quot;tracks&amp;quot;]} tracks&amp;#039;)&lt;br /&gt;
+            lines.append(f&amp;#039;  {r[&amp;quot;url&amp;quot;]}&amp;#039;)&lt;br /&gt;
+            lines.append(&amp;quot;  Credits, word for word (not parsed; a person reads who played what):&amp;quot;)&lt;br /&gt;
+            lines += [f&amp;quot;    {ln}&amp;quot; for ln in r[&amp;quot;credits_text&amp;quot;].splitlines()] or [&amp;quot;    (none)&amp;quot;]&lt;br /&gt;
+            if r[&amp;quot;about_text&amp;quot;]:&lt;br /&gt;
+                lines.append(&amp;quot;  About, word for word:&amp;quot;)&lt;br /&gt;
+                lines += [f&amp;quot;    {ln}&amp;quot; for ln in r[&amp;quot;about_text&amp;quot;].splitlines()]&lt;br /&gt;
+            lines.append(&amp;quot;&amp;quot;)&lt;br /&gt;
+            continue&lt;br /&gt;
         linked = f&amp;#039;, linked from MusicBrainz release {r[&amp;quot;linked_to&amp;quot;]}&amp;#039; if r.get(&amp;quot;linked_to&amp;quot;) else &amp;quot;&amp;quot;&lt;br /&gt;
         lines.append(f&amp;#039;{r[&amp;quot;source&amp;quot;]}: {r[&amp;quot;artist&amp;quot;]}: {r[&amp;quot;title&amp;quot;]} ({r[&amp;quot;date&amp;quot;] or &amp;quot;undated&amp;quot;}), &amp;#039;&lt;br /&gt;
                      f&amp;#039;{r[&amp;quot;tracks&amp;quot;]} tracks{linked}&amp;#039;)&lt;br /&gt;
@@ -511,21 +607,37 @@ def _jsonable(o):&lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;
 def main(argv):&lt;br /&gt;
-    flags = {a for a in argv[1:] if a.startswith(&amp;quot;--&amp;quot;)}&lt;br /&gt;
-    args = [a for a in argv[1:] if not a.startswith(&amp;quot;--&amp;quot;)]&lt;br /&gt;
-    if len(args) != 2 or flags - {&amp;quot;--json&amp;quot;, &amp;quot;--album&amp;quot;}:&lt;br /&gt;
-        print(__doc__.strip().splitlines()[0])&lt;br /&gt;
-        print(&amp;#039;usage: recording_credits.py [--json] &amp;quot;ARTIST&amp;quot; &amp;quot;SONG TITLE&amp;quot;&amp;#039;)&lt;br /&gt;
-        print(&amp;#039;       recording_credits.py [--json] --album &amp;quot;ARTIST&amp;quot; &amp;quot;RECORD TITLE&amp;quot;&amp;#039;)&lt;br /&gt;
+    ap = argparse.ArgumentParser(&lt;br /&gt;
+        description=__doc__.strip().splitlines()[0],&lt;br /&gt;
+        usage=&amp;#039;%(prog)s [--json] &amp;quot;ARTIST&amp;quot; &amp;quot;SONG TITLE&amp;quot;\n&amp;#039;&lt;br /&gt;
+              &amp;#039;       %(prog)s [--json] --album &amp;quot;ARTIST&amp;quot; &amp;quot;RECORD TITLE&amp;quot; [--bandcamp URL]&amp;#039;)&lt;br /&gt;
+    ap.add_argument(&amp;quot;artist&amp;quot;)&lt;br /&gt;
+    ap.add_argument(&amp;quot;title&amp;quot;)&lt;br /&gt;
+    ap.add_argument(&amp;quot;--album&amp;quot;, action=&amp;quot;store_true&amp;quot;, help=&amp;quot;who is on a whole record, source by source&amp;quot;)&lt;br /&gt;
+    ap.add_argument(&amp;quot;--bandcamp&amp;quot;, metavar=&amp;quot;URL&amp;quot;, help=&amp;quot;with --album: the record&amp;#039;s Bandcamp page, read too&amp;quot;)&lt;br /&gt;
+    ap.add_argument(&amp;quot;--json&amp;quot;, action=&amp;quot;store_true&amp;quot;)&lt;br /&gt;
+    try:&lt;br /&gt;
+        args = ap.parse_args(argv[1:])&lt;br /&gt;
+    except SystemExit as e:&lt;br /&gt;
+        return e.code&lt;br /&gt;
+    if args.bandcamp and not args.album:&lt;br /&gt;
+        ap.print_usage()&lt;br /&gt;
+        print(&amp;quot;--bandcamp goes with --album&amp;quot;)&lt;br /&gt;
         return 2&lt;br /&gt;
-    artist, title = args&lt;br /&gt;
-    if &amp;quot;--album&amp;quot; in flags:&lt;br /&gt;
-        readings = album(artist, title)&lt;br /&gt;
-        print(json.dumps(_jsonable(readings), indent=2, ensure_ascii=False) if &amp;quot;--json&amp;quot; in flags&lt;br /&gt;
-              else render_album(artist, title, readings))&lt;br /&gt;
+    if args.bandcamp and not is_bandcamp(args.bandcamp):&lt;br /&gt;
+        print(f&amp;quot;not a bandcamp.com URL: {args.bandcamp}&amp;quot;)&lt;br /&gt;
+        return 2&lt;br /&gt;
+    if args.album:&lt;br /&gt;
+        try:&lt;br /&gt;
+            readings = album(args.artist, args.title, bandcamp=args.bandcamp)&lt;br /&gt;
+        except NotABandcampRecord as e:&lt;br /&gt;
+            print(e, file=sys.stderr)&lt;br /&gt;
+            return 1&lt;br /&gt;
+        print(json.dumps(_jsonable(readings), indent=2, ensure_ascii=False) if args.json&lt;br /&gt;
+              else render_album(args.artist, args.title, readings))&lt;br /&gt;
     else:&lt;br /&gt;
-        versions = lookup(artist, title)&lt;br /&gt;
-        print(json.dumps(versions, indent=2) if &amp;quot;--json&amp;quot; in flags else render(artist, title, versions))&lt;br /&gt;
+        versions = lookup(args.artist, args.title)&lt;br /&gt;
+        print(json.dumps(versions, indent=2) if args.json else render(args.artist, args.title, versions))&lt;br /&gt;
     return 0&lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;
diff --git a/tools/test_recording_credits.py b/tools/test_recording_credits.py&lt;br /&gt;
index bfe6a7e..41a0c18 100644&lt;br /&gt;
--- a/tools/test_recording_credits.py&lt;br /&gt;
+++ b/tools/test_recording_credits.py&lt;br /&gt;
@@ -8,6 +8,8 @@ on canned documents shaped like the real MusicBrainz and Discogs responses,&lt;br /&gt;
 so no test touches the network.&lt;br /&gt;
 &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
 &lt;br /&gt;
+import json&lt;br /&gt;
+&lt;br /&gt;
 import pytest&lt;br /&gt;
 &lt;br /&gt;
 import recording_credits as rc&lt;br /&gt;
@@ -336,7 +338,77 @@ def test_render_album_says_when_nobody_has_it():&lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;
 def test_main_album_flag(monkeypatch, capsys):&lt;br /&gt;
-    monkeypatch.setattr(rc, &amp;quot;album&amp;quot;, lambda a, t: [])&lt;br /&gt;
+    monkeypatch.setattr(rc, &amp;quot;album&amp;quot;, lambda a, t, bandcamp=None: [])&lt;br /&gt;
     assert rc.main([&amp;quot;x&amp;quot;, &amp;quot;--album&amp;quot;, &amp;quot;A&amp;quot;, &amp;quot;T&amp;quot;]) == 0&lt;br /&gt;
     assert &amp;quot;Neither MusicBrainz nor Discogs&amp;quot; in capsys.readouterr().out&lt;br /&gt;
     assert rc.main([&amp;quot;x&amp;quot;, &amp;quot;--bogus&amp;quot;, &amp;quot;A&amp;quot;, &amp;quot;T&amp;quot;]) == 2&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+# ---------------------------------------------------------------------------&lt;br /&gt;
+# --album --bandcamp: credits copied, never parsed&lt;br /&gt;
+# ---------------------------------------------------------------------------&lt;br /&gt;
+&lt;br /&gt;
+def bc_page(obj):&lt;br /&gt;
+    return &amp;#039;&amp;lt;html&amp;gt;&amp;lt;script type=&amp;quot;application/ld+json&amp;quot;&amp;gt;\n&amp;#039; + json.dumps(obj) + &amp;quot;\n&amp;lt;/script&amp;gt;&amp;lt;/html&amp;gt;&amp;quot;&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+BC_ALBUM = {&lt;br /&gt;
+    &amp;quot;@type&amp;quot;: &amp;quot;MusicAlbum&amp;quot;, &amp;quot;name&amp;quot;: &amp;quot;Workin&amp;amp;#39;s Too Hard&amp;quot;, &amp;quot;byArtist&amp;quot;: {&amp;quot;name&amp;quot;: &amp;quot;Rayna Gellert&amp;quot;},&lt;br /&gt;
+    &amp;quot;datePublished&amp;quot;: &amp;quot;20 Jan 2017 00:00:00 GMT&amp;quot;,&lt;br /&gt;
+    &amp;quot;creditText&amp;quot;: &amp;quot;Musicians: Rayna Gellert, Kieran Kane\r\nJoe - keys &amp;amp;amp; vocals&amp;quot;,&lt;br /&gt;
+    &amp;quot;description&amp;quot;: &amp;quot;Cut live in a room.&amp;quot;,&lt;br /&gt;
+    &amp;quot;track&amp;quot;: {&amp;quot;itemListElement&amp;quot;: [{&amp;quot;position&amp;quot;: 1, &amp;quot;item&amp;quot;: {&amp;quot;name&amp;quot;: &amp;quot;Starch&amp;quot;}}]},&lt;br /&gt;
+}&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def test_bandcamp_credits_are_copied_not_parsed():&lt;br /&gt;
+    r = rc.bandcamp_reading(&amp;quot;https://x.bandcamp.com/album/w&amp;quot;,&lt;br /&gt;
+                            get=lambda url: (url, bc_page(BC_ALBUM)))&lt;br /&gt;
+    assert r[&amp;quot;source&amp;quot;] == &amp;quot;Bandcamp&amp;quot; and r[&amp;quot;url&amp;quot;] == &amp;quot;https://x.bandcamp.com/album/w&amp;quot;&lt;br /&gt;
+    assert r[&amp;quot;credits_text&amp;quot;] == &amp;quot;Musicians: Rayna Gellert, Kieran Kane\nJoe - keys &amp;amp; vocals&amp;quot;&lt;br /&gt;
+    assert r[&amp;quot;title&amp;quot;] == &amp;quot;Workin&amp;#039;s Too Hard&amp;quot; and r[&amp;quot;date&amp;quot;] == &amp;quot;20 Jan 2017&amp;quot; and r[&amp;quot;tracks&amp;quot;] == 1&lt;br /&gt;
+    assert r[&amp;quot;people&amp;quot;] == {}  # nobody is extracted from prose&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def test_bandcamp_challenge_page_is_an_error():&lt;br /&gt;
+    with pytest.raises(rc.NotABandcampRecord):&lt;br /&gt;
+        rc.bandcamp_reading(&amp;quot;https://x.bandcamp.com/album/w&amp;quot;,&lt;br /&gt;
+                            get=lambda url: (url, &amp;quot;&amp;lt;title&amp;gt;Client Challenge&amp;lt;/title&amp;gt;&amp;quot;))&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def test_album_reads_bandcamp_last_and_render_says_not_parsed(monkeypatch):&lt;br /&gt;
+    monkeypatch.setattr(rc, &amp;quot;find_releases&amp;quot;, lambda a, t: [])&lt;br /&gt;
+    monkeypatch.setattr(rc, &amp;quot;discogs_find_releases&amp;quot;, lambda a, t: [])&lt;br /&gt;
+    readings = rc.album(&amp;quot;Rayna Gellert&amp;quot;, &amp;quot;Workin&amp;#039;s Too Hard&amp;quot;, bandcamp=&amp;quot;https://x.bandcamp.com/album/w&amp;quot;,&lt;br /&gt;
+                        bandcamp_get=lambda url: (url, bc_page(BC_ALBUM)))&lt;br /&gt;
+    assert [r[&amp;quot;source&amp;quot;] for r in readings] == [&amp;quot;Bandcamp&amp;quot;]&lt;br /&gt;
+    text = rc.render_album(&amp;quot;Rayna Gellert&amp;quot;, &amp;quot;Workin&amp;#039;s Too Hard&amp;quot;, readings)&lt;br /&gt;
+    assert &amp;quot;Credits, word for word (not parsed&amp;quot; in text&lt;br /&gt;
+    assert &amp;quot;    Joe - keys &amp;amp; vocals&amp;quot; in text&lt;br /&gt;
+    assert &amp;quot;  About, word for word:\n    Cut live in a room.&amp;quot; in text&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def test_bandcamp_fetch_is_curl_with_an_argument_list(monkeypatch):&lt;br /&gt;
+    seen = {}&lt;br /&gt;
+&lt;br /&gt;
+    def run(cmd, **kw):&lt;br /&gt;
+        seen[&amp;quot;cmd&amp;quot;] = cmd&lt;br /&gt;
+        return rc.subprocess.CompletedProcess(cmd, 0, stdout=&amp;quot;&amp;lt;html/&amp;gt;\nhttps://x.bandcamp.com/album/w&amp;quot;)&lt;br /&gt;
+    monkeypatch.setattr(rc.subprocess, &amp;quot;run&amp;quot;, run)&lt;br /&gt;
+    monkeypatch.setattr(rc, &amp;quot;BANDCAMP_INTERVAL&amp;quot;, 0)&lt;br /&gt;
+    assert rc.bandcamp_get(&amp;quot;https://x.bandcamp.com/album/w&amp;quot;) == (&amp;quot;https://x.bandcamp.com/album/w&amp;quot;, &amp;quot;&amp;lt;html/&amp;gt;&amp;quot;)&lt;br /&gt;
+    assert seen[&amp;quot;cmd&amp;quot;][0] == &amp;quot;curl&amp;quot; and seen[&amp;quot;cmd&amp;quot;][-2:] == [&amp;quot;--&amp;quot;, &amp;quot;https://x.bandcamp.com/album/w&amp;quot;]&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+@pytest.mark.parametrize(&amp;quot;url,ok&amp;quot;, [&lt;br /&gt;
+    (&amp;quot;https://milkweed.bandcamp.com/album/milkweed-ep&amp;quot;, True),&lt;br /&gt;
+    (&amp;quot;https://evilbandcamp.com/album/x&amp;quot;, False),&lt;br /&gt;
+    (&amp;quot;https://bandcamp.com.example.org/album/x&amp;quot;, False),&lt;br /&gt;
+])&lt;br /&gt;
+def test_is_bandcamp(url, ok):&lt;br /&gt;
+    assert rc.is_bandcamp(url) is ok&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def test_main_bandcamp_needs_album_and_a_bandcamp_url():&lt;br /&gt;
+    assert rc.main([&amp;quot;x&amp;quot;, &amp;quot;A&amp;quot;, &amp;quot;T&amp;quot;, &amp;quot;--bandcamp&amp;quot;, &amp;quot;https://x.bandcamp.com/album/w&amp;quot;]) == 2&lt;br /&gt;
+    assert rc.main([&amp;quot;x&amp;quot;, &amp;quot;--album&amp;quot;, &amp;quot;A&amp;quot;, &amp;quot;T&amp;quot;, &amp;quot;--bandcamp&amp;quot;, &amp;quot;https://example.org/album/w&amp;quot;]) == 2&lt;br /&gt;
-- &lt;br /&gt;
2.53.0&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/nowiki&amp;gt;&amp;lt;/pre&amp;gt;&lt;/div&gt;</summary>
		<author><name>Blu</name></author>
	</entry>
</feed>