Skip to content

Commit d414eab

Browse files
committed
Fix Bytelixir collector: use session cookie (hCaptcha blocks login)
1 parent 0ca2d6f commit d414eab

3 files changed

Lines changed: 87 additions & 96 deletions

File tree

app/collectors/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
"earnfm": ["email", "password"],
5858
"packetstream": ["auth_token"],
5959
"grass": ["access_token"],
60-
"bytelixir": ["email", "password"],
60+
"bytelixir": ["session_cookie"],
6161
}
6262

6363

app/collectors/bytelixir.py

Lines changed: 82 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
"""Bytelixir earnings collector.
22
3-
Authenticates via email/password to the Bytelixir dashboard API and
4-
fetches the current balance.
3+
Uses session cookie from the browser to fetch balance. Bytelixir is a
4+
Laravel app with hCaptcha on login, so automated email/password login
5+
is not possible. Users must extract session cookies from their browser.
6+
7+
To get the cookie: open dash.bytelixir.com, log in (tick "Remember Me"),
8+
press F12 > Application > Cookies, and copy the `bytelixir_session` value.
59
"""
610

711
from __future__ import annotations
812

913
import logging
14+
import re
1015

1116
import httpx
1217

@@ -18,78 +23,33 @@
1823

1924

2025
class BytelixirCollector(BaseCollector):
21-
"""Collect earnings from Bytelixir's dashboard."""
26+
"""Collect earnings from Bytelixir using a session cookie."""
2227

2328
platform = "bytelixir"
2429

25-
def __init__(self, email: str, password: str) -> None:
26-
self.email = email
27-
self.password = password
28-
self._token: str | None = None
29-
30-
async def _authenticate(self, client: httpx.AsyncClient) -> str:
31-
"""Log in and obtain a session/token."""
32-
resp = await client.post(
33-
f"{DASH_BASE}/api/auth/login",
34-
json={"email": self.email, "password": self.password},
35-
headers={
36-
"User-Agent": "Mozilla/5.0",
37-
"Origin": DASH_BASE,
38-
"Referer": f"{DASH_BASE}/",
39-
},
40-
)
41-
resp.raise_for_status()
42-
data = resp.json()
43-
44-
# Try common token locations in response
45-
token = (
46-
data.get("token")
47-
or data.get("access_token")
48-
or data.get("data", {}).get("token", "")
49-
or data.get("data", {}).get("access_token", "")
50-
)
51-
if not token:
52-
raise ValueError(f"No token in Bytelixir login response (keys: {list(data.keys())})")
53-
return token
30+
def __init__(self, session_cookie: str) -> None:
31+
self.session_cookie = session_cookie
5432

5533
async def collect(self) -> EarningsResult:
5634
"""Fetch current Bytelixir balance."""
5735
try:
58-
async with httpx.AsyncClient(timeout=30, follow_redirects=True) as client:
59-
if not self._token:
60-
self._token = await self._authenticate(client)
61-
62-
headers = {
63-
"Authorization": f"Bearer {self._token}",
64-
"User-Agent": "Mozilla/5.0",
65-
"Accept": "application/json",
66-
"Origin": DASH_BASE,
67-
"Referer": f"{DASH_BASE}/",
68-
}
69-
70-
# Try the dashboard/earnings endpoints
71-
for path in (
72-
"/api/user/balance",
73-
"/api/user/earnings",
74-
"/api/dashboard",
75-
"/api/user",
76-
"/api/me",
77-
):
78-
resp = await client.get(
79-
f"{DASH_BASE}{path}",
80-
headers=headers,
81-
)
36+
cookies = {"bytelixir_session": self.session_cookie}
37+
headers = {
38+
"User-Agent": "Mozilla/5.0",
39+
"Accept": "application/json, text/html",
40+
"X-Requested-With": "XMLHttpRequest",
41+
"Referer": f"{DASH_BASE}/",
42+
}
8243

83-
# Token expired — retry auth once
84-
if resp.status_code == 401:
85-
self._token = await self._authenticate(client)
86-
headers["Authorization"] = f"Bearer {self._token}"
87-
resp = await client.get(
88-
f"{DASH_BASE}{path}",
89-
headers=headers,
90-
)
44+
async with httpx.AsyncClient(timeout=30, follow_redirects=True, cookies=cookies) as client:
45+
# Try the JSON API first
46+
resp = await client.get(
47+
f"{DASH_BASE}/api/v1/user",
48+
headers=headers,
49+
)
9150

