Metadata-Version: 2.4
Name: lmsadwl
Version: 1.0.1
Summary: Lenovo LMSA ROM Downloader — download firmware from official Lenovo servers
Home-page: https://github.com/fiksofficial/lmsadwl
Author: fiksofficial
Author-email: fiksofficial <f1xred@hotmail.com>
License: MIT
Project-URL: Homepage, https://github.com/fiksofficial/lmsadwl
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.25.0
Dynamic: author
Dynamic: home-page
Dynamic: license-file
Dynamic: requires-python

# lmsadwl

Download firmware from official Lenovo servers (LMSA / Software Fix API).

```
pip install lmsadwl
```

## Commands

```
lmsadwl login                      # OAuth2 login via Lenovo Passport
lmsadwl login --url URL            # complete login with redirect URL
lmsadwl login --token TOKEN        # save token directly

lmsadwl lookup --sn HA1CQ8SV      # firmware by serial number
lmsadwl lookup --imei 123456789012345 [--model-code X] [--ro-carrier Y]

lmsadwl search --model x606       # search ROMs by keyword
lmsadwl download --model TB-X606X [-o ./download]
lmsadwl batch --model x606 --limit 5

lmsadwl models [-f moto] [-r]     # list supported devices
lmsadwl device                    # show ADB device info
lmsadwl auto                      # detect device + find firmware
lmsadwl status                    # token info and expiry
lmsadwl shell                     # interactive menu
```

## Python API

```python
from lmsadwl import LMSAClient

client = LMSAClient()

# login
url, state = client.login_interactive()
print(url)
redirect = input("Paste redirect URL: ")
client.login_with_url(redirect)

# search
roms = client.search_roms("x606")
for rom in roms:
    print(rom.name, rom.uri)

# lookup by serial
result = client.lookup_by_sn("HA1CQ8SV")
if result.found:
    print(result.roms[0].name, result.roms[0].uri)

# auto-detect (requires adb)
device, roms = client.auto_detect()
if device:
    print(device.title, device.serial)

# models
models = client.get_models(keyword="moto", read_only=True)
for m in models:
    print(m.model_name, m.market_name)

# IMEI lookup
result = client.lookup_by_imei("123456789012345")
```

### Auto-relogin

```python
def relogin():
    url, _ = client.login_interactive()
    print(f"Open: {url}")
    redirect = input("Paste URL: ")
    return client.login_with_url(redirect)

client.set_relogin_callback(relogin)
# API calls will auto-relogin on 403/408/402
```

## Models

```python
from lmsadwl import LMSAClient, TokenManager, OAuth2Flow
from lmsadwl import DeviceInfo, ROMInfo
from lmsadwl import is_valid_sn, is_valid_imei
from lmsadwl import parse_jwt, jwt_is_expired, jwt_time_left
```

### `LMSAClient(token_dir=None, auto_relogin=True, max_retries=2)`

| Method | Returns |
|---|---|
| `login_interactive()` | `(url, state)` |
| `login_with_url(redirect_url)` | `bool` |
| `login_with_token(token)` | `None` |
| `ensure_auth()` | `str` (token) |
| `set_relogin_callback(fn)` | `None` |
| `get_rom_list()` | `list[ROMInfo]` |
| `search_roms(keyword)` | `list[ROMInfo]` |
| `get_models(keyword="", read_only=False)` | `list[ModelInfo]` |
| `lookup_by_sn(sn)` | `LookupResult` |
| `lookup_by_imei(imei, model_code=None, carrier=None)` | `LookupResult` |
| `auto_detect()` | `(DeviceInfo, list[ROMInfo])` |
| `get_rom_match(model_name)` | `dict \| None` |

### `ROMInfo`

| Field | Type |
|---|---|
| `name` | `str` |
| `uri` | `str` |
| `md5` | `str` |
| `rom_type` | `int` |
| `publish_date` | `str` |
| `flash_flow` | `str` |
| `rom_match_id` | `str` |

### `LookupResult`

| Property | Type |
|---|---|
| `found` | `bool` |
| `rescue_available` | `bool` |
| `roms` | `list[ROMInfo]` |
| `code` | `str` |
| `desc` | `str` |

### `ModelInfo`

| Field | Type |
|---|---|
| `category` | `str` |
| `brand` | `str` |
| `model_name` | `str` |
| `market_name` | `str` |
| `platform` | `str` |
| `read_support` | `bool` |

### `DeviceInfo`

| Field | Type |
|---|---|
| `serial` | `str` |
| `model` | `str` |
| `brand` | `str` |
| `fingerprint` | `str` |
| `incremental` | `str` |
| `title` | `str` (property) |

### Utils

```python
is_valid_sn(sn)          # bool — 8 alphanumeric chars, no I/O/L
is_valid_imei(imei)      # bool — 14-15 digits
parse_jwt(token)         # dict | None — decode JWT payload
jwt_is_expired(token, margin=300)  # bool
jwt_time_left(token)     # float — seconds until expiry
```

## Token

Stored in `~/.lmsadwl/token.json`. Auto-refreshes on API errors. Migrates from `.lmsa_tokens.json`.

## License

MIT
