fix bandcamp login

This commit is contained in:
2026-05-17 19:15:03 +02:00
parent 8393271ee3
commit 560d95cfc0
3 changed files with 258 additions and 63 deletions
+87 -31
View File
@@ -45,22 +45,21 @@ SESSION.headers["User-Agent"] = (
# ── Credentials ───────────────────────────────────────────────────────────────
def load_credentials() -> tuple[str, str]:
def load_credentials() -> tuple[str, str, str]:
"""Returns (email, password, cookies). Cookies take priority when present."""
email = os.environ.get("BANDCAMP_EMAIL", "")
password = os.environ.get("BANDCAMP_PASSWORD", "")
cookies = ""
if email and password:
return email, password
return email, password, cookies
try:
r = requests.get("http://localhost:8091/credentials", timeout=3)
if r.ok:
data = r.json()
api_email = data.get("email", "")
# password_set=True but we can't retrieve the plaintext from GET;
# the service exposes /test-login but not the raw password.
# Fall through to the JSON file which stores plaintext.
if api_email:
email = api_email
if data.get("email"):
email = data["email"]
except Exception:
pass
@@ -70,19 +69,20 @@ def load_credentials() -> tuple[str, str]:
cfg = json.load(f)
email = cfg.get("email", email)
password = cfg.get("password", password)
cookies = cfg.get("cookies", cookies)
# Also try the Docker volume path (running on host alongside Docker)
docker_cfg = Path("/var/lib/docker/volumes/audioserver_bandcamp_config/_data/bandcamp.json")
if not password and docker_cfg.exists():
if not password and not cookies and docker_cfg.exists():
try:
with open(docker_cfg) as f:
cfg = json.load(f)
email = cfg.get("email", email)
password = cfg.get("password", password)
cookies = cfg.get("cookies", cookies)
except Exception:
pass
return email, password
return email, password, cookies
# ── Bandcamp auth ─────────────────────────────────────────────────────────────
@@ -90,20 +90,26 @@ def load_credentials() -> tuple[str, str]:
def bc_login(email: str, password: str) -> dict:
"""Login to Bandcamp. Returns account dict with fan_id."""
r = SESSION.get("https://bandcamp.com/login", timeout=15)
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:
raise RuntimeError("Could not get CSRF token from Bandcamp login page")
csrf = m.group(1)
time.sleep(REQUEST_DELAY)
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",
@@ -112,12 +118,53 @@ def bc_login(email: str, password: str) -> dict:
)
payload = r.json()
if payload.get("ok") != 1:
err = payload.get("error") or payload.get("error_message") or json.dumps(payload)
errors = payload.get("errors")
if errors and isinstance(errors, list):
err = "; ".join(f"{e.get('field','')}: {e.get('reason','')}" for e in errors if isinstance(e, dict))
else:
err = payload.get("error") or payload.get("error_message") or json.dumps(payload)
raise RuntimeError(f"Bandcamp login failed: {err}")
return payload.get("account", {})
def apply_cookies(session: requests.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 get_session_info(session: requests.Session) -> tuple[int, str]:
"""Returns (fan_id, username) from session cookies or homepage."""
js_raw = session.cookies.get('js_account_details', '')
fan_id = None
username = ''
if js_raw:
try:
data = json.loads(unquote(js_raw))
fan_id = data.get('fan_id') or data.get('id')
username = data.get('username', '')
if fan_id:
return int(fan_id), username
except Exception:
pass
time.sleep(REQUEST_DELAY)
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)
if not fan_id:
raise RuntimeError("Could not determine fan_id from session cookies — they may have expired")
return fan_id, username
def get_fan_id(account: dict) -> int:
fan_id = account.get("fan_id") or account.get("id")
if fan_id:
@@ -317,18 +364,27 @@ def main():
music_dir = args.music_dir
# Load credentials
email, password = load_credentials()
if not email or not password:
print("✗ No credentials found. Save them in the Settings → Bandcamp section, or set")
print(" BANDCAMP_EMAIL / BANDCAMP_PASSWORD environment variables.")
sys.exit(1)
email, password, cookies = load_credentials()
print(f"Logging in as {email}")
account = bc_login(email, password)
fan_id = get_fan_id(account)
username = account.get("username", str(fan_id))
print(f"✓ Logged in — {username} (fan_id={fan_id})")
if cookies:
apply_cookies(SESSION, cookies)
print("Using saved session cookies…")
try:
fan_id, username = get_session_info(SESSION)
print(f"✓ Session active — {username or 'unknown'} (fan_id={fan_id})")
except RuntimeError as exc:
print(f"{exc}")
sys.exit(1)
elif email and password:
print(f"Logging in as {email}")
account = bc_login(email, password)
fan_id = get_fan_id(account)
username = account.get("username", str(fan_id))
print(f"✓ Logged in — {username} (fan_id={fan_id})")
else:
print("✗ No credentials found. Save session cookies or email/password in Settings → Bandcamp,")
print(" or set BANDCAMP_EMAIL / BANDCAMP_PASSWORD environment variables.")
sys.exit(1)
print("Fetching collection…")
items = fetch_collection(fan_id)