Skip to content

Commit

Permalink
Some refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
leepeuker committed Jun 28, 2023
1 parent c9bcc8a commit 08a2796
Show file tree
Hide file tree
Showing 10 changed files with 233 additions and 276 deletions.
1 change: 1 addition & 0 deletions bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
\Movary\JobQueue\JobQueueScheduler::class => DI\factory([Factory::class, 'createJobQueueScheduler']),
\Movary\Api\Tmdb\TmdbClient::class => DI\factory([Factory::class, 'createTmdbApiClient']),
\Movary\Api\Plex\PlexLocalServerClient::class => DI\factory([Factory::class, 'createPlexLocalServerClient']),
\Movary\Api\Plex\PlexTvClient::class => DI\factory([Factory::class, 'createPlexTvClient']),
\Movary\Api\Plex\PlexApi::class => DI\factory([Factory::class, 'createPlexApi']),
\Movary\Service\UrlGenerator::class => DI\factory([Factory::class, 'createUrlGenerator']),
\Movary\Service\Export\ExportService::class => DI\factory([Factory::class, 'createExportService']),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
class PlexAccessToken
{
public function __construct(
private readonly string $plexAccessToken
){
private readonly string $plexAccessToken,
) {
}

public static function createPlexAccessToken(string $plexAccessToken) : self
Expand All @@ -18,4 +18,4 @@ public function getPlexAccessTokenAsString() : string
{
return $this->plexAccessToken;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@ class PlexAccount
public function __construct(
private readonly int $plexId,
private readonly string $username,
private readonly string $email
){
) {
}

public static function createplexAccount(int $plexId, string $username, string $email) : self
public static function createPlexAccount(int $plexId, string $username) : self
{
return new self($plexId, $username, $email);
return new self($plexId, $username);
}

public function getPlexId() : int
Expand All @@ -25,9 +24,4 @@ public function getPlexUsername() : string
{
return $this->username;
}

public function getPlexEmail() : string
{
return $this->email;
}
}
}
110 changes: 65 additions & 45 deletions src/Api/Plex/PlexApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,34 @@

namespace Movary\Api\Plex;

use Movary\Api\Plex\Exception\PlexNotFoundError;
use Movary\Api\Plex\Dto\PlexAccessToken;
use Movary\Api\Plex\Dto\PlexAccount;
use Movary\Api\Plex\Dto\PlexItem;
use Movary\Api\Plex\Exception\PlexAuthenticationError;
use Movary\Api\Plex\Exception\PlexNoClientIdentifier;
use Movary\Api\Plex\Exception\PlexNotFoundError;
use Movary\Domain\User\Service\Authentication;
use Movary\Domain\User\UserApi;
use Movary\Service\ServerSettings;
use Movary\ValueObject\PersonalRating;
use Psr\Log\LoggerInterface;

