WIP: rewrite to feat NC 30 #2

Draft
Xefir wants to merge 2 commits from rewrite into master
27 changed files with 4427 additions and 667 deletions
Showing only changes of commit 4e0d69385f - Show all commits

3
.gitignore vendored
View File

@ -18,3 +18,6 @@ js/
build/ build/
coverage/ coverage/
utils/docker-ci utils/docker-ci
vendor/
.php-cs-fixer.cache

31
.php-cs-fixer.dist.php Normal file
View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
require_once './vendor/autoload.php';
use Nextcloud\CodingStandard\Config;
class MyConfig extends Config
{
public function getRules(): array
{
$rules = parent::getRules();
$rules['@PhpCsFixer'] = true;
$rules['curly_braces_position']['classes_opening_brace'] = 'next_line_unless_newline_at_signature_end';
$rules['phpdoc_to_comment'] = false;
return $rules;
}
}
$config = new MyConfig();
$config
->getFinder()
->notPath('build')
->notPath('l10n')
->notPath('node_modules')
->notPath('src')
->notPath('vendor')
->in(__DIR__);
return $config;

View File

@ -1,7 +1,9 @@
<?php <?php
declare(strict_types=1);
/** /**
* Radio App * Radio App.
* *
* @author Jonas Heinrich * @author Jonas Heinrich
* @copyright 2021 Jonas Heinrich <onny@project-insanity.org> * @copyright 2021 Jonas Heinrich <onny@project-insanity.org>
@ -18,74 +20,71 @@
* *
* You should have received a copy of the GNU Affero General Public * You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/ */
return [ return [
'resources' => [ 'resources' => [
'favorite' => ['url' => '/api/favorites'], 'favorite' => ['url' => '/api/favorites'],
'recent' => ['url' => '/api/recent'], 'recent' => ['url' => '/api/recent'],
'export' => ['url' => '/export'], 'export' => ['url' => '/export'],
'station' => ['url' => '/station'], 'station' => ['url' => '/station'],
], ],
'routes' => [ 'routes' => [
// Web page templates
[
'name' => 'page#index',
'url' => '/',
'verb' => 'GET',
],
[
'name' => 'page#index',
'url' => '/top',
'verb' => 'GET',
'postfix' => 'top',
],
[
'name' => 'page#index',
'url' => '/recent',
'verb' => 'GET',
'postfix' => 'recent',
],
[
'name' => 'page#index',
'url' => '/new',
'verb' => 'GET',
'postfix' => 'new',
],
[
'name' => 'page#index',
'url' => '/favorites',
'verb' => 'GET',
'postfix' => 'favorites',
],
[
'name' => 'page#index',
'url' => '/categories',
'verb' => 'GET',
'postfix' => 'categories',
],
[
'name' => 'page#index',
'url' => '/search',
'verb' => 'GET',
'postfix' => 'search',
],
// Web page templates // Api
[ [
'name' => 'page#index', 'name' => 'favorite_api#preflighted_cors',
'url' => '/', 'url' => '/api/0.1/{path}',
'verb' => 'GET' 'verb' => 'OPTIONS',
], 'requirements' => ['path' => '.+'],
[ ],
'name' => 'page#index', [
'url' => '/top', 'name' => 'recent_api#preflighted_cors',
'verb' => 'GET', 'url' => '/api/0.1/{path}',
'postfix' => 'top' 'verb' => 'OPTIONS',
], 'requirements' => ['path' => '.+'],
[ ],
'name' => 'page#index', ],
'url' => '/recent',
'verb' => 'GET',
'postfix' => 'recent'
],
[
'name' => 'page#index',
'url' => '/new',
'verb' => 'GET',
'postfix' => 'new'
],
[
'name' => 'page#index',
'url' => '/favorites',
'verb' => 'GET',
'postfix' => 'favorites'
],
[
'name' => 'page#index',
'url' => '/categories',
'verb' => 'GET',
'postfix' => 'categories'
],
[
'name' => 'page#index',
'url' => '/search',
'verb' => 'GET',
'postfix' => 'search',
],
// Api
[
'name' => 'favorite_api#preflighted_cors',
'url' => '/api/0.1/{path}',
'verb' => 'OPTIONS',
'requirements' => ['path' => '.+']
],
[
'name' => 'recent_api#preflighted_cors',
'url' => '/api/0.1/{path}',
'verb' => 'OPTIONS',
'requirements' => ['path' => '.+']
]
]
]; ];

View File

@ -2,11 +2,35 @@
"name": "onny/radio", "name": "onny/radio",
"description": "Lint config for onny/radio", "description": "Lint config for onny/radio",
"license": "AGPL", "license": "AGPL",
"config": { "autoload": {
"optimize-autoloader": true, "psr-4": {
"classmap-authoritative": true "OCA\\Radio\\": "lib/"
}
}, },
"scripts": { "scripts": {
"lint": "find . -name \\*.php -not -path './vendor/*' -exec php -l \"{}\" \\;" "lint": "find . -name \\*.php -not -path './vendor/*' -not -path './vendor-bin/*' -not -path './build/*' -print0 | xargs -0 -n1 php -l",
"cs:check": "php-cs-fixer fix --dry-run --diff",
"cs:fix": "php-cs-fixer fix",
"psalm": "psalm --threads=1 --no-cache --show-info=true",
"rector": "rector && composer cs:fix"
},
"require": {
"php": "^8.1"
},
"require-dev": {
"nextcloud/ocp": "^30.0.2",
"roave/security-advisories": "dev-latest",
"nextcloud/coding-standard": "^1.3.2",
"nextcloud/rector": "^0.2.1",
"rector/rector": "^1.2.10",
"vimeo/psalm": "^5.26.1",
"doctrine/dbal": "^4.2.1"
},
"config": {
"optimize-autoloader": true,
"classmap-authoritative": true,
"platform": {
"php": "8.1"
}
} }
} }

