diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e54398..532d54b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,18 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm consumers can migrate to the strict variant once every issuer emits `aud`. Additive, BC-safe. +### Changed +- **`Arr\AbstractArrClient` extraction** (findings CQ2/CQ5) — the four near-identical + *arr clients (`RadarrClient`, `SonarrClient`, `ProwlarrClient`, `BazarrClient`) now + extend a shared `abstract class AbstractArrClient` that owns the constructor + (`baseUrl`/`apiKey`/`logger`/`timeout`), header building, the GET/POST/PUT/DELETE + cURL methods, and the per-status-code error mapping. Subclasses keep only their + endpoint-specific methods plus a `protected vendorName(): string` used in error + messages. **No behaviour change** — still blocking cURL, identical public class + names, methods, and thrown exceptions/messages; existing tests pass unchanged. This + is the structural enabler for the F2b async-transport seam (transport injection then + happens in one place). Internal refactor only — no consumer impact. + ### Documentation - **`Auth\JwtClaims` security & round-trip docs** (findings S4/B4): - Class docblock now prominently states `JwtClaims` performs **no signature diff --git a/src/Arr/AbstractArrClient.php b/src/Arr/AbstractArrClient.php new file mode 100644 index 0000000..82d9e77 --- /dev/null +++ b/src/Arr/AbstractArrClient.php @@ -0,0 +1,213 @@ +baseUrl = rtrim($baseUrl, '/'); + $this->apiKey = $apiKey; + $this->logger = $logger; + $this->timeout = $timeout; + } + + /** + * Vendor name used in error messages (e.g. "Radarr", "Sonarr"). + */ + abstract protected function vendorName(): string; + + /** + * Performs a GET request. + * + * @param string $path Request path. + * @return array Decoded JSON response. + * @throws RuntimeException On network or HTTP errors. + */ + protected function get(string $path): array + { + return $this->request('GET', $path, null); + } + + /** + * Performs a POST request with a JSON body. + * + * @param string $path Request path. + * @param array $body JSON-serializable body. + * @return array Decoded JSON response. + * @throws RuntimeException On network or HTTP errors. + */ + protected function post(string $path, array $body): array + { + return $this->request('POST', $path, $body); + } + + /** + * Performs a PUT request with a JSON body. + * + * @param string $path Request path. + * @param array $body JSON-serializable body. + * @return array Decoded JSON response. + * @throws RuntimeException On network or HTTP errors. + */ + protected function put(string $path, array $body): array + { + return $this->request('PUT', $path, $body); + } + + /** + * Performs a DELETE request. + * + * @param string $path Request path. + * @return array Decoded JSON response. + * @throws RuntimeException On network or HTTP errors. + */ + protected function delete(string $path): array + { + return $this->request('DELETE', $path, null); + } + + /** + * Executes an HTTP request against the *arr instance and decodes the JSON body. + * + * @param string $method One of GET/POST/PUT/DELETE. + * @param string $path Request path. + * @param array|null $body JSON-serializable body for POST/PUT; null otherwise. + * @return array Decoded JSON response. + * @throws RuntimeException On network or HTTP errors. + */ + private function request(string $method, string $path, ?array $body): array + { + $url = $this->baseUrl . $path; + assert($url !== ''); + + $options = [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => $this->timeout, + CURLOPT_HTTPHEADER => $this->buildHeaders(), + ]; + + if ($method === 'POST') { + $options[CURLOPT_POST] = true; + $options[CURLOPT_POSTFIELDS] = $this->encodeBody($body ?? []); + } elseif ($method === 'PUT') { + $options[CURLOPT_CUSTOMREQUEST] = 'PUT'; + $options[CURLOPT_POSTFIELDS] = $this->encodeBody($body ?? []); + } elseif ($method === 'DELETE') { + $options[CURLOPT_CUSTOMREQUEST] = 'DELETE'; + } + + $ch = curl_init(); + if ($ch === false) { + throw new RuntimeException('curl_init() failed'); + } + + curl_setopt_array($ch, $options); + + /** @var string|false */ + $responseBody = curl_exec($ch); + $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlErrno = curl_errno($ch); + $curlError = curl_error($ch); + curl_close($ch); + + if ($responseBody === false || $curlErrno !== 0) { + throw new RuntimeException('cURL error: ' . $curlError, $curlErrno); + } + + $vendor = $this->vendorName(); + + if ($httpCode === 401) { + throw new RuntimeException($vendor . ' API authentication failed (401)'); + } + + if ($httpCode === 404) { + throw new RuntimeException($vendor . ' API resource not found (404): ' . $path); + } + + if ($httpCode >= 400) { + throw new RuntimeException($vendor . ' API error: HTTP ' . $httpCode); + } + + if ($responseBody === '') { + return []; + } + + $decoded = json_decode($responseBody, true); + if (!is_array($decoded)) { + throw new RuntimeException('Invalid JSON response from ' . $vendor); + } + + return $decoded; + } + + /** + * JSON-encodes a request body, throwing on failure. + * + * @param array $body JSON-serializable body. + * @throws RuntimeException When encoding fails. + */ + private function encodeBody(array $body): string + { + $encoded = json_encode($body); + + if ($encoded === false) { + throw new RuntimeException('json_encode failed for ' . $this->vendorName() . ' request body'); + } + + return $encoded; + } + + /** + * Builds the HTTP headers for *arr API requests. + * + * @return array Headers array. + */ + private function buildHeaders(): array + { + return [ + 'Content-Type: application/json', + 'Accept: application/json', + 'X-Api-Key: ' . $this->apiKey, + ]; + } +} diff --git a/src/Arr/BazarrClient.php b/src/Arr/BazarrClient.php index 86183bb..c37e473 100644 --- a/src/Arr/BazarrClient.php +++ b/src/Arr/BazarrClient.php @@ -4,7 +4,6 @@ namespace Phlix\Shared\Arr; -use Psr\Log\LoggerInterface; use RuntimeException; /** @@ -13,31 +12,14 @@ * @package Phlix\Shared\Arr * @since 0.4.0 */ -class BazarrClient +class BazarrClient extends AbstractArrClient { - private string $baseUrl; - private string $apiKey; - private ?LoggerInterface $logger; - private int $timeout; - /** - * Creates a new BazarrClient. - * - * @param string $baseUrl Base URL of the Bazarr instance (e.g. `http://localhost:6767`). - * @param string $apiKey API key for authentication. - * @param LoggerInterface|null $logger Optional logger instance. - * @param int $timeout Request timeout in seconds (default 30). + * {@inheritdoc} */ - public function __construct( - string $baseUrl, - string $apiKey, - ?LoggerInterface $logger = null, - int $timeout = 30 - ) { - $this->baseUrl = rtrim($baseUrl, '/'); - $this->apiKey = $apiKey; - $this->logger = $logger; - $this->timeout = $timeout; + protected function vendorName(): string + { + return 'Bazarr'; } /** @@ -114,144 +96,4 @@ public function testConnection(): bool return false; } } - - /** - * Performs a GET request. - * - * @param string $path Request path. - * @return array Decoded JSON response. - * @throws RuntimeException On network or HTTP errors. - */ - protected function get(string $path): array - { - $url = $this->baseUrl . $path; - assert($url !== ''); - - $ch = curl_init(); - if ($ch === false) { - throw new RuntimeException('curl_init() failed'); - } - - curl_setopt_array($ch, [ - CURLOPT_URL => $url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => $this->timeout, - CURLOPT_HTTPHEADER => $this->buildHeaders(), - ]); - - /** @var string|false */ - $responseBody = curl_exec($ch); - $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); - $curlErrno = curl_errno($ch); - $curlError = curl_error($ch); - curl_close($ch); - - if ($responseBody === false || $curlErrno !== 0) { - throw new RuntimeException('cURL error: ' . $curlError, $curlErrno); - } - - if ($httpCode === 401) { - throw new RuntimeException('Bazarr API authentication failed (401)'); - } - - if ($httpCode === 404) { - throw new RuntimeException('Bazarr API resource not found (404): ' . $path); - } - - if ($httpCode >= 400) { - throw new RuntimeException('Bazarr API error: HTTP ' . $httpCode); - } - - if ($responseBody === '') { - return []; - } - - $decoded = json_decode($responseBody, true); - if (!is_array($decoded)) { - throw new RuntimeException('Invalid JSON response from Bazarr'); - } - - return $decoded; - } - - /** - * Performs a POST request with a JSON body. - * - * @param string $path Request path. - * @param array $body JSON-serializable body. - * @return array Decoded JSON response. - * @throws RuntimeException On network or HTTP errors. - */ - protected function post(string $path, array $body): array - { - $url = $this->baseUrl . $path; - assert($url !== ''); - $encodedBody = json_encode($body); - - if ($encodedBody === false) { - throw new RuntimeException('json_encode failed for Bazarr request body'); - } - - $ch = curl_init(); - if ($ch === false) { - throw new RuntimeException('curl_init() failed'); - } - - curl_setopt_array($ch, [ - CURLOPT_URL => $url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => $this->timeout, - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => $encodedBody, - CURLOPT_HTTPHEADER => $this->buildHeaders(), - ]); - - /** @var string|false */ - $responseBody = curl_exec($ch); - $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); - $curlErrno = curl_errno($ch); - $curlError = curl_error($ch); - curl_close($ch); - - if ($responseBody === false || $curlErrno !== 0) { - throw new RuntimeException('cURL error: ' . $curlError, $curlErrno); - } - - if ($httpCode === 401) { - throw new RuntimeException('Bazarr API authentication failed (401)'); - } - - if ($httpCode === 404) { - throw new RuntimeException('Bazarr API resource not found (404): ' . $path); - } - - if ($httpCode >= 400) { - throw new RuntimeException('Bazarr API error: HTTP ' . $httpCode); - } - - if ($responseBody === '') { - return []; - } - - $decoded = json_decode($responseBody, true); - if (!is_array($decoded)) { - throw new RuntimeException('Invalid JSON response from Bazarr'); - } - - return $decoded; - } - - /** - * Builds the HTTP headers for Bazarr API requests. - * - * @return array Headers array. - */ - private function buildHeaders(): array - { - return [ - 'Content-Type: application/json', - 'Accept: application/json', - 'X-Api-Key: ' . $this->apiKey, - ]; - } } diff --git a/src/Arr/ProwlarrClient.php b/src/Arr/ProwlarrClient.php index b5522b9..7534fbb 100644 --- a/src/Arr/ProwlarrClient.php +++ b/src/Arr/ProwlarrClient.php @@ -4,7 +4,6 @@ namespace Phlix\Shared\Arr; -use Psr\Log\LoggerInterface; use RuntimeException; /** @@ -13,31 +12,14 @@ * @package Phlix\Shared\Arr * @since 0.4.0 */ -class ProwlarrClient +class ProwlarrClient extends AbstractArrClient { - private string $baseUrl; - private string $apiKey; - private ?LoggerInterface $logger; - private int $timeout; - /** - * Creates a new ProwlarrClient. - * - * @param string $baseUrl Base URL of the Prowlarr instance (e.g. `http://localhost:9696`). - * @param string $apiKey API key for authentication. - * @param LoggerInterface|null $logger Optional logger instance. - * @param int $timeout Request timeout in seconds (default 30). + * {@inheritdoc} */ - public function __construct( - string $baseUrl, - string $apiKey, - ?LoggerInterface $logger = null, - int $timeout = 30 - ) { - $this->baseUrl = rtrim($baseUrl, '/'); - $this->apiKey = $apiKey; - $this->logger = $logger; - $this->timeout = $timeout; + protected function vendorName(): string + { + return 'Prowlarr'; } /** @@ -106,144 +88,4 @@ public function testConnection(): bool return false; } } - - /** - * Performs a GET request. - * - * @param string $path Request path. - * @return array Decoded JSON response. - * @throws RuntimeException On network or HTTP errors. - */ - protected function get(string $path): array - { - $url = $this->baseUrl . $path; - assert($url !== ''); - - $ch = curl_init(); - if ($ch === false) { - throw new RuntimeException('curl_init() failed'); - } - - curl_setopt_array($ch, [ - CURLOPT_URL => $url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => $this->timeout, - CURLOPT_HTTPHEADER => $this->buildHeaders(), - ]); - - /** @var string|false */ - $responseBody = curl_exec($ch); - $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); - $curlErrno = curl_errno($ch); - $curlError = curl_error($ch); - curl_close($ch); - - if ($responseBody === false || $curlErrno !== 0) { - throw new RuntimeException('cURL error: ' . $curlError, $curlErrno); - } - - if ($httpCode === 401) { - throw new RuntimeException('Prowlarr API authentication failed (401)'); - } - - if ($httpCode === 404) { - throw new RuntimeException('Prowlarr API resource not found (404): ' . $path); - } - - if ($httpCode >= 400) { - throw new RuntimeException('Prowlarr API error: HTTP ' . $httpCode); - } - - if ($responseBody === '') { - return []; - } - - $decoded = json_decode($responseBody, true); - if (!is_array($decoded)) { - throw new RuntimeException('Invalid JSON response from Prowlarr'); - } - - return $decoded; - } - - /** - * Performs a POST request with a JSON body. - * - * @param string $path Request path. - * @param array $body JSON-serializable body. - * @return array Decoded JSON response. - * @throws RuntimeException On network or HTTP errors. - */ - protected function post(string $path, array $body): array - { - $url = $this->baseUrl . $path; - assert($url !== ''); - $encodedBody = json_encode($body); - - if ($encodedBody === false) { - throw new RuntimeException('json_encode failed for Prowlarr request body'); - } - - $ch = curl_init(); - if ($ch === false) { - throw new RuntimeException('curl_init() failed'); - } - - curl_setopt_array($ch, [ - CURLOPT_URL => $url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => $this->timeout, - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => $encodedBody, - CURLOPT_HTTPHEADER => $this->buildHeaders(), - ]); - - /** @var string|false */ - $responseBody = curl_exec($ch); - $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); - $curlErrno = curl_errno($ch); - $curlError = curl_error($ch); - curl_close($ch); - - if ($responseBody === false || $curlErrno !== 0) { - throw new RuntimeException('cURL error: ' . $curlError, $curlErrno); - } - - if ($httpCode === 401) { - throw new RuntimeException('Prowlarr API authentication failed (401)'); - } - - if ($httpCode === 404) { - throw new RuntimeException('Prowlarr API resource not found (404): ' . $path); - } - - if ($httpCode >= 400) { - throw new RuntimeException('Prowlarr API error: HTTP ' . $httpCode); - } - - if ($responseBody === '') { - return []; - } - - $decoded = json_decode($responseBody, true); - if (!is_array($decoded)) { - throw new RuntimeException('Invalid JSON response from Prowlarr'); - } - - return $decoded; - } - - /** - * Builds the HTTP headers for Prowlarr API requests. - * - * @return array Headers array. - */ - private function buildHeaders(): array - { - return [ - 'Content-Type: application/json', - 'Accept: application/json', - 'X-Api-Key: ' . $this->apiKey, - ]; - } } diff --git a/src/Arr/RadarrClient.php b/src/Arr/RadarrClient.php index b2113f2..6a9aafb 100644 --- a/src/Arr/RadarrClient.php +++ b/src/Arr/RadarrClient.php @@ -4,7 +4,6 @@ namespace Phlix\Shared\Arr; -use Psr\Log\LoggerInterface; use RuntimeException; /** @@ -13,31 +12,14 @@ * @package Phlix\Shared\Arr * @since 0.4.0 */ -class RadarrClient implements ArrClientInterface +class RadarrClient extends AbstractArrClient implements ArrClientInterface { - private string $baseUrl; - private string $apiKey; - private ?LoggerInterface $logger; - private int $timeout; - /** - * Creates a new RadarrClient. - * - * @param string $baseUrl Base URL of the Radarr instance (e.g. `http://localhost:7878`). - * @param string $apiKey API key for authentication. - * @param LoggerInterface|null $logger Optional logger instance. - * @param int $timeout Request timeout in seconds (default 30). + * {@inheritdoc} */ - public function __construct( - string $baseUrl, - string $apiKey, - ?LoggerInterface $logger = null, - int $timeout = 30 - ) { - $this->baseUrl = rtrim($baseUrl, '/'); - $this->apiKey = $apiKey; - $this->logger = $logger; - $this->timeout = $timeout; + protected function vendorName(): string + { + return 'Radarr'; } /** @@ -229,271 +211,4 @@ public function testConnection(): bool return false; } } - - /** - * Performs a GET request. - * - * @param string $path Request path. - * @return array Decoded JSON response. - * @throws RuntimeException On network or HTTP errors. - */ - protected function get(string $path): array - { - $url = $this->baseUrl . $path; - assert($url !== ''); - - $ch = curl_init(); - if ($ch === false) { - throw new RuntimeException('curl_init() failed'); - } - - curl_setopt_array($ch, [ - CURLOPT_URL => $url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => $this->timeout, - CURLOPT_HTTPHEADER => $this->buildHeaders(), - ]); - - /** @var string|false */ - $responseBody = curl_exec($ch); - $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); - $curlErrno = curl_errno($ch); - $curlError = curl_error($ch); - curl_close($ch); - - if ($responseBody === false || $curlErrno !== 0) { - throw new RuntimeException('cURL error: ' . $curlError, $curlErrno); - } - - if ($httpCode === 401) { - throw new RuntimeException('Radarr API authentication failed (401)'); - } - - if ($httpCode === 404) { - throw new RuntimeException('Radarr API resource not found (404): ' . $path); - } - - if ($httpCode >= 400) { - throw new RuntimeException('Radarr API error: HTTP ' . $httpCode); - } - - if ($responseBody === '') { - return []; - } - - $decoded = json_decode($responseBody, true); - if (!is_array($decoded)) { - throw new RuntimeException('Invalid JSON response from Radarr'); - } - - return $decoded; - } - - /** - * Performs a POST request with a JSON body. - * - * @param string $path Request path. - * @param array $body JSON-serializable body. - * @return array Decoded JSON response. - * @throws RuntimeException On network or HTTP errors. - */ - protected function post(string $path, array $body): array - { - $url = $this->baseUrl . $path; - assert($url !== ''); - $encodedBody = json_encode($body); - - if ($encodedBody === false) { - throw new RuntimeException('json_encode failed for Radarr request body'); - } - - $ch = curl_init(); - if ($ch === false) { - throw new RuntimeException('curl_init() failed'); - } - - curl_setopt_array($ch, [ - CURLOPT_URL => $url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => $this->timeout, - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => $encodedBody, - CURLOPT_HTTPHEADER => $this->buildHeaders(), - ]); - - /** @var string|false */ - $responseBody = curl_exec($ch); - $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); - $curlErrno = curl_errno($ch); - $curlError = curl_error($ch); - curl_close($ch); - - if ($responseBody === false || $curlErrno !== 0) { - throw new RuntimeException('cURL error: ' . $curlError, $curlErrno); - } - - if ($httpCode === 401) { - throw new RuntimeException('Radarr API authentication failed (401)'); - } - - if ($httpCode === 404) { - throw new RuntimeException('Radarr API resource not found (404): ' . $path); - } - - if ($httpCode >= 400) { - throw new RuntimeException('Radarr API error: HTTP ' . $httpCode); - } - - if ($responseBody === '') { - return []; - } - - $decoded = json_decode($responseBody, true); - if (!is_array($decoded)) { - throw new RuntimeException('Invalid JSON response from Radarr'); - } - - return $decoded; - } - - /** - * Performs a PUT request with a JSON body. - * - * @param string $path Request path. - * @param array $body JSON-serializable body. - * @return array Decoded JSON response. - * @throws RuntimeException On network or HTTP errors. - */ - protected function put(string $path, array $body): array - { - $url = $this->baseUrl . $path; - assert($url !== ''); - $encodedBody = json_encode($body); - - if ($encodedBody === false) { - throw new RuntimeException('json_encode failed for Radarr request body'); - } - - $ch = curl_init(); - if ($ch === false) { - throw new RuntimeException('curl_init() failed'); - } - - curl_setopt_array($ch, [ - CURLOPT_URL => $url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => $this->timeout, - CURLOPT_CUSTOMREQUEST => 'PUT', - CURLOPT_POSTFIELDS => $encodedBody, - CURLOPT_HTTPHEADER => $this->buildHeaders(), - ]); - - /** @var string|false */ - $responseBody = curl_exec($ch); - $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); - $curlErrno = curl_errno($ch); - $curlError = curl_error($ch); - curl_close($ch); - - if ($responseBody === false || $curlErrno !== 0) { - throw new RuntimeException('cURL error: ' . $curlError, $curlErrno); - } - - if ($httpCode === 401) { - throw new RuntimeException('Radarr API authentication failed (401)'); - } - - if ($httpCode === 404) { - throw new RuntimeException('Radarr API resource not found (404): ' . $path); - } - - if ($httpCode >= 400) { - throw new RuntimeException('Radarr API error: HTTP ' . $httpCode); - } - - if ($responseBody === '') { - return []; - } - - $decoded = json_decode($responseBody, true); - if (!is_array($decoded)) { - throw new RuntimeException('Invalid JSON response from Radarr'); - } - - return $decoded; - } - - /** - * Performs a DELETE request. - * - * @param string $path Request path. - * @return array Decoded JSON response. - * @throws RuntimeException On network or HTTP errors. - */ - protected function delete(string $path): array - { - $url = $this->baseUrl . $path; - assert($url !== ''); - - $ch = curl_init(); - if ($ch === false) { - throw new RuntimeException('curl_init() failed'); - } - - curl_setopt_array($ch, [ - CURLOPT_URL => $url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => $this->timeout, - CURLOPT_CUSTOMREQUEST => 'DELETE', - CURLOPT_HTTPHEADER => $this->buildHeaders(), - ]); - - /** @var string|false */ - $responseBody = curl_exec($ch); - $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); - $curlErrno = curl_errno($ch); - $curlError = curl_error($ch); - curl_close($ch); - - if ($responseBody === false || $curlErrno !== 0) { - throw new RuntimeException('cURL error: ' . $curlError, $curlErrno); - } - - if ($httpCode === 401) { - throw new RuntimeException('Radarr API authentication failed (401)'); - } - - if ($httpCode === 404) { - throw new RuntimeException('Radarr API resource not found (404): ' . $path); - } - - if ($httpCode >= 400) { - throw new RuntimeException('Radarr API error: HTTP ' . $httpCode); - } - - if ($responseBody === '') { - return []; - } - - $decoded = json_decode($responseBody, true); - if (!is_array($decoded)) { - throw new RuntimeException('Invalid JSON response from Radarr'); - } - - return $decoded; - } - - /** - * Builds the HTTP headers for Radarr API requests. - * - * @return array Headers array. - */ - private function buildHeaders(): array - { - return [ - 'Content-Type: application/json', - 'Accept: application/json', - 'X-Api-Key: ' . $this->apiKey, - ]; - } } diff --git a/src/Arr/SonarrClient.php b/src/Arr/SonarrClient.php index 0f16ad5..df85507 100644 --- a/src/Arr/SonarrClient.php +++ b/src/Arr/SonarrClient.php @@ -4,7 +4,6 @@ namespace Phlix\Shared\Arr; -use Psr\Log\LoggerInterface; use RuntimeException; /** @@ -13,31 +12,14 @@ * @package Phlix\Shared\Arr * @since 0.4.0 */ -class SonarrClient implements ArrClientInterface +class SonarrClient extends AbstractArrClient implements ArrClientInterface { - private string $baseUrl; - private string $apiKey; - private ?LoggerInterface $logger; - private int $timeout; - /** - * Creates a new SonarrClient. - * - * @param string $baseUrl Base URL of the Sonarr instance (e.g. `http://localhost:8989`). - * @param string $apiKey API key for authentication. - * @param LoggerInterface|null $logger Optional logger instance. - * @param int $timeout Request timeout in seconds (default 30). + * {@inheritdoc} */ - public function __construct( - string $baseUrl, - string $apiKey, - ?LoggerInterface $logger = null, - int $timeout = 30 - ) { - $this->baseUrl = rtrim($baseUrl, '/'); - $this->apiKey = $apiKey; - $this->logger = $logger; - $this->timeout = $timeout; + protected function vendorName(): string + { + return 'Sonarr'; } /** @@ -176,144 +158,4 @@ public function testConnection(): bool return false; } } - - /** - * Performs a GET request. - * - * @param string $path Request path. - * @return array Decoded JSON response. - * @throws RuntimeException On network or HTTP errors. - */ - protected function get(string $path): array - { - $url = $this->baseUrl . $path; - assert($url !== ''); - - $ch = curl_init(); - if ($ch === false) { - throw new RuntimeException('curl_init() failed'); - } - - curl_setopt_array($ch, [ - CURLOPT_URL => $url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => $this->timeout, - CURLOPT_HTTPHEADER => $this->buildHeaders(), - ]); - - /** @var string|false */ - $responseBody = curl_exec($ch); - $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); - $curlErrno = curl_errno($ch); - $curlError = curl_error($ch); - curl_close($ch); - - if ($responseBody === false || $curlErrno !== 0) { - throw new RuntimeException('cURL error: ' . $curlError, $curlErrno); - } - - if ($httpCode === 401) { - throw new RuntimeException('Sonarr API authentication failed (401)'); - } - - if ($httpCode === 404) { - throw new RuntimeException('Sonarr API resource not found (404): ' . $path); - } - - if ($httpCode >= 400) { - throw new RuntimeException('Sonarr API error: HTTP ' . $httpCode); - } - - if ($responseBody === '') { - return []; - } - - $decoded = json_decode($responseBody, true); - if (!is_array($decoded)) { - throw new RuntimeException('Invalid JSON response from Sonarr'); - } - - return $decoded; - } - - /** - * Performs a POST request with a JSON body. - * - * @param string $path Request path. - * @param array $body JSON-serializable body. - * @return array Decoded JSON response. - * @throws RuntimeException On network or HTTP errors. - */ - protected function post(string $path, array $body): array - { - $url = $this->baseUrl . $path; - assert($url !== ''); - $encodedBody = json_encode($body); - - if ($encodedBody === false) { - throw new RuntimeException('json_encode failed for Sonarr request body'); - } - - $ch = curl_init(); - if ($ch === false) { - throw new RuntimeException('curl_init() failed'); - } - - curl_setopt_array($ch, [ - CURLOPT_URL => $url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => $this->timeout, - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => $encodedBody, - CURLOPT_HTTPHEADER => $this->buildHeaders(), - ]); - - /** @var string|false */ - $responseBody = curl_exec($ch); - $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); - $curlErrno = curl_errno($ch); - $curlError = curl_error($ch); - curl_close($ch); - - if ($responseBody === false || $curlErrno !== 0) { - throw new RuntimeException('cURL error: ' . $curlError, $curlErrno); - } - - if ($httpCode === 401) { - throw new RuntimeException('Sonarr API authentication failed (401)'); - } - - if ($httpCode === 404) { - throw new RuntimeException('Sonarr API resource not found (404): ' . $path); - } - - if ($httpCode >= 400) { - throw new RuntimeException('Sonarr API error: HTTP ' . $httpCode); - } - - if ($responseBody === '') { - return []; - } - - $decoded = json_decode($responseBody, true); - if (!is_array($decoded)) { - throw new RuntimeException('Invalid JSON response from Sonarr'); - } - - return $decoded; - } - - /** - * Builds the HTTP headers for Sonarr API requests. - * - * @return array Headers array. - */ - private function buildHeaders(): array - { - return [ - 'Content-Type: application/json', - 'Accept: application/json', - 'X-Api-Key: ' . $this->apiKey, - ]; - } }