92-
if resp.status_code == 200:
51+
if resp.status_code == 200:
52+
try:
9353
data = resp.json()
9454
balance = _extract_balance(data)
9555
if balance is not None:
@@ -98,11 +58,36 @@ async def collect(self) -> EarningsResult:
9858
balance=round(balance, 4),
9959
currency="USD",
10060
)
61+
except Exception:
62+
pass
63+
64+
# Fallback: scrape the dashboard HTML for data-balance
65+
resp = await client.get(
66+
f"{DASH_BASE}/en",
67+
headers={**headers, "Accept": "text/html"},
68+
)
69+
70+
if resp.status_code == 200:
71+
balance = _extract_balance_from_html(resp.text)
72+
if balance is not None:
73+
return EarningsResult(
74+
platform=self.platform,
75+
balance=round(balance, 4),
76+
currency="USD",
77+
)
78+
79+
# Check if session expired (redirected to login)
80+
if "/sign-in" in str(resp.url):
81+
return EarningsResult(
82+
platform=self.platform,
83+
balance=0.0,
84+
error="Session expired — get a new bytelixir_session cookie from your browser",
85+
)
10186

10287
return EarningsResult(
10388
platform=self.platform,
10489
balance=0.0,
105-
error="Could not find balance in Bytelixir API responses",
90+
error="Could not extract balance from Bytelixir dashboard",
10691
)
10792
except Exception as exc:
10893
logger.error("Bytelixir collection failed: %s", exc)
@@ -114,8 +99,7 @@ async def collect(self) -> EarningsResult:
11499

115100

116101
def _extract_balance(data: dict) -> float | None:
117-
"""Try to extract a USD balance from various response shapes."""
118-
# Direct balance field
102+
"""Try to extract a USD balance from JSON response."""
119103
for key in ("balance", "total_balance", "earnings", "total_earnings"):
120104
val = data.get(key)
121105
if val is not None:
@@ -124,26 +108,35 @@ def _extract_balance(data: dict) -> float | None:
124108
except (ValueError, TypeError):
125109
continue
126110

127-
# Nested under 'data'
128-
inner = data.get("data", {})
129-
if isinstance(inner, dict):
130-
for key in ("balance", "total_balance", "earnings", "total_earnings"):
131-
val = inner.get(key)
132-
if val is not None:
133-
try:
134-
return float(val)
135-
except (ValueError, TypeError):
136-
continue
137-
138-
# Nested under 'user'
139-
user = data.get("user", {})
140-
if isinstance(user, dict):
141-
for key in ("balance", "earnings"):
142-
val = user.get(key)
143-
if val is not None:
144-
try:
145-
return float(val)
146-
except (ValueError, TypeError):
147-
continue
111+
# Nested under 'data' or 'user'
112+
for wrapper in ("data", "user"):
113+
inner = data.get(wrapper, {})
114+
if isinstance(inner, dict):
115+
for key in ("balance", "total_balance", "earnings", "total_earnings"):
116+
val = inner.get(key)
117+
if val is not None:
118+
try:
119+
return float(val)
120+
except (ValueError, TypeError):
121+
continue
122+
return None
123+
124+
125+
def _extract_balance_from_html(html: str) -> float | None:
126+
"""Extract balance from data-balance attribute in dashboard HTML."""
127+
match = re.search(r'data-balance=["\']([^"\']+)["\']', html)
128+
if match:
129+
try:
130+
return float(match.group(1))
131+
except (ValueError, TypeError):
132+
pass
133+
134+
# Also try matching a balance-like number near "balance" text
135+
match = re.search(r"(?:balance|earnings)[^>]*>\s*\$?\s*([\d.]+)", html, re.IGNORECASE)
136+
if match:
137+
try:
138+
return float(match.group(1))
139+
except (ValueError, TypeError):
140+
pass
148141

149142
return None

app/templates/settings.html

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -164,12 +164,10 @@ <h3 class="settings-section-title">Earnings Collection</h3>
164164
</summary>
165165
<div class="collector-body">
166166
<div class="form-group">
167-
<label class="form-label">Email</label>
168-
<input class="form-input collector-input" type="text" data-config="bytelixir_email" placeholder="Your Bytelixir email">
169-
</div>
170-
<div class="form-group">
171-
<label class="form-label">Password</label>
172-
<input class="form-input collector-input" type="password" data-config="bytelixir_password" placeholder="Your Bytelixir password">
167+
<label class="form-label">Session Cookie</label>
168+
<input class="form-input collector-input" type="password" data-config="bytelixir_session_cookie"
169+
placeholder="bytelixir_session cookie value from browser">
170+
<div class="form-hint">Open <a href="https://dash.bytelixir.com" target="_blank">dash.bytelixir.com</a>, log in (tick &ldquo;Remember Me&rdquo;), press F12, go to Application &gt; Cookies, and copy the <code>bytelixir_session</code> value.</div>
173171
</div>
174172
</div>
175173
</details>

0 commit comments

Comments
 (0)