3567
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
<?php <?php
/** /**
* Radio App * Radio App.
* *
* @author Jonas Heinrich * @author Jonas Heinrich
* @copyright 2021 Jonas Heinrich <onny@project-insanity.org> * @copyright 2021 Jonas Heinrich <onny@project-insanity.org>
@ -18,25 +18,23 @@
* *
* You should have received a copy of the GNU Affero General Public * You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/ */
declare(strict_types=1); declare(strict_types=1);
namespace OCA\Radio\AppInfo; namespace OCA\Radio\AppInfo;
use OC\Security\CSP\ContentSecurityPolicy; use OCA\Radio\Dashboard\RadioWidget;
use OCA\Radio\Search\SearchProvider; use OCA\Radio\Search\SearchProvider;
use OCP\AppFramework\App; use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\IRequest; use OCP\IRequest;
use Psr\Container\ContainerInterface;
use OCA\Radio\Dashboard\RadioWidget; class Application extends App implements IBootstrap
{
class Application extends App implements IBootstrap {
public const APP_ID = 'radio'; public const APP_ID = 'radio';
public function __construct() { public function __construct() {
@ -44,32 +42,11 @@ class Application extends App implements IBootstrap {
} }
public function register(IRegistrationContext $context): void { public function register(IRegistrationContext $context): void {
$context->registerSearchProvider(SearchProvider::class); $context->registerSearchProvider(SearchProvider::class);
$context->registerDashboardWidget(RadioWidget::class); $context->registerDashboardWidget(RadioWidget::class);
$context->registerService('request', static function ($c) { $context->registerService('request', static fn (ContainerInterface $c): mixed => $c->get(IRequest::class));
return $c->get(IRequest::class);
});
$this->registerCsp();
} }
public function boot(IBootContext $context): void { public function boot(IBootContext $context): void {}
}
/**
* Allow radio-browser hosts in the csp
*
* @throws \OCP\AppFramework\QueryException
*/
public function registerCsp() {
$manager = $this->getContainer()->getServer()->getContentSecurityPolicyManager();
$policy = new ContentSecurityPolicy();
$policy->addAllowedConnectDomain('https://de1.api.radio-browser.info');
$policy->addAllowedImageDomain('*');
$policy->addAllowedMediaDomain('*');
$manager->addDefaultPolicy($policy);
}
} }

View File

@ -1,7 +1,9 @@
<?php <?php
declare(strict_types=1);
/** /**
* Radio App * Radio App.
* *
* @author Jonas Heinrich * @author Jonas Heinrich
* @copyright 2021 Jonas Heinrich <onny@project-insanity.org> * @copyright 2021 Jonas Heinrich <onny@project-insanity.org>
@ -18,24 +20,23 @@
* *
* You should have received a copy of the GNU Affero General Public * You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/ */
namespace OCA\Radio\Controller; namespace OCA\Radio\Controller;
use Closure; use OCA\Radio\Service\StationNotFound;
use OCP\AppFramework\Http; use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\DataResponse;
use OCA\Radio\Service\StationNotFound; trait Errors
{
trait Errors { protected function handleNotFound(\Closure $callback): DataResponse {
protected function handleNotFound(Closure $callback): DataResponse {
try { try {
/** @psalm-suppress MixedArgument */
return new DataResponse($callback()); return new DataResponse($callback());
} catch (StationNotFound $e) { } catch (StationNotFound $stationNotFound) {
$message = ['message' => $e->getMessage()]; $message = ['message' => $stationNotFound->getMessage()];
return new DataResponse($message, Http::STATUS_NOT_FOUND); return new DataResponse($message, Http::STATUS_NOT_FOUND);
} }
} }

View File

@ -1,7 +1,9 @@
<?php <?php
declare(strict_types=1);
/** /**
* Radio App * Radio App.
* *
* @author Jonas Heinrich * @author Jonas Heinrich
* @copyright 2021 Jonas Heinrich <onny@project-insanity.org> * @copyright 2021 Jonas Heinrich <onny@project-insanity.org>
@ -18,60 +20,70 @@
* *
* You should have received a copy of the GNU Affero General Public * You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/ */
namespace OCA\Radio\Controller; namespace OCA\Radio\Controller;
use OC;
use OCA\Radio\ExportResponse;
use OCA\Radio\AppInfo\Application; use OCA\Radio\AppInfo\Application;
use OCA\Radio\Service\FavoriteService; use OCA\Radio\Service\FavoriteService;
use OCA\Radio\Service\UserService;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataDownloadResponse;
use OCP\Defaults;
use OCP\HintException;
use OCP\IDateTimeFormatter;
use OCP\IRequest; use OCP\IRequest;
use SimpleXMLElement;
use DOMDocument;
class ExportController extends Controller {
/** @var FavoriteService */
private $service;
/** @var string */
private $userId;
class ExportController extends Controller
{
use Errors; use Errors;
public function __construct(IRequest $request, public function __construct(
FavoriteService $service, IRequest $request,
$userId) { private readonly FavoriteService $service,
private readonly UserService $user,
private readonly Defaults $defaults,
private readonly IDateTimeFormatter $dateTime,
) {
parent::__construct(Application::APP_ID, $request); parent::__construct(Application::APP_ID, $request);
$this->service = $service;
$this->userId = $userId;
} }
/** #[NoAdminRequired]
* @NoAdminRequired public function index(): DataDownloadResponse {
*/ $user = $this->user->getUser();
public function index() { if (is_null($user)) {
throw new HintException('User not logged in');
$xml = new SimpleXMLElement('<?xml version="1.0"?><playlist></playlist>');
$xml->addAttribute('encoding', 'UTF-8');
$trackList = $xml->addChild('trackList');
foreach($this->service->findAll($this->userId) as $station) {
$track = $trackList->addChild('track');
$track->addChild('location', $station->getUrlresolved());
$track->addChild('title', $station->getName());
$track->addChild('image', $station->getFavicon());
} }
$dom = new DOMDocument("1.0"); $xml = new \SimpleXMLElement('<?xml version="1.0"?><playlist></playlist>');
$xml->addAttribute('encoding', 'UTF-8');
$trackList = $xml->addChild('trackList');
if ($trackList instanceof \SimpleXMLElement) {
foreach ($this->service->findAll($user->getUID()) as $station) {
$track = $trackList->addChild('track');
if ($track instanceof \SimpleXMLElement) {
$track->addChild('location', $station->getUrlresolved());
$track->addChild('title', $station->getName());
$track->addChild('image', $station->getFavicon());
}
}
}
$dom = new \DOMDocument('1.0');
$dom->preserveWhiteSpace = false; $dom->preserveWhiteSpace = false;
$dom->formatOutput = true; $dom->formatOutput = true;
/** @psalm-suppress ArgumentTypeCoercion */
$dom->loadXML($xml->asXML()); $dom->loadXML($xml->asXML());
$returnstring = $dom->saveXML();
return new ExportResponse($dom->saveXML()); $userName = $user->getDisplayName();
$productName = $this->defaults->getName();
$date = $this->dateTime->formatDate(time());
$export_name = sprintf('%s Radio Favorites (%s) (%s).xspf', $productName, $userName, $date);
return new DataDownloadResponse($returnstring, $export_name, 'application/xspf+xml');
} }
} }

View File

@ -1,7 +1,9 @@
<?php <?php
declare(strict_types=1);
/** /**
* Radio App * Radio App.
* *
* @author Jonas Heinrich * @author Jonas Heinrich
* @copyright 2021 Jonas Heinrich <onny@project-insanity.org> * @copyright 2021 Jonas Heinrich <onny@project-insanity.org>
@ -18,83 +20,101 @@
* *
* You should have received a copy of the GNU Affero General Public * You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/ */
namespace OCA\Radio\Controller; namespace OCA\Radio\Controller;
use OCA\Radio\AppInfo\Application; use OCA\Radio\AppInfo\Application;
use OCA\Radio\Db\Station;
use OCA\Radio\Service\FavoriteService; use OCA\Radio\Service\FavoriteService;
use OCA\Radio\Service\UserService;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest; use OCP\IRequest;
class FavoriteController extends Controller { class FavoriteController extends Controller
/** @var FavoriteService */ {
private $service;
/** @var string */
private $userId;
use Errors; use Errors;
public function __construct(IRequest $request, public function __construct(
FavoriteService $service, IRequest $request,
$userId) { private readonly FavoriteService $service,
private readonly UserService $user,
) {
parent::__construct(Application::APP_ID, $request); parent::__construct(Application::APP_ID, $request);
$this->service = $service;
$this->userId = $userId;
} }
/** #[NoAdminRequired]
* @NoAdminRequired
*/
public function index(): DataResponse { public function index(): DataResponse {
return new DataResponse($this->service->findAll($this->userId)); return new DataResponse($this->service->findAll($this->user->getUserUID()));
} }
/** #[NoAdminRequired]
* @NoAdminRequired
*/
public function show(int $id): DataResponse { public function show(int $id): DataResponse {
return $this->handleNotFound(function () use ($id) { return $this->handleNotFound(fn (): ?Station => $this->service->find($id, $this->user->getUserUID()));
return $this->service->find($id, $this->userId);
});
} }
/** #[NoAdminRequired]
* @NoAdminRequired public function create(
*/ string $stationuuid,
public function create(string $stationuuid, string $name, string $favicon, string $urlresolved, string $name,
string $bitrate, string $country, string $language, string $homepage, string $favicon,
string $codec, string $tags): DataResponse { string $urlresolved,
return new DataResponse($this->service->create($stationuuid, $name, string $bitrate,
$favicon, $urlresolved, $bitrate, $country, $language, $homepage, $codec, string $country,
$tags, $this->userId)); string $language,
} string $homepage,
string $codec,
string $tags
): DataResponse {
return new DataResponse($this->service->create(
$stationuuid,
$name,
$favicon,
$urlresolved,
$bitrate,
$country,
$language,
$homepage,
$codec,
$tags,
$this->user->getUserUID()
));
}
/** #[NoAdminRequired]
* @NoAdminRequired public function update(
*/ int $id,
public function update(int $id, string $stationuuid, string $stationuuid,
string $name, string $favicon, string $urlresolved, string $name,
string $bitrate, string $country, string $language, string $homepage, string $favicon,
string $codec, string $tags): DataResponse { string $urlresolved,
return $this->handleNotFound(function () use ($id, $stationuuid, $name, string $bitrate,
$favicon, $urlresolved, $bitrate, $country, $language, $homepage, $codec, string $country,
$tags) { string $language,
return $this->service->update($id, $stationuuid, $name, $favicon, string $homepage,
$urlresolved, $bitrate, $country, $language, $homepage, $codec, string $codec,
$tags, $this->userId); string $tags
}); ): DataResponse {
} return $this->handleNotFound(fn (): ?Station => $this->service->update(
$id,
$stationuuid,
$name,
$favicon,
$urlresolved,
$bitrate,
$country,
$language,
$homepage,
$codec,
$tags,
$this->user->getUserUID()
));
}
/** #[NoAdminRequired]
* @NoAdminRequired
*/
public function destroy(int $id): DataResponse { public function destroy(int $id): DataResponse {
return $this->handleNotFound(function () use ($id) { return $this->handleNotFound(fn (): ?Station => $this->service->delete($id, $this->user->getUserUID()));
return $this->service->delete($id, $this->userId);
});
} }
} }

