Compare commits

..

No commits in common. "rewrite" and "master" have entirely different histories.

31 changed files with 674 additions and 4492 deletions

View File

@ -1,23 +0,0 @@
.DS_Store
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
.marginalia
js/
build/
coverage/
utils/docker-ci
vendor/
.php-cs-fixer.cache

3
.gitignore vendored
View File

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

View File

@ -1,31 +0,0 @@
<?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,21 +0,0 @@
FROM nextcloud:30
ARG APP_NAME=radio
ENV NEXTCLOUD_UPDATE=1
ENV NEXTCLOUD_ADMIN_USER=$APP_NAME
ENV NEXTCLOUD_ADMIN_PASSWORD=$APP_NAME
ENV NEXTCLOUD_INIT_HTACCESS=1
ENV SQLITE_DATABASE=$APP_NAME
RUN apt-get update && \
apt-get install -y nodejs npm sqlite3 && \
rm -f /usr/local/etc/php/conf.d/opcache-recommended.ini && \
/entrypoint.sh true
USER www-data
COPY --chown=www-data:www-data . apps/$APP_NAME
RUN php occ app:enable $APP_NAME && \
php occ config:system:set debug --value=true
USER root

View File

@ -4,7 +4,7 @@
all: dev-setup lint build-js-production test all: dev-setup lint build-js-production test
# Dev env management # Dev env management
dev-setup: clean clean-dev composer-init npm-init dev-setup: clean clean-dev npm-init
npm-init: npm-init:
npm ci npm ci
@ -12,12 +12,6 @@ npm-init:
npm-update: npm-update:
npm update npm update
composer-init:
composer install
composer-update:
composer update
# Building # Building
build-js: build-js:
npm run dev npm run dev
@ -58,7 +52,6 @@ clean:
clean-dev: clean-dev:
rm -rf node_modules rm -rf node_modules
rm -rf vendor
release: release:
krankerl package krankerl package
@ -70,13 +63,6 @@ translations:
php utils/docker-ci/translations/translationtool/translationtool.phar create-pot-files php utils/docker-ci/translations/translationtool/translationtool.phar create-pot-files
php utils/docker-ci/translations/translationtool/translationtool.phar convert-po-files php utils/docker-ci/translations/translationtool/translationtool.phar convert-po-files
dev: dev-setup
docker stop radio || true
docker rm radio || true
docker build -t radio .
docker run -itd --rm --name radio -v $(PWD):/var/www/html/apps/radio -p 80:80 radio
npm run watch
publish-appstore-nightly: publish-appstore-nightly:
$(eval ASSET_URL = $(shell curl -s https://git.project-insanity.org/api/v4/projects/onny%2Fnextcloud-app-radio/releases | jq -r '.[0].assets.links[0].url')) $(eval ASSET_URL = $(shell curl -s https://git.project-insanity.org/api/v4/projects/onny%2Fnextcloud-app-radio/releases | jq -r '.[0].assets.links[0].url'))
wget $(ASSET_URL) -O build/artifacts/radio.tar.gz wget $(ASSET_URL) -O build/artifacts/radio.tar.gz

View File

@ -16,17 +16,17 @@ app uses radio-browser.info database as a backend.
- 👂 Smoth audio playback and transitions - 👂 Smoth audio playback and transitions
</description> </description>
<version>2.0.0</version> <version>1.1.0</version>
<licence>agpl</licence> <licence>agpl</licence>
<author mail="onny@project-insanity.org">Jonas Heinrich</author> <author mail="onny@project-insanity.org" >Jonas Heinrich</author>
<namespace>Radio</namespace> <namespace>Radio</namespace>
<category>multimedia</category> <category>multimedia</category>
<website>https://git.project-insanity.org/onny/nextcloud-app-radio</website> <website>https://git.project-insanity.org/onny/nextcloud-app-radio</website>
<bugs>https://git.project-insanity.org/onny/nextcloud-app-radio/issues</bugs> <bugs>https://git.project-insanity.org/onny/nextcloud-app-radio/issues</bugs>
<screenshot small-thumbnail="https://git.project-insanity.org/onny/nextcloud-app-radio/raw/master/screenshot-thumbnail.jpg">https://git.project-insanity.org/onny/nextcloud-app-radio/raw/master/screenshot.png</screenshot> <screenshot small-thumbnail="https://git.project-insanity.org/onny/nextcloud-app-radio/raw/master/screenshot-thumbnail.jpg">https://git.project-insanity.org/onny/nextcloud-app-radio/raw/master/screenshot.png</screenshot>
<dependencies> <dependencies>
<php min-version="8.1" /> <php min-version="7.4" max-version="8" />
<nextcloud min-version="29" max-version="30"/> <nextcloud min-version="20" max-version="21"/>
</dependencies> </dependencies>
<navigations> <navigations>
<navigation> <navigation>

View File

@ -1,9 +1,7 @@
<?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>
@ -20,6 +18,7 @@ declare(strict_types=1);
* *
* 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 [
@ -30,41 +29,42 @@ return [
'station' => ['url' => '/station'], 'station' => ['url' => '/station'],
], ],
'routes' => [ 'routes' => [
// Web page templates // Web page templates
[ [
'name' => 'page#index', 'name' => 'page#index',
'url' => '/', 'url' => '/',
'verb' => 'GET', 'verb' => 'GET'
], ],
[ [
'name' => 'page#index', 'name' => 'page#index',
'url' => '/top', 'url' => '/top',
'verb' => 'GET', 'verb' => 'GET',
'postfix' => 'top', 'postfix' => 'top'
], ],
[ [
'name' => 'page#index', 'name' => 'page#index',
'url' => '/recent', 'url' => '/recent',
'verb' => 'GET', 'verb' => 'GET',
'postfix' => 'recent', 'postfix' => 'recent'
], ],
[ [
'name' => 'page#index', 'name' => 'page#index',
'url' => '/new', 'url' => '/new',
'verb' => 'GET', 'verb' => 'GET',
'postfix' => 'new', 'postfix' => 'new'
], ],
[ [
'name' => 'page#index', 'name' => 'page#index',
'url' => '/favorites', 'url' => '/favorites',
'verb' => 'GET', 'verb' => 'GET',
'postfix' => 'favorites', 'postfix' => 'favorites'
], ],
[ [
'name' => 'page#index', 'name' => 'page#index',
'url' => '/categories', 'url' => '/categories',
'verb' => 'GET', 'verb' => 'GET',
'postfix' => 'categories', 'postfix' => 'categories'
], ],
[ [
'name' => 'page#index', 'name' => 'page#index',
@ -78,13 +78,14 @@ return [
'name' => 'favorite_api#preflighted_cors', 'name' => 'favorite_api#preflighted_cors',
'url' => '/api/0.1/{path}', 'url' => '/api/0.1/{path}',
'verb' => 'OPTIONS', 'verb' => 'OPTIONS',
'requirements' => ['path' => '.+'], 'requirements' => ['path' => '.+']
], ],
[ [
'name' => 'recent_api#preflighted_cors', 'name' => 'recent_api#preflighted_cors',
'url' => '/api/0.1/{path}', 'url' => '/api/0.1/{path}',
'verb' => 'OPTIONS', 'verb' => 'OPTIONS',
'requirements' => ['path' => '.+'], 'requirements' => ['path' => '.+']
], ]
],
]
]; ];

View File

@ -2,35 +2,11 @@
"name": "onny/radio", "name": "onny/radio",
"description": "Lint config for onny/radio", "description": "Lint config for onny/radio",
"license": "AGPL", "license": "AGPL",
"autoload": {
"psr-4": {
"OCA\\Radio\\": "lib/"
}
},
"scripts": {
"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": { "config": {
"optimize-autoloader": true, "optimize-autoloader": true,
"classmap-authoritative": true, "classmap-authoritative": true
"platform": { },
"php": "8.1" "scripts": {
"lint": "find . -name \\*.php -not -path './vendor/*' -exec php -l \"{}\" \\;"
} }
} }
}

