fix bandcamp login
This commit is contained in:
+106
-13
@@ -43,6 +43,68 @@ def _spotify_write(data: dict):
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
|
||||
# ── Bandcamp cookie auth ─────────────────────────────────────────────────────
|
||||
|
||||
def _apply_cookies(session, cookie_str: str):
|
||||
for part in cookie_str.split(';'):
|
||||
part = part.strip()
|
||||
if '=' in part:
|
||||
name, _, value = part.partition('=')
|
||||
session.cookies.set(name.strip(), value.strip(), domain='.bandcamp.com')
|
||||
|
||||
|
||||
def _bc_verify_cookies(cookie_str: str) -> dict:
|
||||
session = _requests.Session()
|
||||
session.headers["User-Agent"] = (
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
|
||||
"(KHTML, like Gecko) Chrome/124.0 Safari/537.36"
|
||||
)
|
||||
_apply_cookies(session, cookie_str)
|
||||
|
||||
# Try js_account_details cookie first (avoids an HTTP round-trip)
|
||||
fan_id = None
|
||||
username = ''
|
||||
js_raw = session.cookies.get('js_account_details', '')
|
||||
if js_raw:
|
||||
try:
|
||||
data = json.loads(unquote(js_raw))
|
||||
fan_id = data.get('fan_id') or data.get('id')
|
||||
username = data.get('username', '')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fall back to fetching the homepage
|
||||
if not fan_id:
|
||||
try:
|
||||
r = session.get("https://bandcamp.com/", timeout=15)
|
||||
m = re.search(r'"fan_id"\s*:\s*(\d+)', r.text)
|
||||
if m:
|
||||
fan_id = int(m.group(1))
|
||||
if not username:
|
||||
m2 = re.search(r'"username"\s*:\s*"([^"]+)"', r.text)
|
||||
if m2:
|
||||
username = m2.group(1)
|
||||
except Exception as exc:
|
||||
return {"ok": False, "error": f"Could not reach bandcamp.com: {exc}"}
|
||||
|
||||
if not fan_id:
|
||||
return {"ok": False, "error": "Cookies invalid or expired — no fan account found"}
|
||||
|
||||
# Verify with a lightweight API call
|
||||
try:
|
||||
r = session.post(
|
||||
"https://bandcamp.com/api/fan/2/collection_summary",
|
||||
json={"fan_id": fan_id},
|
||||
timeout=15,
|
||||
)
|
||||
if not r.ok:
|
||||
return {"ok": False, "error": "Cookies rejected by Bandcamp (may have expired)"}
|
||||
except Exception as exc:
|
||||
return {"ok": False, "error": str(exc)}
|
||||
|
||||
return {"ok": True, "username": username}
|
||||
|
||||
|
||||
# ── Bandcamp login ────────────────────────────────────────────────────────────
|
||||
|
||||
def _bc_login(email: str, password: str) -> dict:
|
||||
@@ -52,33 +114,41 @@ def _bc_login(email: str, password: str) -> dict:
|
||||
"(KHTML, like Gecko) Chrome/124.0 Safari/537.36"
|
||||
)
|
||||
|
||||
# Step 1 — obtain CSRF token
|
||||
# Step 1 — obtain CSRF token from meta tag
|
||||
try:
|
||||
r = session.get("https://bandcamp.com/login", timeout=15)
|
||||
except Exception as exc:
|
||||
return {"ok": False, "error": f"Could not reach bandcamp.com: {exc}"}
|
||||
|
||||
csrf = unquote(r.cookies.get("csrf_token", ""))
|
||||
if not csrf:
|
||||
m = re.search(r'"csrf_token"\s*:\s*"([^"]+)"', r.text)
|
||||
if m:
|
||||
csrf = unquote(m.group(1))
|
||||
if not csrf:
|
||||
m = re.search(r'<meta\s+name=["\']csrf-token["\']\s+content=["\']([^"\']+)["\']', r.text, re.IGNORECASE)
|
||||
if not m:
|
||||
m = re.search(r'<meta\s+content=["\']([^"\']+)["\']\s+name=["\']csrf-token["\']', r.text, re.IGNORECASE)
|
||||
if not m:
|
||||
return {"ok": False, "error": "Could not obtain CSRF token from Bandcamp"}
|
||||
csrf = m.group(1)
|
||||
|
||||
# Step 2 — POST login
|
||||
# Step 2 — POST login using field names from Bandcamp's login JS
|
||||
try:
|
||||
r = session.post(
|
||||
"https://bandcamp.com/login_cb",
|
||||
data={"username": email, "password": password, "csrf_token": csrf},
|
||||
data={
|
||||
"user.name": email,
|
||||
"login.password": password,
|
||||
"login.twofactor": "",
|
||||
"login.twofactor_remember": "",
|
||||
"login.from": "",
|
||||
"to_band_path": "",
|
||||
},
|
||||
headers={
|
||||
"X-CSRFToken": csrf,
|
||||
"X-CSRF-TOKEN": csrf,
|
||||
"Referer": "https://bandcamp.com/login",
|
||||
"Accept": "application/json, text/javascript, */*",
|
||||
"Origin": "https://bandcamp.com",
|
||||
},
|
||||
timeout=15,
|
||||
)
|
||||
if not r.text:
|
||||
return {"ok": False, "error": "Empty response from Bandcamp"}
|
||||
payload = r.json()
|
||||
except Exception as exc:
|
||||
return {"ok": False, "error": str(exc)}
|
||||
@@ -87,7 +157,16 @@ def _bc_login(email: str, password: str) -> dict:
|
||||
username = (payload.get("account") or {}).get("username", "")
|
||||
return {"ok": True, "username": username}
|
||||
|
||||
err = payload.get("error") or payload.get("error_message") or "Login failed"
|
||||
# Extract error from various response shapes
|
||||
err = payload.get("error") or payload.get("error_message")
|
||||
if not err and payload.get("errors"):
|
||||
errors = payload["errors"]
|
||||
if isinstance(errors, list):
|
||||
parts = [f"{e.get('field','')}: {e.get('reason','')}" for e in errors if isinstance(e, dict)]
|
||||
err = "; ".join(parts) if parts else str(errors)
|
||||
else:
|
||||
err = str(errors)
|
||||
err = err or "Login failed"
|
||||
return {"ok": False, "error": err}
|
||||
|
||||
|
||||
@@ -113,7 +192,11 @@ class _Handler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
if self.path in ("/credentials", "/credentials/"):
|
||||
cfg = _read()
|
||||
self._json(200, {"email": cfg.get("email", ""), "password_set": bool(cfg.get("password"))})
|
||||
self._json(200, {
|
||||
"email": cfg.get("email", ""),
|
||||
"password_set": bool(cfg.get("password")),
|
||||
"cookies_set": bool(cfg.get("cookies")),
|
||||
})
|
||||
elif self.path in ("/spotify/credentials", "/spotify/credentials/"):
|
||||
cfg = _spotify_read()
|
||||
self._json(200, {
|
||||
@@ -139,15 +222,25 @@ class _Handler(BaseHTTPRequestHandler):
|
||||
cfg["email"] = body["email"].strip()
|
||||
if "password" in body and body["password"]:
|
||||
cfg["password"] = body["password"]
|
||||
if "cookies" in body:
|
||||
if body["cookies"]:
|
||||
cfg["cookies"] = body["cookies"].strip()
|
||||
else:
|
||||
cfg.pop("cookies", None)
|
||||
_write(cfg)
|
||||
self._json(200, {"ok": True})
|
||||
|
||||
elif self.path in ("/test-login", "/test-login/"):
|
||||
cfg = _read()
|
||||
cookies = cfg.get("cookies", "")
|
||||
if cookies:
|
||||
result = _bc_verify_cookies(cookies)
|
||||
self._json(200, result)
|
||||
return
|
||||
email = cfg.get("email", "")
|
||||
password = cfg.get("password", "")
|
||||
if not email or not password:
|
||||
self._json(400, {"ok": False, "error": "No credentials saved — save them first"})
|
||||
self._json(400, {"ok": False, "error": "No credentials saved — save cookies or email/password first"})
|
||||
return
|
||||
result = _bc_login(email, password)
|
||||
self._json(200, result)
|
||||
|
||||
Reference in New Issue
Block a user