View File

@ -1,7 +1,7 @@
<?php <?php
/** /**
* Radio App * Radio App.
* *
* @author Jonas Heinrich * @author Jonas Heinrich
* @copyright 2021 Jonas Heinrich <onny@project-insanity.org> * @copyright 2021 Jonas Heinrich <onny@project-insanity.org>
@ -18,36 +18,42 @@
* *
* You should have received a copy of the GNU Affero General Public * You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/ */
declare(strict_types=1); declare(strict_types=1);
namespace OCA\Radio\Controller; namespace OCA\Radio\Controller;
use OCP\IRequest; use OCA\Radio\AppInfo\Application;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IRequest;
use OCP\Util; use OCP\Util;
class PageController extends Controller { class PageController extends Controller
{
protected $appName; public function __construct(IRequest $request) {
parent::__construct(Application::APP_ID, $request);
public function __construct($appName, IRequest $request) {
parent::__construct($appName, $request);
$this->appName = $appName;
} }
/** #[NoAdminRequired]
* @NoAdminRequired #[NoCSRFRequired]
* @NoCSRFRequired public function index(): TemplateResponse {
*/ Util::addScript(Application::APP_ID, 'radio-main');
public function index() { Util::addStyle(Application::APP_ID, 'icons');
Util::addScript($this->appName, 'radio-main');
Util::addStyle($this->appName, 'icons'); // Allow radio-browser hosts in the csp
$policy = new ContentSecurityPolicy();
$policy->addAllowedConnectDomain('https://de1.api.radio-browser.info');
$policy->addAllowedImageDomain('*');
$policy->addAllowedMediaDomain('*');
$response = new TemplateResponse(Application::APP_ID, 'main');
$response->setContentSecurityPolicy($policy);
$response = new TemplateResponse($this->appName, 'main');
return $response; return $response;
} }
} }

View File

@ -1,7 +1,9 @@
<?php <?php
declare(strict_types=1);
/** /**
* Radio App * Radio App.
* *
* @author Jonas Heinrich * @author Jonas Heinrich
* @copyright 2021 Jonas Heinrich <onny@project-insanity.org> * @copyright 2021 Jonas Heinrich <onny@project-insanity.org>
@ -18,82 +20,101 @@
* *
* You should have received a copy of the GNU Affero General Public * You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/ */
namespace OCA\Radio\Controller; namespace OCA\Radio\Controller;
use OCA\Radio\AppInfo\Application; use OCA\Radio\AppInfo\Application;
use OCA\Radio\Db\Station;
use OCA\Radio\Service\RecentService; use OCA\Radio\Service\RecentService;
use OCA\Radio\Service\UserService;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest; use OCP\IRequest;
class RecentController extends Controller { class RecentController extends Controller
/** @var RecentService */ {
private $service;
/** @var string */
private $userId;
use Errors; use Errors;
public function __construct(IRequest $request, public function __construct(
RecentService $service, IRequest $request,
$userId) { private readonly RecentService $service,
private readonly UserService $user,
) {
parent::__construct(Application::APP_ID, $request); parent::__construct(Application::APP_ID, $request);
$this->service = $service;
$this->userId = $userId;
} }
/** #[NoAdminRequired]
* @NoAdminRequired
*/
public function index(): DataResponse { public function index(): DataResponse {
return new DataResponse($this->service->findAll($this->userId)); return new DataResponse($this->service->findAll($this->user->getUserUID()));
} }
/** #[NoAdminRequired]
* @NoAdminRequired
*/
public function show(int $id): DataResponse { public function show(int $id): DataResponse {
return $this->handleNotFound(function () use ($id) { return $this->handleNotFound(fn (): ?Station => $this->service->find($id, $this->user->getUserUID()));
return $this->service->find($id, $this->userId);
});
} }
/** #[NoAdminRequired]
* @NoAdminRequired public function create(
*/ string $stationuuid,
public function create(string $stationuuid, string $name, string $favicon, string $urlresolved, string $name,
string $bitrate, string $country, string $language, string $homepage, string $favicon,
string $codec, string $tags): DataResponse { string $urlresolved,
return new DataResponse($this->service->create($stationuuid, $name, string $bitrate,
$favicon, $urlresolved, $bitrate, $country, $language, $homepage, $codec, string $country,
$tags, $this->userId)); string $language,
string $homepage,
string $codec,
string $tags
): DataResponse {
return new DataResponse($this->service->create(
$stationuuid,
$name,
$favicon,
$urlresolved,
$bitrate,
$country,
$language,
$homepage,
$codec,
$tags,
$this->user->getUserUID()
));
} }
/** #[NoAdminRequired]
* @NoAdminRequired public function update(
*/ int $id,
public function update(int $id, string $stationuuid, string $name, string $stationuuid,
string $favicon, string $urlresolved, string $bitrate, string $country, string $name,
string $language, string $homepage, string $codec, string $tags): DataResponse { string $favicon,
return $this->handleNotFound(function () use ($id, $stationuuid, $name, string $urlresolved,
$favicon, $urlresolved, $bitrate, $country, $language, $homepage, $codec, string $bitrate,
$tags) { string $country,
return $this->service->update($id, $stationuuid, $name, $favicon, string $language,
$urlresolved, $bitrate, $country, $language, $homepage, $codec, string $homepage,
$tags, $this->userId); string $codec,
}); string $tags
): DataResponse {
return $this->handleNotFound(fn (): ?Station => $this->service->update(
$id,
$stationuuid,
$name,
$favicon,
$urlresolved,
$bitrate,
$country,
$language,
$homepage,
$codec,
$tags,
$this->user->getUserUID()
));
} }
/** #[NoAdminRequired]
* @NoAdminRequired
*/
public function destroy(int $id): DataResponse { public function destroy(int $id): DataResponse {
return $this->handleNotFound(function () use ($id) { return $this->handleNotFound(fn (): ?Station => $this->service->delete($id, $this->user->getUserUID()));
return $this->service->delete($id, $this->userId);
});
} }
} }