3567
composer.lock generated

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,23 +18,25 @@
* *
* 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 OCA\Radio\Dashboard\RadioWidget; use OC\Security\CSP\ContentSecurityPolicy;
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;
class Application extends App implements IBootstrap use OCA\Radio\Dashboard\RadioWidget;
{
class Application extends App implements IBootstrap {
public const APP_ID = 'radio'; public const APP_ID = 'radio';
public function __construct() { public function __construct() {
@ -42,11 +44,32 @@ 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 fn (ContainerInterface $c): mixed => $c->get(IRequest::class)); $context->registerService('request', static function ($c) {
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,9 +1,7 @@
<?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>
@ -20,23 +18,24 @@ declare(strict_types=1);
* *
* 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\Service\StationNotFound; use Closure;
use OCP\AppFramework\Http; use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\DataResponse;
trait Errors use OCA\Radio\Service\StationNotFound;
{
protected function handleNotFound(\Closure $callback): DataResponse {
try {
/** @psalm-suppress MixedArgument */
return new DataResponse($callback());
} catch (StationNotFound $stationNotFound) {
$message = ['message' => $stationNotFound->getMessage()];
trait Errors {
protected function handleNotFound(Closure $callback): DataResponse {
try {
return new DataResponse($callback());
} catch (StationNotFound $e) {
$message = ['message' => $e->getMessage()];
return new DataResponse($message, Http::STATUS_NOT_FOUND); return new DataResponse($message, Http::STATUS_NOT_FOUND);
} }
} }

View File

@ -1,9 +1,7 @@
<?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>
@ -20,70 +18,60 @@ declare(strict_types=1);
* *
* 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\Attribute\NoAdminRequired; use OCP\AppFramework\Http\DataResponse;
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( public function __construct(IRequest $request,
IRequest $request, FavoriteService $service,
private readonly FavoriteService $service, $userId) {
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] /**
public function index(): DataDownloadResponse { * @NoAdminRequired
$user = $this->user->getUser(); */
if (is_null($user)) { public function index() {
throw new HintException('User not logged in');
}
$xml = new \SimpleXMLElement('<?xml version="1.0"?><playlist></playlist>'); $xml = new SimpleXMLElement('<?xml version="1.0"?><playlist></playlist>');
$xml->addAttribute('encoding', 'UTF-8'); $xml->addAttribute('encoding', 'UTF-8');
$trackList = $xml->addChild('trackList'); $trackList = $xml->addChild('trackList');
if ($trackList instanceof \SimpleXMLElement) { foreach($this->service->findAll($this->userId) as $station) {
foreach ($this->service->findAll($user->getUID()) as $station) {
$track = $trackList->addChild('track'); $track = $trackList->addChild('track');
if ($track instanceof \SimpleXMLElement) {
$track->addChild('location', $station->getUrlresolved()); $track->addChild('location', $station->getUrlresolved());
$track->addChild('title', $station->getName()); $track->addChild('title', $station->getName());
$track->addChild('image', $station->getFavicon()); $track->addChild('image', $station->getFavicon());
} }
}
}
$dom = new \DOMDocument('1.0'); $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();
$userName = $user->getDisplayName(); return new ExportResponse($dom->saveXML());
$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,9 +1,7 @@
<?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>
@ -20,101 +18,83 @@ declare(strict_types=1);
* *
* 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( public function __construct(IRequest $request,
IRequest $request, FavoriteService $service,
private readonly FavoriteService $service, $userId) {
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->user->getUserUID())); return new DataResponse($this->service->findAll($this->userId));
} }
#[NoAdminRequired] /**
* @NoAdminRequired
*/
public function show(int $id): DataResponse { public function show(int $id): DataResponse {
return $this->handleNotFound(fn (): ?Station => $this->service->find($id, $this->user->getUserUID())); return $this->handleNotFound(function () use ($id) {
return $this->service->find($id, $this->userId);
});
} }
#[NoAdminRequired] /**
public function create( * @NoAdminRequired
string $stationuuid, */
string $name, public function create(string $stationuuid, string $name, string $favicon, string $urlresolved,
string $favicon, string $bitrate, string $country, string $language, string $homepage,
string $urlresolved, string $codec, string $tags): DataResponse {
string $bitrate, return new DataResponse($this->service->create($stationuuid, $name,
string $country, $favicon, $urlresolved, $bitrate, $country, $language, $homepage, $codec,
string $language, $tags, $this->userId));
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] /**
public function update( * @NoAdminRequired
int $id, */
string $stationuuid, public function update(int $id, string $stationuuid,
string $name, string $name, string $favicon, string $urlresolved,
string $favicon, string $bitrate, string $country, string $language, string $homepage,
string $urlresolved, string $codec, string $tags): DataResponse {
string $bitrate, return $this->handleNotFound(function () use ($id, $stationuuid, $name,
string $country, $favicon, $urlresolved, $bitrate, $country, $language, $homepage, $codec,
string $language, $tags) {
string $homepage, return $this->service->update($id, $stationuuid, $name, $favicon,
string $codec, $urlresolved, $bitrate, $country, $language, $homepage, $codec,
string $tags $tags, $this->userId);
): 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(fn (): ?Station => $this->service->delete($id, $this->user->getUserUID())); return $this->handleNotFound(function () use ($id) {
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,42 +18,36 @@
* *
* 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 OCA\Radio\AppInfo\Application;
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\IRequest;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Controller;
use OCP\Util; use OCP\Util;
class PageController extends Controller class PageController extends Controller {
{
public function __construct(IRequest $request) { protected $appName;
parent::__construct(Application::APP_ID, $request);
public function __construct($appName, IRequest $request) {
parent::__construct($appName, $request);
$this->appName = $appName;
} }
#[NoAdminRequired] /**
#[NoCSRFRequired] * @NoAdminRequired
public function index(): TemplateResponse { * @NoCSRFRequired
Util::addScript(Application::APP_ID, 'radio-main'); */
Util::addStyle(Application::APP_ID, 'icons'); public function index() {
Util::addScript($this->appName, 'radio-main');
// Allow radio-browser hosts in the csp Util::addStyle($this->appName, 'icons');
$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,9 +1,7 @@
<?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>
@ -20,101 +18,82 @@ declare(strict_types=1);
* *
* 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( public function __construct(IRequest $request,
IRequest $request, RecentService $service,
private readonly RecentService $service, $userId) {
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->user->getUserUID())); return new DataResponse($this->service->findAll($this->userId));
} }
#[NoAdminRequired] /**
* @NoAdminRequired
*/
public function show(int $id): DataResponse { public function show(int $id): DataResponse {
return $this->handleNotFound(fn (): ?Station => $this->service->find($id, $this->user->getUserUID())); return $this->handleNotFound(function () use ($id) {
return $this->service->find($id, $this->userId);
});
} }
#[NoAdminRequired] /**
public function create( * @NoAdminRequired
string $stationuuid, */
string $name, public function create(string $stationuuid, string $name, string $favicon, string $urlresolved,
string $favicon, string $bitrate, string $country, string $language, string $homepage,
string $urlresolved, string $codec, string $tags): DataResponse {
string $bitrate, return new DataResponse($this->service->create($stationuuid, $name,
string $country, $favicon, $urlresolved, $bitrate, $country, $language, $homepage, $codec,
string $language, $tags, $this->userId));
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] /**
public function update( * @NoAdminRequired
int $id, */
string $stationuuid, public function update(int $id, string $stationuuid, string $name,
string $name, string $favicon, string $urlresolved, string $bitrate, string $country,
string $favicon, string $language, string $homepage, 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(fn (): ?Station => $this->service->delete($id, $this->user->getUserUID())); return $this->handleNotFound(function () use ($id) {
return $this->service->delete($id, $this->userId);
});
} }
} }

View File

@ -1,9 +1,7 @@
<?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>
@ -20,103 +18,87 @@ declare(strict_types=1);
* *
* 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( public function __construct(IRequest $request,
IRequest $request, RecentService $service,
private readonly RecentService $service, RadioBrowserApiService $radiobrowserapi,
private readonly RadioBrowserApiService $radiobrowserapi, $userId) {
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->user->getUserUID())); return new DataResponse($this->service->findAll($this->userId));
} }
#[NoAdminRequired] /**
* @NoAdminRequired
*/
public function show(int $id): DataResponse { public function show(int $id): DataResponse {
return $this->handleNotFound(fn (): ?Station => $this->service->find($id, $this->user->getUserUID())); return $this->handleNotFound(function () use ($id) {
return $this->service->find($id, $this->userId);
});
} }
#[NoAdminRequired] /**
public function create( * @NoAdminRequired
string $stationuuid, */
string $name, public function create(string $stationuuid, string $name, string $favicon, string $urlresolved,
string $favicon, string $bitrate, string $country, string $language, string $homepage,
string $urlresolved, string $codec, string $tags): DataResponse {
string $bitrate, return new DataResponse($this->service->create($stationuuid, $name,
string $country, $favicon, $urlresolved, $bitrate, $country, $language, $homepage, $codec,
string $language, $tags, $this->userId));
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] /**
public function update( * @NoAdminRequired
int $id, */
string $stationuuid, public function update(int $id, string $stationuuid, string $name,
string $name, string $favicon, string $urlresolved, string $bitrate, string $country,
string $favicon, string $language, string $homepage, 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(fn (): ?Station => $this->service->delete($id, $this->user->getUserUID())); return $this->handleNotFound(function () use ($id) {
return $this->service->delete($id, $this->userId);
});
} }
} }

View File

@ -1,9 +1,7 @@
<?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>
@ -20,43 +18,72 @@ declare(strict_types=1);
* *
* 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;
class RadioWidget implements IWidget use OCA\Radio\AppInfo\Application;
{
public function __construct(
private readonly IL10N $l10n,
private readonly IURLGenerator $urlGenerator
) {}
class RadioWidget implements IWidget {
/** @var IL10N */
private $l10n;
/** @var IURLGenerator */
private $urlGenerator;
public function __construct(
IL10N $l10n,
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,9 +1,7 @@
<?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>
@ -20,50 +18,49 @@ declare(strict_types=1);
* *
* 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\MultipleObjectsReturnedException; use OCP\AppFramework\Db\Entity;
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);
} }
/** /**
* @throws MultipleObjectsReturnedException * @param int $id
* @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);
} }
/** /**
* @return Station[] * @param string $userId
* @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,9 +1,7 @@
<?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>
@ -20,30 +18,31 @@ declare(strict_types=1);
* *
* 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\MultipleObjectsReturnedException; use OCP\AppFramework\Db\Entity;
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);
} }
/** /**
* @throws MultipleObjectsReturnedException * @param int $id
* @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')
@ -58,16 +57,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);
} }
/** /**
* @return Station[] * @param string $userId
* @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')
@ -81,9 +80,7 @@ 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,9 +1,7 @@
<?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>
@ -20,61 +18,27 @@ declare(strict_types=1);
* *
* 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 {
* @method string getStationuuid() protected $stationuuid;
* @method string getName() protected $name;
* @method ?string getFavicon() protected $favicon;
* @method ?string getUrlresolved() protected $urlresolved;
* @method ?string getBitrate() protected $bitrate;
* @method ?string getCountry() protected $country;
* @method ?string getLanguage() protected $language;
* @method ?string getHomepage() protected $homepage;
* @method ?string getCodec() protected $codec;
* @method ?string getTags() protected $tags;
* @method string getUserId() protected $userId;
* @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 [
@ -88,8 +52,7 @@ 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,
]; ];
} }
} }

56
lib/ExportResponse.php Normal file
View File

@ -0,0 +1,56 @@
<?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,24 +18,27 @@
* *
* 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\IOutput;
use OCP\Migration\SimpleMigrationStep; use OCP\Migration\SimpleMigrationStep;
use OCP\Migration\IOutput;
class Version000000Date20181013124731 extends SimpleMigrationStep {
class Version000000Date20181013124731 extends SimpleMigrationStep
{
/** /**
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` * @param IOutput $output
* * @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,6 +18,7 @@
* *
* 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);
@ -25,22 +26,32 @@ declare(strict_types=1);
namespace OCA\Radio\Search; namespace OCA\Radio\Search;
use OCA\Radio\AppInfo\Application; use OCA\Radio\AppInfo\Application;
use OCP\Http\Client\IClientService;
use OCP\IURLGenerator;
use OCP\IUser; use OCP\IUser;
use OCP\IURLGenerator;
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;
class SearchProvider implements IProvider use OCP\Http\Client\IClientService;
{
use function urlencode;
class SearchProvider implements IProvider {
/** @var IClientService */
private $clientService;
/** @var IURLGenerator */
private $url;
public function __construct( public function __construct(
private readonly IClientService $clientService, IClientService $clientService,
private readonly IURLGenerator $url, 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;
@ -51,43 +62,38 @@ class SearchProvider implements IProvider
} }
public function getOrder(string $route, array $routeParameters): int { public function getOrder(string $route, array $routeParameters): int {
if (str_starts_with($route, 'files.')) { if (strpos($route, 'files' . '.') === 0) {
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 $exception) { } catch (Exception $e) {
$this->logger->error('Could not search for radio stations: '.$exception->getMessage()); $this->logger->error("Could not search for radio stations: " . $e->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(fn (array $result): SearchResultEntry => new SearchResultEntry( $result = array_map(function (array $result) use ($term) {
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,9 +1,7 @@
<?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>
@ -20,55 +18,56 @@ declare(strict_types=1);
* *
* 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 OCA\Radio\Db\FavoriteMapper; use Exception;
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;
class FavoriteService use OCA\Radio\Db\Station;
{ use OCA\Radio\Db\FavoriteMapper;
public function __construct(
private readonly FavoriteMapper $mapper class FavoriteService {
) {
/** @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);
} }
public function find(int $id, string $userId): ?Station { private function handleException(Exception $e): void {
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 $exception) { } catch (Exception $e) {
return $this->handleException($exception); $this->handleException($e);
} }
} }
public function create( public function create($stationuuid, $name, $favicon, $urlresolved,
string $stationuuid, $bitrate, $country, $language, $homepage, $codec, $tags, $userId) {
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);
@ -81,24 +80,11 @@ 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( public function update($id, $stationuuid, $name, $favicon, $urlresolved,
int $id, $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 {
try { try {
$station = $this->mapper->find($id, $userId); $station = $this->mapper->find($id, $userId);
$station->setStationuuid($stationuuid); $station->setStationuuid($stationuuid);
@ -111,30 +97,19 @@ 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 $exception) { } catch (Exception $e) {
return $this->handleException($exception); $this->handleException($e);
} }
} }
public function delete(int $id, string $userId): ?Station { public function delete($id, $userId) {
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 $exception) { } catch (Exception $e) {
return $this->handleException($exception); $this->handleException($e);
} }
} }
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,116 +18,119 @@
* *
* 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;
class RadioBrowserApiService use function urlencode;
{
public string $baseUrl = 'https://api.fyyd.de/0.2'; class RadioBrowserApiService {
/** @var IClientService */
private $clientService;
/** @var IURLGenerator */
private $url;
public $baseUrl = "https://api.fyyd.de/0.2";
public function __construct( public function __construct(
private readonly IClientService $clientService, IClientService $clientService,
private readonly LoggerInterface $logger, IURLGenerator $url
) {} ) {
$this->clientService = $clientService;
$this->url = $url;
}
public function queryEpisodes(int $podcast_id, int $count = 20, int $page = 0): mixed { public function queryEpisodes(int $podcast_id, int $count = 20, int $page = 0) {
$url = $this->baseUrl.'/podcast/episodes';
$options = []; $url = $this->baseUrl . "/podcast/episodes";
$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 $exception) { } catch (Exception $e) {
$this->logger->error('Could not search for podcasts: '.$exception->getMessage()); $this->logger->error("Could not search for podcasts: " . $e->getMessage());
throw $e;
}
$body = $response->getBody();
throw $exception; $parsed = json_decode($body, true);
return $parsed;
} }
$body = (string) $response->getBody(); public function queryEpisode(int $episode_id) {
return json_decode($body, true); $url = $this->baseUrl . "/episode";
}
public function queryEpisode(int $episode_id): mixed {
$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 $exception) { } catch (Exception $e) {
$this->logger->error('Could not search for podcasts: '.$exception->getMessage()); $this->logger->error("Could not search for podcasts: " . $e->getMessage());
throw $e;
}
$body = $response->getBody();
$parsed = json_decode($body, true);
throw $exception; return $parsed;
} }
$body = (string) $response->getBody(); public function queryPodcast(int $podcast_id) {
return json_decode($body, true); $url = $this->baseUrl . "/podcast";
}
public function queryPodcast(int $podcast_id): mixed {
$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 $exception) { } catch (Exception $e) {
$this->logger->error('Could not search for podcasts: '.$exception->getMessage()); $this->logger->error("Could not search for podcasts: " . $e->getMessage());
throw $e;
}
$body = $response->getBody();
$parsed = json_decode($body, true);
throw $exception; return $parsed;
} }
$body = (string) $response->getBody(); public function queryCategory(string $category, int $count = 20,
int $page = 0) {
return json_decode($body, true); if ($category === 'hot') {
} $url = $this->baseUrl . "/feature/podcast/hot";
public function queryCategory(
string $category,
int $count = 20,
int $page = 0
): mixed {
$options = [];
if ('hot' === $category) {
$url = $this->baseUrl.'/feature/podcast/hot';
$options['query'] = [ $options['query'] = [
'count' => $count, 'count' => $count,
'page' => $page, 'page' => $page,
]; ];
} elseif ('latest' === $category) { } else if ($category === 'latest') {
$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,
@ -136,17 +139,15 @@ class RadioBrowserApiService
} }
$client = $this->clientService->newClient(); $client = $this->clientService->newClient();
try { try {
$response = $client->get($url, $options); $response = $client->get($url, $options);
} catch (\Exception $exception) { } catch (Exception $e) {
$this->logger->error('Could not search for podcasts: '.$exception->getMessage()); $this->logger->error("Could not search for podcasts: " . $e->getMessage());
throw $e;
throw $exception;
} }
$body = $response->getBody();
$parsed = json_decode($body, true);
$body = (string) $response->getBody(); return $parsed;
return json_decode($body, true);
} }
} }

View File

@ -1,9 +1,7 @@
<?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>
@ -20,55 +18,56 @@ declare(strict_types=1);
* *
* 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 OCA\Radio\Db\RecentMapper; use Exception;
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;
class RecentService use OCA\Radio\Db\Station;
{ use OCA\Radio\Db\RecentMapper;
public function __construct(
private readonly RecentMapper $mapper class RecentService {
) {
/** @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);
} }
public function find(int $id, string $userId): ?Station { private function handleException(Exception $e): void {
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 $exception) { } catch (Exception $e) {
return $this->handleException($exception); $this->handleException($e);
} }
} }
public function create( public function create($stationuuid, $name, $favicon, $urlresolved,
string $stationuuid, $bitrate, $country, $language, $homepage, $codec, $tags, $userId) {
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);
@ -81,24 +80,11 @@ 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( public function update($id, $stationuuid, $name, $favicon, $urlresolved,
int $id, $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 {
try { try {
$station = $this->mapper->find($id, $userId); $station = $this->mapper->find($id, $userId);
$station->setStationuuid($stationuuid); $station->setStationuuid($stationuuid);
@ -111,33 +97,19 @@ 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 $exception) { } catch (Exception $e) {
return $this->handleException($exception); $this->handleException($e);
} }
} }
public function delete(int $id, string $userId): ?Station { public function delete($id, $userId) {
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 $exception) { } catch (Exception $e) {
return $this->handleException($exception); $this->handleException($e);
} }
} }
/**
* @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,9 +1,7 @@
<?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>
@ -20,8 +18,10 @@ declare(strict_types=1);
* *
* 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

@ -1,25 +0,0 @@
<?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();
}
}

View File

@ -1,21 +0,0 @@
<?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>

View File

@ -1,34 +0,0 @@
<?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,5 +18,7 @@
* *
* 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>";