Metadata-Version: 2.4
Name: bypass-vuotlink-sdk
Version: 0.5.0
Summary: Python SDK for the bypass-vuotlink API
Requires-Python: >=3.10
Requires-Dist: httpx>=0.24
Requires-Dist: websockets>=13
Description-Content-Type: text/markdown

# bypass-vuotlink-sdk

Python SDK for the [bypass-vuotlink](https://github.com/htilssu/bypass_vuotlink) API — a service that resolves shortlink/safelink URLs (e.g. vuotnhanh.com) to their final destination by running a real browser workflow.

## Install

```bash
pip install bypass-vuotlink-sdk
```

## Quick start

```python
from bypass_vuotlink_sdk import BypassVuotLink

with BypassVuotLink(base_url="http://localhost:8000") as client:
    result = client.resolve("https://vuotnhanh.com/abc123")
    print(result.final_url)
```

### Async

```python
from bypass_vuotlink_sdk import AsyncBypassVuotLink

async with AsyncBypassVuotLink(base_url="http://localhost:8000") as client:
    result = await client.resolve("https://vuotnhanh.com/abc123")
    print(result.final_url)
```

## Configuration

```python
from bypass_vuotlink_sdk import BypassVuotLink, ClientConfig

# Pass parameters directly
client = BypassVuotLink(
    base_url="http://localhost:8000",
    timeout=90.0,
    api_key="secret",
)

# Or via a config object
config = ClientConfig(base_url="http://localhost:8000", api_key="secret")
client = BypassVuotLink.from_config(config)

# Or from environment variables
# BYPASS_BASE_URL, BYPASS_TIMEOUT, BYPASS_API_KEY
client = BypassVuotLink.from_env()
```

## Result

```python
result.requested_url  # the URL you passed in
result.final_url      # resolved destination
result.status_code    # HTTP status at final_url
result.title          # page title
result.workflow       # workflow used (e.g. "vuotnhanh")
result.hops_used      # how many workflow hops were actually followed
result.hops_exceeded  # True if max_hops was hit before a terminal workflow was reached
result.free_interrupted  # True when free mode stopped before an unsupported workflow
result.free_interrupted_workflow  # workflow name that stopped free mode, or None
result.solved_workflows  # successful hops in execution order

for hop in result.solved_workflows:
    print(hop.workflow, hop.resolved_url)
    print(hop.price, hop.price_auto_solve_captcha)
    print(hop.is_support_free)
```

`solved_workflows` is part of the final `/api/v1/browser` result only. It does
not expose the CAPTCHA token. Each item records the workflow output URL, free-usage
support, and the current base/automatic-CAPTCHA prices loaded from workflow metadata.

## Supported workflows

```python
workflows = client.list_workflows()

for workflow in workflows.workflows:
    print(workflow.name)
    print(workflow.link_formats)
    print(workflow.maintenance, workflow.maintenance_message)
    print(workflow.code_ttl_seconds)
    print(workflow.max_codes_per_link)
    print(workflow.has_captcha)
    print(workflow.is_support_free)
    print(workflow.price)
    print(workflow.price_auto_solve_captcha)
```

Kiểm tra một URL thuộc workflow nào mà không chạy workflow:

```python
workflow = client.check_workflow("https://layma.net/example")
print(
    workflow.name,
    workflow.price,
    workflow.has_captcha,
    workflow.is_support_free,
)
```

Kiểm tra trực tiếp theo domain hoặc hostname:

```python
workflow = client.check_workflow_domain("layma.net")
print(workflow.name, workflow.is_support_free, workflow.price)
```

`resolve()` raises `WorkflowMaintenanceError` when the matched workflow is
temporarily disabled. The exception exposes `workflow` and
`maintenance_message`.

Example item:

```python
workflow.name          # "funlink"
workflow.link_formats  # ["https://funlink.io/..."]
workflow.code_ttl_seconds  # 600, or None when using an older server
workflow.max_codes_per_link  # per-workflow pool target, or None on older servers
workflow.has_captcha         # True only for workflows exposing the captcha flow
workflow.is_support_free     # True when the workflow allows free usage
workflow.price               # per-workflow price, or None on older servers
workflow.price_auto_solve_captcha  # auto CAPTCHA surcharge, or None on older servers
```

Layma mặc định để người dùng giải thủ công qua event `captcha_pending` khi
không truyền option. Truyền `True` để chọn auto solve một cách tường minh:

```python
manual = client.resolve(url, auto_solve_captcha=False)
automatic = client.resolve(url, auto_solve_captcha=True)
```

Free mode kiểm tra từng workflow trước khi chạy. Nếu chuỗi hop gặp workflow
không hỗ trợ free, SDK trả về URL tại hop đó và thông tin workflow bị chặn:

```python
result = client.resolve(url, is_free=True)
if result.free_interrupted:
    print(result.free_interrupted_workflow, result.final_url)
```

The SDK ignores unknown response fields, supplies defaults for fields missing
from older servers, and filters items marked `is_dead=true`. This keeps patch
and additive API contract changes backward compatible.

## Chained links (max_hops)

Some links resolve through more than one bypass workflow in a row (e.g. a
shortlink resolving to a funlink link, which itself resolves to a toplinks
link). By default `resolve()` follows up to 10 such hops before returning.
Pass `max_hops` to cap that:

```python
# Solve only the first link and return immediately, without following any
# further chained hop.
result = client.resolve(url, max_hops=1)
```

`result.hops_used` reports how many hops were actually followed. If the chain
doesn't reach a terminal workflow within `max_hops`, `result.hops_exceeded` is
`True` (`hops_used == max_hops`) and `result.final_url` is just wherever the
last hop left off, not necessarily the fully-resolved destination.

## Fetch a code without submitting it

```python
result = client.fetch_code(
    "https://funlink.io/some/destination",
    "funlink",  # or "toplinks", "ontops", "ontops-dr", "gtraffic", "gtraffic-dr", "layma", "link4m"
    source="live",  # or "background"
)
result.code       # confirmation code
result.dest_url   # the destination it was fetched from
```

## Code pool

```python
# Pop a prefetched code for a cached keyword_text/image_url pair.
result = client.consume_code("some keyword", image_url="https://...")
result.code
result.resolved_url
```

## Not-found tasks (manual search fallback)

When a keyword's destination link couldn't be auto-resolved (e.g. via OCR +
Google search), it's marked `not_found` in the cache. These helpers let a
human/worker claim one, search the web manually, and report back a candidate:

```python
task = client.claim_not_found_task(lease_seconds=900)
task.keyword_id
task.keyword_text
task.image_url
task.search_query

# ... search the web manually, find a candidate destination URL ...

result = client.verify_not_found_task(task.keyword_id, "https://found-it.example/page")
if result.verified:
    print("Accepted:", result.resolved_url)
else:
    print("Rejected:", result.reason)
```

`claim_not_found_task()` raises `ApiError` (status_code 404) when there are no
claimable not-found entries. The lease (`lease_seconds`) auto-expires if never
reported back, so an unreported claim becomes claimable by someone else again
without any explicit release call.

```python
# Re-open every not-found entry for another search attempt.
result = client.reset_not_found()
result.reset_count
```

## Watch realtime events (WebSocket)

```python
# Connect to /api/v1/events/ws and stream generic realtime events
async for event in client.watch_events(events_ws_token="secret"):
    print("Received event:", event)
    # Examples:
    # {"event": "captcha_pending", "appToken": "...", "captchaSite": "..."}
    # {"event": "captcha_solved", "appToken": "...", "status": "success"}
```

## Error handling

```python
from bypass_vuotlink_sdk import (
    UnsupportedUrlError,
    UnsafeUrlError,
    BrowserExecutionError,
    CodeExhaustedError,
    BypassVuotlinkError,
)

try:
    result = client.resolve(url)
except UnsupportedUrlError:
    ...  # no workflow supports this URL type (HTTP 422)
except UnsafeUrlError:
    ...  # URL is private or unsafe (HTTP 400)
except BrowserExecutionError:
    ...  # browser workflow failed (HTTP 502)
except CodeExhaustedError:
    ...  # provider has no confirmation code available
except BypassVuotlinkError:
    ...  # catch-all
```