View File

@ -1,7 +1,9 @@
<?php <?php
declare(strict_types=1);
/** /**
* Radio App * Radio App.
* *
* @author Jonas Heinrich * @author Jonas Heinrich
* @copyright 2021 Jonas Heinrich <onny@project-insanity.org> * @copyright 2021 Jonas Heinrich <onny@project-insanity.org>
@ -18,87 +20,103 @@
* *
* You should have received a copy of the GNU Affero General Public * You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/ */
namespace OCA\Radio\Controller; namespace OCA\Radio\Controller;
use OCA\Radio\AppInfo\Application; use OCA\Radio\AppInfo\Application;
use OCA\Radio\Db\Station;
use OCA\Radio\Service\RadioBrowserApiService;
use OCA\Radio\Service\RecentService; use OCA\Radio\Service\RecentService;
use OCA\Radio\Service\UserService;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest; use OCP\IRequest;
class StationController extends Controller { class StationController extends Controller
/** @var StationController */ {
private $service;
/** @var RadioBrowserApiService */
private $radiobrowserapi;
/** @var string */
private $userId;
use Errors; use Errors;
public function __construct(IRequest $request, public function __construct(
RecentService $service, IRequest $request,
RadioBrowserApiService $radiobrowserapi, private readonly RecentService $service,
$userId) { private readonly RadioBrowserApiService $radiobrowserapi,
private readonly UserService $user,
) {
parent::__construct(Application::APP_ID, $request); parent::__construct(Application::APP_ID, $request);
$this->service = $service;
$this->radiobrowserapi = $radiobrowserapi;
$this->userId = $userId;
} }
/** #[NoAdminRequired]
* @NoAdminRequired
*/
public function index(): DataResponse { public function index(): DataResponse {
return new DataResponse($this->service->findAll($this->userId)); return new DataResponse($this->service->findAll($this->user->getUserUID()));
} }
/** #[NoAdminRequired]
* @NoAdminRequired
*/
public function show(int $id): DataResponse { public function show(int $id): DataResponse {
return $this->handleNotFound(function () use ($id) { return $this->handleNotFound(fn (): ?Station => $this->service->find($id, $this->user->getUserUID()));
return $this->service->find($id, $this->userId);
});
} }
/** #[NoAdminRequired]
* @NoAdminRequired public function create(
*/ string $stationuuid,
public function create(string $stationuuid, string $name, string $favicon, string $urlresolved, string $name,
string $bitrate, string $country, string $language, string $homepage, string $favicon,
string $codec, string $tags): DataResponse { string $urlresolved,
return new DataResponse($this->service->create($stationuuid, $name, string $bitrate,
$favicon, $urlresolved, $bitrate, $country, $language, $homepage, $codec, string $country,
$tags, $this->userId)); string $language,
string $homepage,
string $codec,
string $tags
): DataResponse {
return new DataResponse($this->service->create(
$stationuuid,
$name,
$favicon,
$urlresolved,
$bitrate,
$country,
$language,
$homepage,
$codec,
$tags,
$this->user->getUserUID()
));
} }
/** #[NoAdminRequired]
* @NoAdminRequired public function update(
*/ int $id,
public function update(int $id, string $stationuuid, string $name, string $stationuuid,
string $favicon, string $urlresolved, string $bitrate, string $country, string $name,
string $language, string $homepage, string $codec, string $tags): DataResponse { string $favicon,
return $this->handleNotFound(function () use ($id, $stationuuid, $name, string $urlresolved,
$favicon, $urlresolved, $bitrate, $country, $language, $homepage, $codec, string $bitrate,
$tags) { string $country,
return $this->service->update($id, $stationuuid, $name, $favicon, string $language,
$urlresolved, $bitrate, $country, $language, $homepage, $codec, string $homepage,
$tags, $this->userId); string $codec,
}); string $tags
): DataResponse {
return $this->handleNotFound(fn (): ?Station => $this->service->update(
$id,
$stationuuid,
$name,
$favicon,
$urlresolved,
$bitrate,
$country,
$language,
$homepage,
$codec,
$tags,
$this->user->getUserUID()
));
} }
/** #[NoAdminRequired]
* @NoAdminRequired
*/
public function destroy(int $id): DataResponse { public function destroy(int $id): DataResponse {
return $this->handleNotFound(function () use ($id) { return $this->handleNotFound(fn (): ?Station => $this->service->delete($id, $this->user->getUserUID()));
return $this->service->delete($id, $this->userId);
});
} }
} }

View File

@ -1,7 +1,9 @@
<?php <?php
declare(strict_types=1);
/** /**
* Radio App * Radio App.
* *
* @author Jonas Heinrich * @author Jonas Heinrich
* @copyright 2021 Jonas Heinrich <onny@project-insanity.org> * @copyright 2021 Jonas Heinrich <onny@project-insanity.org>
@ -18,74 +20,45 @@
* *
* You should have received a copy of the GNU Affero General Public * You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/ */
namespace OCA\Radio\Dashboard; namespace OCA\Radio\Dashboard;
use OCA\Radio\AppInfo\Application;
use OCP\Dashboard\IWidget; use OCP\Dashboard\IWidget;
use OCP\IL10N; use OCP\IL10N;
use OCP\IURLGenerator; use OCP\IURLGenerator;
use OCP\Util; use OCP\Util;
use OCA\Radio\AppInfo\Application; class RadioWidget implements IWidget
{
class RadioWidget implements IWidget {
/** @var IL10N */
private $l10n;
/** @var IURLGenerator */
private $urlGenerator;
public function __construct( public function __construct(
IL10N $l10n, private readonly IL10N $l10n,
IURLGenerator $urlGenerator private readonly IURLGenerator $urlGenerator
) { ) {}
$this->l10n = $l10n;
$this->urlGenerator = $urlGenerator;
}
/**
* @inheritDoc
*/
public function getId(): string { public function getId(): string {
return Application::APP_ID; return Application::APP_ID;
} }
/**
* @inheritDoc
*/
public function getTitle(): string { public function getTitle(): string {
return $this->l10n->t('Radio stations'); return $this->l10n->t('Radio stations');
} }
/**
* @inheritDoc
*/
public function getOrder(): int { public function getOrder(): int {
return 10; return 10;
} }
/**
* @inheritDoc
*/
public function getIconClass(): string { public function getIconClass(): string {
return 'icon-radio'; return 'icon-radio';
} }
/**
* @inheritDoc
*/
public function getUrl(): ?string { public function getUrl(): ?string {
return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute('radio.page.index')); return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute('radio.page.index'));
} }
/**
* @inheritDoc
*/
public function load(): void { public function load(): void {
Util::addScript(Application::APP_ID, 'radio-dashboard'); Util::addScript(Application::APP_ID, 'radio-dashboard');
Util::addStyle(Application::APP_ID, 'dashboard'); Util::addStyle(Application::APP_ID, 'dashboard');
} }
} }

View File

