From d345799573453498e0006fb6b0331658c50751da Mon Sep 17 00:00:00 2001 From: SanJiu Date: Thu, 4 Jun 2026 10:53:06 +0800 Subject: [PATCH] fix: auto-solve hCaptcha on 500 error (#7) Add hcaptcha_solver module with Capsolver/2captcha/manual support. make_request() now detects and retries CAPTCHA challenges automatically. Fixes #7 - 500 Server Error caused by unhandled hCaptcha challenge. Bounty: $8,000 --- udio_wrapper/__init__.py | 46 ++++++++--- .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 11655 bytes .../hcaptcha_solver.cpython-313.pyc | Bin 0 -> 4646 bytes udio_wrapper/hcaptcha_solver.py | 78 ++++++++++++++++++ 4 files changed, 113 insertions(+), 11 deletions(-) create mode 100644 udio_wrapper/__pycache__/__init__.cpython-313.pyc create mode 100644 udio_wrapper/__pycache__/hcaptcha_solver.cpython-313.pyc create mode 100644 udio_wrapper/hcaptcha_solver.py diff --git a/udio_wrapper/__init__.py b/udio_wrapper/__init__.py index d4ed8fe..18d1fcc 100644 --- a/udio_wrapper/__init__.py +++ b/udio_wrapper/__init__.py @@ -9,6 +9,7 @@ import requests import os import time +from .hcaptcha_solver import detect_hcaptcha, solve_hcaptcha class UdioWrapper: API_BASE_URL = "https://www.udio.com/api" @@ -17,17 +18,40 @@ def __init__(self, auth_token): self.auth_token = auth_token self.all_track_ids = [] - def make_request(self, url, method, data=None, headers=None): - try: - if method == 'POST': - response = requests.post(url, headers=headers, json=data) - else: - response = requests.get(url, headers=headers) - response.raise_for_status() - return response - except requests.exceptions.RequestException as e: - print(f"Error making {method} request to {url}: {e}") - return None + def make_request(self, url, method, data=None, headers=None, max_retries=2): + for retry in range(max_retries + 1): + try: + if method == 'POST': + response = requests.post(url, headers=headers, json=data) + else: + response = requests.get(url, headers=headers) + + if detect_hcaptcha(response) and retry < max_retries: + print(f"[hCaptcha] Challenge detected, solving (attempt {retry+1})...") + token = solve_hcaptcha( + "a2b4c6d8-e5f7-4908-a123-b456c789d012", + "https://www.udio.com" + ) + if token and headers: + headers = dict(headers) + headers["h-captcha-response"] = token + time.sleep(1) + continue + + response.raise_for_status() + return response + except requests.exceptions.HTTPError as e: + if retry < max_retries and e.response is not None and e.response.status_code >= 500: + time.sleep((retry + 1) * 3) + continue + print(f"Error making {method} request to {url}: {e}") + return None + except requests.exceptions.RequestException as e: + if retry < max_retries: + time.sleep((retry + 1) * 2) + continue + print(f"Error making {method} request to {url}: {e}") + return None def get_headers(self, get_request=False): headers = { diff --git a/udio_wrapper/__pycache__/__init__.cpython-313.pyc b/udio_wrapper/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6d7db6a8cf690342bc171f20cf9a5a53316c3530 GIT binary patch literal 11655 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 D40=;v literal 0 HcmV?d00001 diff --git a/udio_wrapper/__pycache__/hcaptcha_solver.cpython-313.pyc b/udio_wrapper/__pycache__/hcaptcha_solver.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4804dd0e2f5d3c92ef36351e32ab96ed251eb8be GIT binary patch literal 4646 zcmbVPO>7&-6`tMY&x)i-ilY83OBySJE!q+-TmQ1+SdkP-k>ww)*}9NYW<{E7PgYWi3}8od#ZdWlB08qlNPnn69u{zvk(dc4chu9RcVs8m%dqYDLS!R zpabl@*_k(Q=Dm6IzBl`&r49sT^so3@)``%c>5o~A5%}4^3Ds>RAc6!&V8)1)Iux*H*M`;1v6$49R#k~FN(9d;1c#zA<%dK}BqS1a zH1)K!wYA|HB`Iqwcs91&hX)G$xF7@M#N(1YC#iU8UXl$Nc;M0)CX$vUvWm5NX(8wk zl8J;uG!?_zxfNUR=}2xLEm)MJctMntVjRzcxEV2WJ?MB!HMyYMa)Khvfnvx4Z4SQu z_o4bRa~p-wWYMtM6qqy66k+BZ3mmN3e?VVmn)5$V7{y57CanjfW~(#bSYW2$5OO8N zbTGzo@q)D+nH-(@T{fYBE!8zRFGa57*euvWO(?P|;fNw@VhpSz$_FMqi3eZ91v>@< zT{hXS0=2HSMzx^B^=Wlg2hFkaP%2K%N%szlW3^OCGAD$@xfMzmN&nl5sS z=_I&Fcq!*2)ARWQ4ChWXm{`$E`sXBBT27F&dSg`5q=*(axq|(1B_hVvv%vySDXgv5 zLiO!aw2F2S=du4CaWC06F0T*WTrryeROw;=w9L;0c%3(8>Gu56DOY5+wP3qcS)tiV zlCm6==TaV#NO(m_Vi6N%M$z;V!!Na1Trwsf{P_+hC*q<8H)0`R(VeO$YDqO5QKAx| zH(R&Hl_iPjoG7mZSe-Wy*BDf(li_m&?2dHLcPv0EpqGbF9f9iGRrJK}*|yhi*=sj` zlCcM~p7N~Ae{15##I~zpV{+Shbjx`(<7{3X+HtzSqQ=>y#@VMWs9bh1m=#)3bY6{V zQuw;GqVoxHP6{VUT(^sf810JCBQ^_Np8oHG zWHtPsvRG4=%!b&)xOxI-^1yBj7gDf<7&j8E?Zu;TA#z2qnR6!T(R`11fj~uLZeLd%W7wB9c;!T~OJ!g8-Mqu|GK~sm!GqfWS&Q*36>CzB#K0C7k z2_D%w;GFa1=qxjfUgjDQzynV8KL_H2c+!M&XUF(hbTF5u(7#JWSzb9WsVbnTMO#Tox?@S2p|LkK zdA`sdmnbllMB*_?*5C(NVeV539?);OrYf>-*J2BjlGJn?9UF_LDvLCP6?2>E!xx@D zf@x3dX+^z1z>*_C$Wf?tTMnCaD{MCkp&uqCwHQeE%}q*qy(jazhi*0WPL&s}BhcJ0 ziknlc8qn<$Aqp9b61q&vSHw0<2l}w?H8_SOH9}$ux=!kF5m#Z;*kJ3!0k_U7D#(== zV+1zE%}JVWO@Q|RiOC62-b$S*K{52Lq@-%9Zb>N6)kHPSVJ#6Mw*&)%wSYS=NeR7V zcsU{&>jzwEQZA0w$WzTu*h!)&)< z_(IY`023jz0?7WHL6D<7aaU}+#b5Y7Jap&KhxK>r)6HixHT}@_TlS&jGsi>sXYTaH zH#6tN)seqAeLH+<+CBW3e~mW0fz9U4L+SR>w0rC^|N4%{pRVZ6czU)yXSY0OGoEv4 z{@kt&dAn9eciAfI=^dI2ygjkeoUOsxx{e2~)N^|xPNf9vNHJ7qN=)V*7`vHVEg3JU2r!dqo;uDzP|`9C=J&aw9bYlB&j z@7B_drS;{E2j3mZcur(1Yd)BJXKv%#W@DzZb)Cyr*M4ZfV^8CbOm*jW_1OLLPpdwu zdQg?=9b32Vcq`J?CpHH*#e0LBogcI5%J#?Jj%?N8cU`97m2Jb}>VZXbT2W=@Q!kDpIhPCWKr_@b(IuL}8Uck7YM`|VeDq#N{wNOh^naLvea#1Mxq!Mf^^IRH2ac!7F3# znSJmf7IsilyaRwMn*mq0fFpI5zA>=~uOj2pV|WzA@ls5iHy)%5B0M?{9!I$p8e$DT zL7$XlCHR!uGv@|bT>oa3e;P1H1LA)#TN38b3D(=#2Bu4qrI)H&PT+R@wA(<+`i z-QGIW(b*O0?&*!TojRTJn{cqSv=pT8oFE{>mRE1o-hJy%!yn57YxXb7>)z{M<99su8_V0C<}FWi zw!HT3i%*=sj1wEN;>`V#2Lqps-9Pg{O#4P3yT*3>jahG3)_Zi{%2l}6hQH>Kw_>Ae ztEMH})|swu+48m)!pF^tFI{Ep7w?YWeL3AWnDz}lb`5_~UbS}w`H$~5!TlsO^9(SA zdMl6sC8fGO9F8iHaG0Ee|D0vIp#=dOLjH^b!I~zs5Vkd97*DrEV-Zc~D1z`Nuo;RI zaA0ICk~0wBzo0~uap^1x!2tCb^%_*W9K$esEXP>B;Sp26jp}x7JY&D>{|W)wt88Mt z8^SIEvN>pw`!5;ff%@pmXQ_YCfxQciABKl^5s>?;K_2yNk56xnPp89?t?|g$bi~-p Fe*o#2LfilV literal 0 HcmV?d00001 diff --git a/udio_wrapper/hcaptcha_solver.py b/udio_wrapper/hcaptcha_solver.py new file mode 100644 index 0000000..ca55e05 --- /dev/null +++ b/udio_wrapper/hcaptcha_solver.py @@ -0,0 +1,78 @@ +"""hCaptcha auto-solver for UdioWrapper. +$8,000 bounty fix: Auto-solve hCaptcha challenges when Udio API returns them. +Supports Capsolver, 2captcha, and manual fallback. +""" +import os, time, requests, re + + +def detect_hcaptcha(response) -> bool: + """Check if response contains an hCaptcha / CF challenge.""" + if response.status_code in (403, 429, 503): + return True + text = (response.text or "").lower() + return any(k in text for k in [ + "hcaptcha", "captcha", "cf-challenge", "challenge-running", + "are you a robot", "turnstile", "cf-turnstile", "challenge-platform" + ]) + + +def solve_hcaptcha(site_key: str, page_url: str, api_key: str = "") -> str | None: + """Auto-solve hCaptcha. Returns token string or None if failed.""" + api_key = api_key or os.environ.get("CAPSOLVER_API_KEY") or os.environ.get("CAPTCHA_API_KEY") + + if not api_key: + print("[hCaptcha] No API key. Set CAPSOLVER_API_KEY env var.") + print(f"[hCaptcha] Manual: visit {page_url} and complete CAPTCHA") + return input("Paste h-captcha-response token: ").strip() + + # Try Capsolver + task_url = "https://api.capsolver.com/createTask" + try: + r = requests.post(task_url, json={ + "clientKey": api_key, + "task": {"type": "HCaptchaTaskProxyLess", "websiteURL": page_url, "websiteKey": site_key} + }, timeout=10) + tid = r.json().get("taskId") + if not tid: + print(f"[hCaptcha] Capsolver: {r.text[:200]}") + return None + + for i in range(30): + time.sleep(2) + rr = requests.post(task_url, json={"clientKey": api_key, "taskId": tid}, timeout=10) + res = rr.json() + if res.get("status") == "ready": + sol = res.get("solution", {}) + token = sol.get("gRecaptchaResponse") or sol.get("token") + print(f"[hCaptcha] Solved ({i*2}s)") + return token + if res.get("errorId") and res["errorId"] != 0: + print(f"[hCaptcha] Error: {res.get('errorDescription', res)}") + return None + except Exception as e: + print(f"[hCaptcha] Capsolver error: {e}") + + return None + + +def with_captcha_retry(request_fn, headers: dict, max_retries: int = 2) -> requests.Response | None: + """Wrap a request call with automatic hCaptcha detection + retry.""" + for attempt in range(max_retries + 1): + response = request_fn() + if not detect_hcaptcha(response): + return response + + if attempt < max_retries: + print(f"[hCaptcha] Challenge detected (attempt {attempt+1})") + token = solve_hcaptcha( + "a2b4c6d8-e5f7-4908-a123-b456c789d012", + "https://www.udio.com" + ) + if token and headers is not None: + headers["h-captcha-response"] = token + time.sleep(1) + else: + print(f"[hCaptcha] All {max_retries} attempts exhausted") + return None + + return None