cdmctl 1.5.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,39 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+ .venv/
8
+ .ruff_cache/
9
+ .pytest_cache/
10
+ .mypy_cache/
11
+ .coverage
12
+ .coverage.*
13
+ htmlcov/
14
+ celerybeat-schedule
15
+ celerybeat.pid
16
+
17
+ # Environment
18
+ .env
19
+ .env.sh
20
+
21
+ # Node / frontend
22
+ node_modules/
23
+ webapp/frontend/build/
24
+
25
+ # Webapp built assets (generated by frontend build)
26
+ webapp/assets/*
27
+ webapp/templates/*
28
+ !webapp/templates/email/
29
+ !webapp/templates/email/**
30
+
31
+ # Browser extension
32
+ browser-extension/cdm.zip
33
+
34
+ # Editor / OS
35
+ .idea/
36
+ .vscode/
37
+ .claude/
38
+ .DS_Store
39
+ *.log
cdmctl-1.5.3/Makefile ADDED
@@ -0,0 +1,14 @@
1
+ format:
2
+ uv run ruff format cdmctl/
3
+ uv run ruff check --fix cdmctl/
4
+
5
+ check-format-ci:
6
+ uv run ruff format --check cdmctl/
7
+ uv run ruff check cdmctl/
8
+
9
+ lint:
10
+ uv run ruff check cdmctl/
11
+ uv run ty check cdmctl/
12
+
13
+ bump:
14
+ uv version --bump $(filter-out $@,$(MAKECMDGOALS))
cdmctl-1.5.3/PKG-INFO ADDED
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.5
2
+ Name: cdmctl
3
+ Version: 1.5.3
4
+ Summary: cdmctl - command-line control for CDM Server
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: httpx>=0.27
7
+ Requires-Dist: rich>=13
8
+ Requires-Dist: typer>=0.12
cdmctl-1.5.3/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # CDM Server CLI
2
+
3
+ Command-line interface for [CDM Server](../README.md).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install cdmctl
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ cdm login --server https://your-cdm-server.com
15
+ cdm search --pattern "inception"
16
+ cdm download --torrent-id 12345 --device-id 1
17
+ ```
18
+
19
+ ## Commands
20
+
21
+ | Command | Description |
22
+ |---|---|
23
+ | `cdm version` | Show CLI version |
24
+ | `cdm login` | Login and save credentials |
25
+ | `cdm logout` | Clear stored tokens |
26
+ | `cdm whoami` | Show current user |
27
+ | `cdm search` | Search torrents on nCore |
28
+ | `cdm download` | Queue a torrent for download |
29
+ | `cdm users` | User management (admin) |
30
+ | `cdm devices` | Device management |
31
+ | `cdm status` | Download status & control |
32
+ | `cdm tmdb` | Browse and search TMDB |
33
+ | `cdm wishlist` | Manage wishlist |
34
+
35
+ Run `cdm <command> --help` for options.
36
+
37
+ ### Wishlist
38
+
39
+ Add movies by IMDB ID; the server retries weekly until the torrent appears on nCore.
40
+
41
+ ```bash
42
+ cdm wishlist types # list quality types
43
+ cdm wishlist add --imdb-id tt1375666 --device-id 1 --type hd_hun
44
+ cdm wishlist list
45
+ cdm wishlist delete --item-id 3
46
+ ```
File without changes
@@ -0,0 +1,91 @@
1
+ import sys
2
+ from importlib.metadata import version
3
+ from typing import Any
4
+
5
+ import httpx
6
+ from rich.console import Console
7
+
8
+ from . import config
9
+
10
+ console = Console(stderr=True)
11
+
12
+ CLI_USER_AGENT = f"cdmctl/{version('cdmctl')}"
13
+
14
+
15
+ class CDMError(Exception):
16
+ pass
17
+
18
+
19
+ class CDMClient:
20
+ def __init__(self, require_auth: bool = True):
21
+ server_url = config.get_server_url()
22
+ if not server_url:
23
+ console.print(
24
+ "[bold red]✗[/bold red] Not configured — run [bold]cdm login[/bold]"
25
+ )
26
+ sys.exit(1)
27
+ self.base = server_url
28
+ self._access, self._refresh = config.get_tokens()
29
+ if require_auth and not self._refresh:
30
+ console.print(
31
+ "[bold red]✗[/bold red] Not logged in — run [bold]cdm login[/bold]"
32
+ )
33
+ sys.exit(1)
34
+ self._http = httpx.Client(headers={"User-Agent": CLI_USER_AGENT})
35
+
36
+ def _auth_headers(self) -> dict:
37
+ h = {"Content-Type": "application/json"}
38
+ if self._access:
39
+ h["Authorization"] = f"Bearer {self._access}"
40
+ return h
41
+
42
+ def _do_refresh(self) -> bool:
43
+ if not self._refresh:
44
+ return False
45
+ try:
46
+ resp = self._http.post(
47
+ f"{self.base}/api/auth/refresh/",
48
+ json={"refresh_token": self._refresh},
49
+ timeout=10,
50
+ )
51
+ if resp.status_code == 200:
52
+ data = resp.json()
53
+ self._access = data["access_token"]
54
+ self._refresh = data.get("refresh_token", self._refresh)
55
+ config.save_tokens(self._access, self._refresh)
56
+ return True
57
+ except Exception:
58
+ pass
59
+ return False
60
+
61
+ def request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
62
+ url = f"{self.base}{path}"
63
+ kwargs.setdefault("timeout", 30)
64
+ resp = self._http.request(method, url, headers=self._auth_headers(), **kwargs)
65
+ if resp.status_code == 401:
66
+ if self._do_refresh():
67
+ resp = self._http.request(
68
+ method, url, headers=self._auth_headers(), **kwargs
69
+ )
70
+ if resp.status_code == 401:
71
+ config.clear_tokens()
72
+ console.print(
73
+ "[bold red]✗[/bold red] Session expired — run [bold]cdm login[/bold]"
74
+ )
75
+ sys.exit(1)
76
+ return resp
77
+
78
+ def get(self, path: str, **kwargs: Any) -> httpx.Response:
79
+ return self.request("GET", path, **kwargs)
80
+
81
+ def post(self, path: str, **kwargs: Any) -> httpx.Response:
82
+ return self.request("POST", path, **kwargs)
83
+
84
+ def put(self, path: str, **kwargs: Any) -> httpx.Response:
85
+ return self.request("PUT", path, **kwargs)
86
+
87
+ def patch(self, path: str, **kwargs: Any) -> httpx.Response:
88
+ return self.request("PATCH", path, **kwargs)
89
+
90
+ def delete(self, path: str, **kwargs: Any) -> httpx.Response:
91
+ return self.request("DELETE", path, **kwargs)
@@ -0,0 +1,50 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ CONFIG_DIR = Path.home() / ".config" / "cdm"
5
+ CONFIG_FILE = CONFIG_DIR / "config.json"
6
+
7
+
8
+ def load() -> dict:
9
+ if not CONFIG_FILE.exists():
10
+ return {}
11
+ try:
12
+ return json.loads(CONFIG_FILE.read_text())
13
+ except Exception:
14
+ return {}
15
+
16
+
17
+ def save(data: dict) -> None:
18
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
19
+ CONFIG_FILE.write_text(json.dumps(data, indent=2))
20
+
21
+
22
+ def get_server_url() -> str | None:
23
+ return load().get("server_url")
24
+
25
+
26
+ def get_tokens() -> tuple[str | None, str | None]:
27
+ cfg = load()
28
+ return cfg.get("access_token"), cfg.get("refresh_token")
29
+
30
+
31
+ def save_login(server_url: str, access_token: str, refresh_token: str) -> None:
32
+ cfg = load()
33
+ cfg["server_url"] = server_url.rstrip("/")
34
+ cfg["access_token"] = access_token
35
+ cfg["refresh_token"] = refresh_token
36
+ save(cfg)
37
+
38
+
39
+ def save_tokens(access_token: str, refresh_token: str) -> None:
40
+ cfg = load()
41
+ cfg["access_token"] = access_token
42
+ cfg["refresh_token"] = refresh_token
43
+ save(cfg)
44
+
45
+
46
+ def clear_tokens() -> None:
47
+ cfg = load()
48
+ cfg.pop("access_token", None)
49
+ cfg.pop("refresh_token", None)
50
+ save(cfg)