@ -1,7 +1,9 @@
<?php <?php
declare(strict_types=1);
/** /**
* Radio App * Radio App.
* *
* @author Jonas Heinrich * @author Jonas Heinrich
* @copyright 2021 Jonas Heinrich <onny@project-insanity.org> * @copyright 2021 Jonas Heinrich <onny@project-insanity.org>
@ -18,49 +20,50 @@
* *
* You should have received a copy of the GNU Affero General Public * You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/ */
namespace OCA\Radio\Db; namespace OCA\Radio\Db;
use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\Entity; use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\AppFramework\Db\QBMapper; use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection; use OCP\IDBConnection;
class FavoriteMapper extends QBMapper { /**
* @extends QBMapper<Station>
*/
class FavoriteMapper extends QBMapper
{
public function __construct(IDBConnection $db) { public function __construct(IDBConnection $db) {
parent::__construct($db, 'favorites', Station::class); parent::__construct($db, 'favorites', Station::class);
} }
/** /**
* @param int $id * @throws MultipleObjectsReturnedException
* @param string $userId
* @return Entity|Station
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws DoesNotExistException * @throws DoesNotExistException
*/ */
public function find(int $id, string $userId): Station { public function find(int $id, string $userId): Station {
/* @var $qb IQueryBuilder */
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$qb->select('*') $qb->select('*')
->from('favorites') ->from('favorites')
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))) ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->eq('user_id', $qb->createNamedParameter($userId))); ->andWhere($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)))
;
return $this->findEntity($qb); return $this->findEntity($qb);
} }
/** /**
* @param string $userId * @return Station[]
* @return array
*/ */
public function findAll(string $userId): array { public function findAll(string $userId): array {
/* @var $qb IQueryBuilder */
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$qb->select('*') $qb->select('*')
->from('favorites') ->from('favorites')
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId))); ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)))
;
return $this->findEntities($qb); return $this->findEntities($qb);
} }
} }

View File

@ -1,7 +1,9 @@
<?php <?php
declare(strict_types=1);
/** /**
* Radio App * Radio App.
* *
* @author Jonas Heinrich * @author Jonas Heinrich
* @copyright 2021 Jonas Heinrich <onny@project-insanity.org> * @copyright 2021 Jonas Heinrich <onny@project-insanity.org>
@ -18,31 +20,30 @@
* *
* You should have received a copy of the GNU Affero General Public * You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/ */
namespace OCA\Radio\Db; namespace OCA\Radio\Db;
use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\Entity; use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\AppFramework\Db\QBMapper; use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection; use OCP\IDBConnection;
class RecentMapper extends QBMapper { /**
* @extends QBMapper<Station>
*/
class RecentMapper extends QBMapper
{
public function __construct(IDBConnection $db) { public function __construct(IDBConnection $db) {
parent::__construct($db, 'recent', Station::class); parent::__construct($db, 'recent', Station::class);
} }
/** /**
* @param int $id * @throws MultipleObjectsReturnedException
* @param string $userId
* @return Entity|Station
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws DoesNotExistException * @throws DoesNotExistException
*/ */
public function find(int $id, string $userId): Station { public function find(int $id, string $userId): Station {
/* @var $qb IQueryBuilder */
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$qb->selectDistinct('stationuuid') $qb->selectDistinct('stationuuid')
->addSelect('name') ->addSelect('name')
@ -57,16 +58,16 @@ class RecentMapper extends QBMapper {
->from('recent') ->from('recent')
->orderBy('id', 'DESC') ->orderBy('id', 'DESC')
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))) ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->eq('user_id', $qb->createNamedParameter($userId))); ->andWhere($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)))
;
return $this->findEntity($qb); return $this->findEntity($qb);
} }
/** /**
* @param string $userId * @return Station[]
* @return array
*/ */
public function findAll(string $userId): array { public function findAll(string $userId): array {
/* @var $qb IQueryBuilder */
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$qb->selectDistinct('stationuuid') $qb->selectDistinct('stationuuid')
->addSelect('name') ->addSelect('name')
@ -80,7 +81,9 @@ class RecentMapper extends QBMapper {
->addSelect('tags') ->addSelect('tags')
->from('recent') ->from('recent')
->orderBy('id', 'DESC') ->orderBy('id', 'DESC')
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId))); ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)))
;
return $this->findEntities($qb); return $this->findEntities($qb);
} }
} }

View File