/**
* @link https://github.com/Arcanemagus/plex-api/wiki Comprehensive documentation of the Plex API
* @link https://github.com/Arcanemagus/plex-api/wiki Comprehensive documentation of the Plex API
* @link https://forums.plex.tv/t/authenticating-with-plex/609370 For more info on authenticating with Plex
*
*
* The authentication flow is as follows:
* 1. The user visits /settings/plex
* 2. The settingsController will check if an access token exists in the database.
* 2. The settingsController will check if an access token exists in the database.
* 3. If the acess token does not exist or the access token is invalid, a new authentication URL will be generated with generatePlexAuthenticationUrl(). See more on info there.
* 4. The URL will be returned to the settingsController and injected in the Plex settings page.
* 5. When the user clicks on the 'login' button, they'll be redirected to the url, authenticate and return to url stated in the forwardUrl parameter.
* 6. The user returns to the URL callback and the callback controller will fetch the Plex Access Token.
* 6. The user returns to the URL callback and the callback controller will fetch the Plex Access Token.
* 7. After fetching this, and storing it in the database, the user returns to the Plex Settings page.
*/
class PlexApi
{
private const BASE_URL = 'https://app.plex.tv/auth#?';

public function __construct(
private readonly Authentication $authenticationService,
private readonly ServerSettings $serverSettings,
Expand All @@ -40,73 +41,92 @@ public function __construct(
) {
}

/**
* 1. A HTTP POST request will be sent to the Plex API, requesting a client ID and a client Code. The code is usually valid for 1800 seconds or 15 minutes. After 15min, a new code has to be requested.
* 2. Both the pin ID and code will be stored in the database for later use in the plexCallback controller
* 3. Based on the info returned by the Plex API, a new url will be generated, which looks like this: `https://app.plex.tv/auth#?clientID=<clientIdentifier>&code=<clientCode>&context[device][product]=<AppName>&forwardUrl=<urlCallback>`
* 4. The URL is returned to the settingsController
*/
public function generatePlexAuthenticationUrl() : ?string
public function findPlexAccessToken(string $plexPinId, string $temporaryPlexClientCode) : ?PlexAccessToken
{
try {
$base_url = 'https://app.plex.tv/auth#?';
$plexAuthenticationData = $this->plexTvClient->sendPostRequest('/pins');
$this->userApi->updatePlexClientId($this->authenticationService->getCurrentUserId(), $plexAuthenticationData['id']);
$this->userApi->updateTemporaryPlexClientCode($this->authenticationService->getCurrentUserId(), $plexAuthenticationData['code']);
$plexAppName = $plexAuthenticationData['product'];
$plexClientIdentifier = $plexAuthenticationData['clientIdentifier'];
$plexTemporaryClientCode = $plexAuthenticationData['code'];
$applicationUrl = $this->serverSettings->getApplicationUrl() ?? $_SERVER['SERVER_NAME'];
$protocol = str_starts_with($applicationUrl, 'http://') ? '' : (str_starts_with($applicationUrl, 'https://') ? '' : (stripos($_SERVER['SERVER_PROTOCOL'],'https') === 0 ? 'https://' : 'http://'));
$urlCallback = $protocol . $applicationUrl . '/settings/plex/callback';
$response = $base_url . 'clientID=' . urlencode($plexClientIdentifier) . '&code=' . urlencode((string)$plexTemporaryClientCode) . '&' . urlencode('context[device][product]') . '=' . urlencode($plexAppName) . '&forwardUrl=' . urlencode($urlCallback);
return $response;
} catch (PlexNoClientIdentifier) {
return null;
}
}

public function fetchPlexAccessToken(string $plexPinId, string $temporaryPlexClientCode) : ?PlexAccessToken
{
$query = [
$headers = [
'code' => $temporaryPlexClientCode,
];

try {
$plexRequest = $this->plexTvClient->sendGetRequest('/pins/' . $plexPinId, [], $query);
$plexAccessCode = PlexAccessToken::createPlexAccessToken($plexRequest['authToken']);
return $plexAccessCode;
$plexRequest = $this->plexTvClient->get('/pins/' . $plexPinId, $headers);
} catch (PlexNotFoundError) {
$this->logger->error('Plex pin does not exist');

return null;
}

return PlexAccessToken::createPlexAccessToken($plexRequest['authToken']);
}

public function fetchPlexAccount(PlexAccessToken $plexAccessToken) : ?PlexAccount
public function findPlexAccount(PlexAccessToken $plexAccessToken) : ?PlexAccount
{
$query = [
$headers = [
'X-Plex-Token' => $plexAccessToken->getPlexAccessTokenAsString()
];

try {
$accountData = $this->plexTvClient->sendGetRequest('/user', [], $query);
$plexAccount = PlexAccount::createplexAccount((int)$accountData['id'], $accountData['username'], $accountData['email']);
return $plexAccount;
$accountData = $this->plexTvClient->get('/user', $headers);
} catch (PlexAuthenticationError) {
$this->logger->error('Plex access token is invalid');

return null;
}

return PlexAccount::createPlexAccount((int)$accountData['id'], $accountData['username']);
}

/**
* 1. A HTTP POST request will be sent to the Plex API, requesting a client ID and a client Code. The code is usually valid for 1800 seconds or 15 minutes. After 15min, a new code has to be requested.
* 2. Both the pin ID and code will be stored in the database for later use in the plexCallback controller
* 3. Based on the info returned by the Plex API, a new url will be generated, which looks like this: `https://app.plex.tv/auth#?clientID=<clientIdentifier>&code=<clientCode>&context[device][product]=<AppName>&forwardUrl=<urlCallback>`
* 4. The URL is returned to the settingsController
*/
public function generatePlexAuthenticationUrl() : ?string
{
try {
$plexAuthenticationData = $this->plexTvClient->sendPostRequest('/pins');
} catch (PlexNoClientIdentifier) {
return null;
}

$this->userApi->updatePlexClientId($this->authenticationService->getCurrentUserId(), $plexAuthenticationData['id']);
$this->userApi->updateTemporaryPlexClientCode($this->authenticationService->getCurrentUserId(), $plexAuthenticationData['code']);

$plexAppName = $plexAuthenticationData['product'];
$plexClientIdentifier = $plexAuthenticationData['clientIdentifier'];
$plexTemporaryClientCode = $plexAuthenticationData['code'];

$applicationUrl = $this->serverSettings->getApplicationUrl();
if ($applicationUrl === null) {
return null;
}

$urlCallback = $applicationUrl . '/settings/plex/callback';

$getParameters = [
'clientID' => $plexClientIdentifier,
'code' => (string)$plexTemporaryClientCode,
'context[device][product]' => $plexAppName,
'forwardUrl' => $urlCallback,
];

return self::BASE_URL . http_build_query($getParameters);
}

public function verifyPlexUrl(string $url) : bool
{
$query = [
'X-Plex-Token' => $this->plexAccessToken->getPlexAccessTokenAsString()
];

try {
$this->localClient->sendGetRequest('', $query, [], [], $url);
$this->localClient->sendGetRequest('', $query, $url);

return true;
} catch(PlexAuthenticationError) {
} catch (PlexAuthenticationError) {
$this->logger->error('Plex access token is invalid');

return false;
}
}
}
}
87 changes: 25 additions & 62 deletions src/Api/Plex/PlexLocalServerClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,103 +2,66 @@

namespace Movary\Api\Plex;

use Movary\Api\Plex\Exception\PlexAuthenticationError;
use Movary\Util\Json;
use GuzzleHttp\Client as httpClient;
use GuzzleHttp\Client as HttpClient;
use GuzzleHttp\Exception\ClientException;
use Movary\Api\Plex\Exception\PlexAuthenticationError;
use Movary\Api\Plex\Exception\PlexNoClientIdentifier;
use Movary\Api\Plex\Exception\PlexNotFoundError;
use Psr\Log\LoggerInterface;
use Movary\Util\Json;
use RuntimeException;

class PlexLocalServerClient
{
private const APP_NAME = 'Movary';
private const DEFAULTPOSTANDGETHEADERS = [

private const DEFAULT_HEADERS = [
'accept' => 'application/json',
'Content-Type' => 'application/json'
];

private array $defaultPostAndGetData;

public function __construct(
private readonly httpClient $httpClient,
private readonly HttpClient $httpClient,
private readonly string $plexIdentifier,
private readonly string $application_version,
private readonly string $plexServerUrl
private readonly string $applicationVersion,
) {
$this->defaultPostAndGetData = [
'X-Plex-Client-Identifier' => $this->plexIdentifier,
'X-Plex-Product' => self::APP_NAME,
'X-Plex-Product-Version' => $this->application_version,
'X-Plex-Product-Version' => $this->applicationVersion,
'X-Plex-Platform' => php_uname('s'),
'X-Plex-Platform-Version' => php_uname('v'),
'X-Plex-Provides' => 'Controller',
'strong' => 'true'
];
}

/**
* @throws PlexNotFoundError
* @throws PlexAuthenticationError
* @throws PlexNoClientIdentifier
* @throws RuntimeException
* @psalm-suppress InvalidReturnType
*/
public function sendGetRequest(string $relativeUrl, ?array $customGetQuery = [], array $customGetData = [], array $customGetHeaders = [], ?string $customBaseUrl = null) : array
{
public function sendGetRequest(
string $userId,
?array $customGetQuery = [],
) : array {
if ($this->plexIdentifier === '') {
throw PlexNoClientIdentifier::create();
}
$baseUrl = $customBaseUrl ?? $this->plexServerUrl;
$url = $baseUrl . $relativeUrl;
$data = array_merge($this->defaultPostAndGetData, $customGetData);
$httpHeaders = array_merge(self::DEFAULTPOSTANDGETHEADERS, $customGetHeaders);
$options = [
'form_params' => $data,

$requestUrl = $baseUrl . $relativeUrl;
$requestOptions = [
'form_params' => $this->defaultPostAndGetData,
'query' => $customGetQuery,
'headers' => $httpHeaders
'headers' => self::DEFAULT_HEADERS
];
try {
$response = $this->httpClient->request('GET', $url, $options);
return Json::decode((string)$response->getBody());
} catch (ClientException $e) {
match(true) {
$e->getCode() === 401 => throw PlexAuthenticationError::create(),
$e->getCode() === 404 => throw PlexNotFoundError::create($url),
default => throw new RuntimeException('Plex API error. Response message: '. $e->getMessage()),
};
}
}

/**
* @throws PlexNotFoundError
* @throws PlexAuthenticationError
* @throws PlexNoClientIdentifier
* @throws RuntimeException
* @psalm-suppress InvalidReturnType
*/
public function sendPostRequest(string $relativeUrl, array $customPostData = [], array $customPostHeaders = [], string $customBaseUrl = null) : array
{
if ($this->plexIdentifier === '') {
throw PlexNoClientIdentifier::create();
}
$baseUrl = $customBaseUrl ?? $this->plexServerUrl;
$url = $baseUrl . $relativeUrl;
$postData = array_merge($this->defaultPostAndGetData, $customPostData);
$httpHeaders = array_merge(self::DEFAULTPOSTANDGETHEADERS, $customPostHeaders);
$options = [
'form_params' => $postData,
'headers' => $httpHeaders
];
try {
$response = $this->httpClient->request('POST', $url, $options);
return Json::decode((string)$response->getBody());
$response = $this->httpClient->request('GET', $requestUrl, $requestOptions);
} catch (ClientException $e) {
match(true) {
match (true) {
$e->getCode() === 401 => throw PlexAuthenticationError::create(),
$e->getCode() === 404 => throw PlexNotFoundError::create($url),
default => throw new RuntimeException('Plex API error. Response message: '. $e->getMessage()),
$e->getCode() === 404 => throw PlexNotFoundError::create($requestUrl),
default => throw new RuntimeException('Plex API error. Response message: ' . $e->getMessage()),
};
}

return Json::decode((string)$response->getBody());
}
}
}
Loading

0 comments on commit 08a2796

Please sign in to comment.