@ -1,7 +1,9 @@
<?php <?php
declare(strict_types=1);
/** /**
* Radio App * Radio App.
* *
* @author Jonas Heinrich * @author Jonas Heinrich
* @copyright 2021 Jonas Heinrich <onny@project-insanity.org> * @copyright 2021 Jonas Heinrich <onny@project-insanity.org>
@ -18,27 +20,61 @@
* *
* You should have received a copy of the GNU Affero General Public * You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/ */
namespace OCA\Radio\Db; namespace OCA\Radio\Db;
use JsonSerializable;
use OCP\AppFramework\Db\Entity; use OCP\AppFramework\Db\Entity;
class Station extends Entity implements JsonSerializable { /**
protected $stationuuid; * @method string getStationuuid()
protected $name; * @method string getName()
protected $favicon; * @method ?string getFavicon()
protected $urlresolved; * @method ?string getUrlresolved()
protected $bitrate; * @method ?string getBitrate()
protected $country; * @method ?string getCountry()
protected $language; * @method ?string getLanguage()
protected $homepage; * @method ?string getHomepage()
protected $codec; * @method ?string getCodec()
protected $tags; * @method ?string getTags()
protected $userId; * @method string getUserId()
* @method void setStationuuid(string $stationuuid)
* @method void setName(string $name)
* @method void setFavicon(?string $favicon)
* @method void setUrlresolved(?string $urlresolved)
* @method void setBitrate(?string $bitrate)
* @method void setCountry(?string $country)
* @method void setLanguage(?string $language)
* @method void setHomepage(?string $homepage)
* @method void setCodec(?string $codec)
* @method void setTags(?string $tags)
* @method void setUserId(string $userId)
*
* @psalm-suppress PropertyNotSetInConstructor
*/
class Station extends Entity implements \JsonSerializable
{
protected string $stationuuid;
protected string $name;
protected ?string $favicon = null;
protected ?string $urlresolved = null;
protected ?string $bitrate = null;
protected ?string $country = null;
protected ?string $language = null;
protected ?string $homepage = null;
protected ?string $codec = null;
protected ?string $tags = null;
protected string $userId;
public function jsonSerialize(): array { public function jsonSerialize(): array {
return [ return [
@ -52,7 +88,8 @@ class Station extends Entity implements JsonSerializable {
'language' => $this->language, 'language' => $this->language,
'homepage' => $this->homepage, 'homepage' => $this->homepage,
'codec' => $this->codec, 'codec' => $this->codec,
'tags' => $this->tags 'tags' => $this->tags,
'userId' => $this->userId,
]; ];
} }
} }

View File

@ -1,56 +0,0 @@
<?php
/**
* Radio App
*
* @author Jonas Heinrich
* @copyright 2021 Jonas Heinrich <onny@project-insanity.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Radio;
use OC;
use OC\HintException;
use OCP\AppFramework\Http\Response;
class ExportResponse extends Response {
private $returnstring;
public function __construct($returnstring) {
parent::__construct();
$user = OC::$server->getUserSession()->getUser();
if (is_null($user)) {
throw new HintException('User not logged in');
}
$userName = $user->getDisplayName();
$productName = OC::$server->getThemingDefaults()->getName();
$dateTime = OC::$server->getDateTimeFormatter();
$export_name = '"' . $productName . ' Radio Favorites (' . $userName . ') (' . $dateTime->formatDate(time()) . ').xspf"';
$this->addHeader("Cache-Control", "private");
$this->addHeader("Content-Type", " application/xspf+xml");
$this->addHeader("Content-Length", strlen($returnstring));
$this->addHeader("Content-Disposition", "attachment; filename=" . $export_name);
$this->returnstring = $returnstring;
}
public function render() {
return $this->returnstring;
}
}

View File

@ -1,7 +1,7 @@
<?php <?php
/** /**
* Radio App * Radio App.
* *
* @author Jonas Heinrich * @author Jonas Heinrich
* @copyright 2021 Jonas Heinrich <onny@project-insanity.org> * @copyright 2021 Jonas Heinrich <onny@project-insanity.org>
@ -18,27 +18,24 @@
* *
* You should have received a copy of the GNU Affero General Public * You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/ */
declare(strict_types=1); declare(strict_types=1);
namespace OCA\Radio\Migration; namespace OCA\Radio\Migration;
use Closure;
use OCP\DB\ISchemaWrapper; use OCP\DB\ISchemaWrapper;
use OCP\Migration\SimpleMigrationStep;
use OCP\Migration\IOutput; use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version000000Date20181013124731 extends SimpleMigrationStep { class Version000000Date20181013124731 extends SimpleMigrationStep
{
/** /**
* @param IOutput $output * @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` *
* @param array $options
* @return null|ISchemaWrapper * @return null|ISchemaWrapper
*/ */
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) { public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */ /** @var ISchemaWrapper $schema */
$schema = $schemaClosure(); $schema = $schemaClosure();

View File

@ -1,7 +1,7 @@
<?php <?php
/** /**
* Radio App * Radio App.
* *
* @author Jonas Heinrich * @author Jonas Heinrich
* @copyright 2021 Jonas Heinrich <onny@project-insanity.org> * @copyright 2021 Jonas Heinrich <onny@project-insanity.org>
@ -18,7 +18,6 @@
* *
* You should have received a copy of the GNU Affero General Public * You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/ */
declare(strict_types=1); declare(strict_types=1);
@ -26,32 +25,22 @@ declare(strict_types=1);
namespace OCA\Radio\Search; namespace OCA\Radio\Search;
use OCA\Radio\AppInfo\Application; use OCA\Radio\AppInfo\Application;
use OCP\IUser; use OCP\Http\Client\IClientService;
use OCP\IURLGenerator; use OCP\IURLGenerator;
use OCP\IUser;
use OCP\Search\IProvider; use OCP\Search\IProvider;
use OCP\Search\ISearchQuery; use OCP\Search\ISearchQuery;
use OCP\Search\SearchResult; use OCP\Search\SearchResult;
use OCP\Search\SearchResultEntry; use OCP\Search\SearchResultEntry;
use Psr\Log\LoggerInterface;
use OCP\Http\Client\IClientService; class SearchProvider implements IProvider
{
use function urlencode;
class SearchProvider implements IProvider {
/** @var IClientService */
private $clientService;
/** @var IURLGenerator */
private $url;
public function __construct( public function __construct(
IClientService $clientService, private readonly IClientService $clientService,
IURLGenerator $url private readonly IURLGenerator $url,
) { private readonly LoggerInterface $logger,
$this->clientService = $clientService; ) {}
$this->url = $url;
}
public function getId(): string { public function getId(): string {
return Application::APP_ID; return Application::APP_ID;
@ -62,38 +51,43 @@ class SearchProvider implements IProvider {
} }
public function getOrder(string $route, array $routeParameters): int { public function getOrder(string $route, array $routeParameters): int {
if (strpos($route, 'files' . '.') === 0) { if (str_starts_with($route, 'files.')) {
return 25; return 25;
} elseif (strpos($route, Application::APP_ID . '.') === 0) { }
if (str_starts_with($route, Application::APP_ID.'.')) {
return -1; return -1;
} }
return 4; return 4;
} }
public function search(IUser $user, ISearchQuery $query): SearchResult { public function search(IUser $user, ISearchQuery $query): SearchResult {
$term = $query->getTerm(); $term = $query->getTerm();
$url = "https://de1.api.radio-browser.info/json/stations/byname/" . $term . "?limit=20"; $url = 'https://de1.api.radio-browser.info/json/stations/byname/'.$term.'?limit=20';
$client = $this->clientService->newClient(); $client = $this->clientService->newClient();
try { try {
$response = $client->get($url); $response = $client->get($url);
} catch (Exception $e) { } catch (\Exception $exception) {
$this->logger->error("Could not search for radio stations: " . $e->getMessage()); $this->logger->error('Could not search for radio stations: '.$exception->getMessage());
throw $e;
throw $exception;
} }
$body = $response->getBody();
$body = (string) $response->getBody();
/** @var array<array<string, string>> $parsed */
$parsed = json_decode($body, true); $parsed = json_decode($body, true);
$result = array_map(function (array $result) use ($term) { $result = array_map(fn (array $result): SearchResultEntry => new SearchResultEntry(
return new SearchResultEntry( $result['favicon'],
$result['favicon'], $result['name'],
$result['name'], str_replace(',', ', ', $result['tags']),
str_replace(",",", ",$result['tags']), $this->url->linkToRouteAbsolute('radio.page.index').'#/search/'.$term,
$this->url->linkToRouteAbsolute('radio.page.index') . '#/search/' . $term, 'icon-radio-trans'
'icon-radio-trans' ), $parsed);
);
}, $parsed);
return SearchResult::complete( return SearchResult::complete(
$this->getName(), $this->getName(),

View File

@ -1,7 +1,9 @@
<?php <?php
declare(strict_types=1);
/** /**
* Radio App * Radio App.
* *
* @author Jonas Heinrich * @author Jonas Heinrich
* @copyright 2021 Jonas Heinrich <onny@project-insanity.org> * @copyright 2021 Jonas Heinrich <onny@project-insanity.org>
@ -18,56 +20,55 @@
* *
* You should have received a copy of the GNU Affero General Public * You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/ */
namespace OCA\Radio\Service; namespace OCA\Radio\Service;
use Exception; use OCA\Radio\Db\FavoriteMapper;
use OCA\Radio\Db\Station;
use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCA\Radio\Db\Station; class FavoriteService
use OCA\Radio\Db\FavoriteMapper; {
public function __construct(
class FavoriteService { private readonly FavoriteMapper $mapper
) {
/** @var FavoriteMapper */
private $mapper;
public function __construct(FavoriteMapper $mapper) {
$this->mapper = $mapper; $this->mapper = $mapper;
} }
/**
* @return Station[]
*/
public function findAll(string $userId): array { public function findAll(string $userId): array {
return $this->mapper->findAll($userId); return $this->mapper->findAll($userId);
} }
private function handleException(Exception $e): void { public function find(int $id, string $userId): ?Station {
if ($e instanceof DoesNotExistException ||
$e instanceof MultipleObjectsReturnedException) {
throw new StationNotFound($e->getMessage());
} else {
throw $e;
}
}
public function find($id, $userId) {
try { try {
return $this->mapper->find($id, $userId); return $this->mapper->find($id, $userId);
// in order to be able to plug in different storage backends like files // in order to be able to plug in different storage backends like files
// for instance it is a good idea to turn storage related exceptions // for instance it is a good idea to turn storage related exceptions
// into service related exceptions so controllers and service users // into service related exceptions so controllers and service users
// have to deal with only one type of exception // have to deal with only one type of exception
} catch (Exception $e) { } catch (\Exception $exception) {
$this->handleException($e); return $this->handleException($exception);
} }
} }
public function create($stationuuid, $name, $favicon, $urlresolved, public function create(
$bitrate, $country, $language, $homepage, $codec, $tags, $userId) { string $stationuuid,
string $name,
?string $favicon,
?string $urlresolved,
?string $bitrate,
?string $country,
?string $language,
?string $homepage,
?string $codec,
?string $tags,
string $userId
): Station {
$station = new Station(); $station = new Station();
$station->setStationuuid($stationuuid); $station->setStationuuid($stationuuid);
$station->setName($name); $station->setName($name);
@ -80,11 +81,24 @@ class FavoriteService {
$station->setCodec($codec); $station->setCodec($codec);
$station->setTags($tags); $station->setTags($tags);
$station->setUserId($userId); $station->setUserId($userId);
return $this->mapper->insert($station); return $this->mapper->insert($station);
} }
public function update($id, $stationuuid, $name, $favicon, $urlresolved, public function update(
$bitrate, $country, $language, $homepage, $codec, $tags, $userId) { int $id,
string $stationuuid,
string $name,
?string $favicon,
?string $urlresolved,
?string $bitrate,
?string $country,
?string $language,
?string $homepage,
?string $codec,
?string $tags,
string $userId
): ?Station {
try { try {
$station = $this->mapper->find($id, $userId); $station = $this->mapper->find($id, $userId);
$station->setStationuuid($stationuuid); $station->setStationuuid($stationuuid);
@ -97,19 +111,30 @@ class FavoriteService {
$station->setHomepage($homepage); $station->setHomepage($homepage);
$station->setCodec($codec); $station->setCodec($codec);
$station->setTags($tags); $station->setTags($tags);
return $this->mapper->update($station); return $this->mapper->update($station);
} catch (Exception $e) { } catch (\Exception $exception) {
$this->handleException($e); return $this->handleException($exception);
} }
} }
public function delete($id, $userId) { public function delete(int $id, string $userId): ?Station {
try { try {
$station = $this->mapper->find($id, $userId); $station = $this->mapper->find($id, $userId);
$this->mapper->delete($station); $this->mapper->delete($station);
return $station; return $station;
} catch (Exception $e) { } catch (\Exception $exception) {
$this->handleException($e); return $this->handleException($exception);
} }
} }
private function handleException(\Throwable $e): void {
if ($e instanceof DoesNotExistException
|| $e instanceof MultipleObjectsReturnedException) {
throw new StationNotFound($e->getMessage());
}
throw $e;
}
} }

View File

@ -1,7 +1,7 @@
<?php <?php
/** /**
* Radio App * Radio App.
* *
* @author Jonas Heinrich * @author Jonas Heinrich
* @copyright 2021 Jonas Heinrich <onny@project-insanity.org> * @copyright 2021 Jonas Heinrich <onny@project-insanity.org>
@ -18,119 +18,116 @@
* *
* You should have received a copy of the GNU Affero General Public * You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/ */
declare(strict_types=1); declare(strict_types=1);
namespace OCA\Radio\Service; namespace OCA\Radio\Service;
use OCP\IURLGenerator;
use OCP\Http\Client\IClientService; use OCP\Http\Client\IClientService;
use Psr\Log\LoggerInterface;
use function urlencode; class RadioBrowserApiService
{
class RadioBrowserApiService { public string $baseUrl = 'https://api.fyyd.de/0.2';
/** @var IClientService */
private $clientService;
/** @var IURLGenerator */
private $url;
public $baseUrl = "https://api.fyyd.de/0.2";
public function __construct( public function __construct(
IClientService $clientService, private readonly IClientService $clientService,
IURLGenerator $url private readonly LoggerInterface $logger,
) { ) {}
$this->clientService = $clientService;
$this->url = $url;
}
public function queryEpisodes(int $podcast_id, int $count = 20, int $page = 0) { public function queryEpisodes(int $podcast_id, int $count = 20, int $page = 0): mixed {
$url = $this->baseUrl.'/podcast/episodes';
$url = $this->baseUrl . "/podcast/episodes"; $options = [];
$options['query'] = [ $options['query'] = [
'podcast_id' => $podcast_id, 'podcast_id' => $podcast_id,
'count' => $count, 'count' => $count,
'page' => $page 'page' => $page,
]; ];
$client = $this->clientService->newClient(); $client = $this->clientService->newClient();
try { try {
$response = $client->get($url, $options); $response = $client->get($url, $options);
} catch (Exception $e) { } catch (\Exception $exception) {
$this->logger->error("Could not search for podcasts: " . $e->getMessage()); $this->logger->error('Could not search for podcasts: '.$exception->getMessage());
throw $e;
throw $exception;
} }
$body = $response->getBody();
$parsed = json_decode($body, true); $body = (string) $response->getBody();
return $parsed; return json_decode($body, true);
} }
public function queryEpisode(int $episode_id) { public function queryEpisode(int $episode_id): mixed {
$url = $this->baseUrl.'/episode';
$url = $this->baseUrl . "/episode"; $options = [];
$options['query'] = [ $options['query'] = [
'episode_id' => $episode_id, 'episode_id' => $episode_id,
]; ];
$client = $this->clientService->newClient(); $client = $this->clientService->newClient();
try { try {
$response = $client->get($url, $options); $response = $client->get($url, $options);
} catch (Exception $e) { } catch (\Exception $exception) {
$this->logger->error("Could not search for podcasts: " . $e->getMessage()); $this->logger->error('Could not search for podcasts: '.$exception->getMessage());
throw $e;
}
$body = $response->getBody();
$parsed = json_decode($body, true);
return $parsed; throw $exception;
}
$body = (string) $response->getBody();
return json_decode($body, true);
} }
public function queryPodcast(int $podcast_id) { public function queryPodcast(int $podcast_id): mixed {
$url = $this->baseUrl.'/podcast';
$url = $this->baseUrl . "/podcast"; $options = [];
$options['query'] = [ $options['query'] = [
'podcast_id' => $podcast_id, 'podcast_id' => $podcast_id,
]; ];
$client = $this->clientService->newClient(); $client = $this->clientService->newClient();
try { try {
$response = $client->get($url, $options); $response = $client->get($url, $options);
} catch (Exception $e) { } catch (\Exception $exception) {
$this->logger->error("Could not search for podcasts: " . $e->getMessage()); $this->logger->error('Could not search for podcasts: '.$exception->getMessage());
throw $e;
}
$body = $response->getBody();
$parsed = json_decode($body, true);
return $parsed; throw $exception;
}
$body = (string) $response->getBody();
return json_decode($body, true);
} }
public function queryCategory(string $category, int $count = 20, public function queryCategory(
int $page = 0) { string $category,
int $count = 20,
int $page = 0
): mixed {
$options = [];
if ($category === 'hot') { if ('hot' === $category) {
$url = $this->baseUrl . "/feature/podcast/hot"; $url = $this->baseUrl.'/feature/podcast/hot';
$options['query'] = [ $options['query'] = [
'count' => $count, 'count' => $count,
'page' => $page, 'page' => $page,
]; ];
} else if ($category === 'latest') { } elseif ('latest' === $category) {
$url = $this->baseUrl . "/podcast/latest"; $url = $this->baseUrl.'/podcast/latest';
$options['query'] = [ $options['query'] = [
'count' => $count, 'count' => $count,
'page' => $page, 'page' => $page,
]; ];
} else { } else {
$url = $this->baseUrl . "/category"; $url = $this->baseUrl.'/category';
$options['query'] = [ $options['query'] = [
'count' => $count, 'count' => $count,
'page' => $page, 'page' => $page,
@ -139,15 +136,17 @@ class RadioBrowserApiService {
} }
$client = $this->clientService->newClient(); $client = $this->clientService->newClient();
try { try {
$response = $client->get($url, $options); $response = $client->get($url, $options);
} catch (Exception $e) { } catch (\Exception $exception) {
$this->logger->error("Could not search for podcasts: " . $e->getMessage()); $this->logger->error('Could not search for podcasts: '.$exception->getMessage());
throw $e;
}
$body = $response->getBody();
$parsed = json_decode($body, true);
return $parsed; throw $exception;
}
$body = (string) $response->getBody();
return json_decode($body, true);
} }
} }

View File

@ -1,7 +1,9 @@
<?php <?php
declare(strict_types=1);
/** /**
* Radio App * Radio App.
* *
* @author Jonas Heinrich * @author Jonas Heinrich
* @copyright 2021 Jonas Heinrich <onny@project-insanity.org> * @copyright 2021 Jonas Heinrich <onny@project-insanity.org>
@ -18,56 +20,55 @@
* *
* You should have received a copy of the GNU Affero General Public * You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/ */
namespace OCA\Radio\Service; namespace OCA\Radio\Service;
use Exception; use OCA\Radio\Db\RecentMapper;
use OCA\Radio\Db\Station;
use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCA\Radio\Db\Station; class RecentService
use OCA\Radio\Db\RecentMapper; {
public function __construct(
class RecentService { private readonly RecentMapper $mapper
) {
/** @var RecentMapper */
private $mapper;
public function __construct(RecentMapper $mapper) {
$this->mapper = $mapper; $this->mapper = $mapper;
} }
/**
* @return Station[]
*/
public function findAll(string $userId): array { public function findAll(string $userId): array {
return $this->mapper->findAll($userId); return $this->mapper->findAll($userId);
} }
private function handleException(Exception $e): void { public function find(int $id, string $userId): ?Station {
if ($e instanceof DoesNotExistException ||
$e instanceof MultipleObjectsReturnedException) {
throw new StationNotFound($e->getMessage());
} else {
throw $e;
}
}
public function find($id, $userId) {
try { try {
return $this->mapper->find($id, $userId); return $this->mapper->find($id, $userId);
// in order to be able to plug in different storage backends like files // in order to be able to plug in different storage backends like files
// for instance it is a good idea to turn storage related exceptions // for instance it is a good idea to turn storage related exceptions
// into service related exceptions so controllers and service users // into service related exceptions so controllers and service users
// have to deal with only one type of exception // have to deal with only one type of exception
} catch (Exception $e) { } catch (\Exception $exception) {
$this->handleException($e); return $this->handleException($exception);
} }
} }
public function create($stationuuid, $name, $favicon, $urlresolved, public function create(
$bitrate, $country, $language, $homepage, $codec, $tags, $userId) { string $stationuuid,
string $name,
?string $favicon,
?string $urlresolved,
?string $bitrate,
?string $country,
?string $language,
?string $homepage,
?string $codec,
?string $tags,
string $userId
): Station {
$station = new Station(); $station = new Station();
$station->setStationuuid($stationuuid); $station->setStationuuid($stationuuid);
$station->setName($name); $station->setName($name);
@ -80,11 +81,24 @@ class RecentService {
$station->setCodec($codec); $station->setCodec($codec);
$station->setTags($tags); $station->setTags($tags);
$station->setUserId($userId); $station->setUserId($userId);
return $this->mapper->insert($station); return $this->mapper->insert($station);
} }
public function update($id, $stationuuid, $name, $favicon, $urlresolved, public function update(
$bitrate, $country, $language, $homepage, $codec, $tags, $userId) { int $id,
string $stationuuid,
string $name,
?string $favicon,
?string $urlresolved,
?string $bitrate,
?string $country,
?string $language,
?string $homepage,
?string $codec,
?string $tags,
string $userId
): ?Station {
try { try {
$station = $this->mapper->find($id, $userId); $station = $this->mapper->find($id, $userId);
$station->setStationuuid($stationuuid); $station->setStationuuid($stationuuid);
@ -97,19 +111,33 @@ class RecentService {
$station->setHomepage($homepage); $station->setHomepage($homepage);
$station->setCodec($codec); $station->setCodec($codec);
$station->setTags($tags); $station->setTags($tags);
return $this->mapper->update($station); return $this->mapper->update($station);
} catch (Exception $e) { } catch (\Exception $exception) {
$this->handleException($e); return $this->handleException($exception);
} }
} }
public function delete($id, $userId) { public function delete(int $id, string $userId): ?Station {
try { try {
$station = $this->mapper->find($id, $userId); $station = $this->mapper->find($id, $userId);
$this->mapper->delete($station); $this->mapper->delete($station);
return $station; return $station;
} catch (Exception $e) { } catch (\Exception $exception) {
$this->handleException($e); return $this->handleException($exception);
} }
} }
/**
* @throws \Exception
*/
private function handleException(\Throwable $e): void {
if ($e instanceof DoesNotExistException
|| $e instanceof MultipleObjectsReturnedException) {
throw new StationNotFound($e->getMessage());
}
throw $e;
}
} }

View File

@ -1,7 +1,9 @@
<?php <?php
declare(strict_types=1);
/** /**
* Radio App * Radio App.
* *
* @author Jonas Heinrich * @author Jonas Heinrich
* @copyright 2021 Jonas Heinrich <onny@project-insanity.org> * @copyright 2021 Jonas Heinrich <onny@project-insanity.org>
@ -18,10 +20,8 @@
* *
* You should have received a copy of the GNU Affero General Public * You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/ */
namespace OCA\Radio\Service; namespace OCA\Radio\Service;
class StationNotFound extends \Exception { class StationNotFound extends \Exception {}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace OCA\Radio\Service;
use OCP\IUser;
use OCP\IUserSession;
class UserService
{
public function __construct(
private readonly IUserSession $userSession
) {}
public function getUserUID(): string {
$user = $this->getUser();
return $user instanceof IUser ? $user->getUID() : '';
}
public function getUser(): ?IUser {
return $this->userSession->getUser();
}
}

21
psalm.xml Normal file
View File

@ -0,0 +1,21 @@
<?xml version="1.0"?>
<psalm
errorLevel="1"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
findUnusedBaselineEntry="true"
findUnusedCode="false"
phpVersion="8.1"
>
<projectFiles>
<directory name="lib" />
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
<extraFiles>
<directory name="vendor"/>
</extraFiles>
</psalm>

34
rector.php Normal file
View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
use Nextcloud\Rector\Set\NextcloudSets;
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPaths([
__DIR__.'/appinfo',
__DIR__.'/lib',
])
->withPhpSets(php81: true)
->withSets([
NextcloudSets::NEXTCLOUD_27,
])
->withPreparedSets(
deadCode: true,
codeQuality: true,
codingStyle: true,
typeDeclarations: true,
privatization: true,
instanceOf: true,
earlyReturn: true,
strictBooleans: true,
rectorPreset: true,
phpunitCodeQuality: true,
doctrineCodeQuality: true,
symfonyCodeQuality: true,
symfonyConfigs: true,
twig: true,
phpunit: true,
)
;

View File

@ -1,7 +1,7 @@
<?php <?php
/** /**
* Radio App * Radio App.
* *
* @author Jonas Heinrich * @author Jonas Heinrich
* @copyright 2021 Jonas Heinrich <onny@project-insanity.org> * @copyright 2021 Jonas Heinrich <onny@project-insanity.org>
@ -18,7 +18,5 @@
* *
* You should have received a copy of the GNU Affero General Public * You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/ */
echo "<div id='vue-content'></div>"; echo "<div id='vue-content'></div>";