Files
AudioCore/plugins/artists/ArtistsController.php

545 lines
21 KiB
PHP

<?php
declare(strict_types=1);
namespace Plugins\Artists;
use Core\Http\Response;
use Core\Services\Auth;
use Core\Views\View;
use Core\Services\Database;
use PDO;
use Throwable;
class ArtistsController
{
private View $view;
public function __construct()
{
$this->view = new View(__DIR__ . '/views');
}
public function index(): Response
{
$db = Database::get();
$page = null;
$artists = [];
if ($db instanceof PDO) {
try {
$stmt = $db->prepare("SELECT title, content_html FROM ac_pages WHERE slug = 'artists' AND is_published = 1 LIMIT 1");
$stmt->execute();
$page = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
$listStmt = $db->query("
SELECT id, name, slug, country, avatar_url
FROM ac_artists
WHERE is_active = 1
ORDER BY created_at DESC
");
$artists = $listStmt ? $listStmt->fetchAll(PDO::FETCH_ASSOC) : [];
} catch (Throwable $e) {
}
}
return new Response($this->view->render('site/index.php', [
'title' => (string)($page['title'] ?? 'Artists'),
'content_html' => (string)($page['content_html'] ?? ''),
'artists' => $artists,
]));
}
public function show(): Response
{
$slug = trim((string)($_GET['slug'] ?? ''));
$artist = null;
$artistReleases = [];
if ($slug !== '') {
$db = Database::get();
if ($db instanceof PDO) {
$stmt = $db->prepare("SELECT * FROM ac_artists WHERE slug = :slug AND is_active = 1 LIMIT 1");
$stmt->execute([':slug' => $slug]);
$artist = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
if ($artist) {
try {
$artistIdReady = false;
$probe = $db->query("SHOW COLUMNS FROM ac_releases LIKE 'artist_id'");
if ($probe && $probe->fetch(PDO::FETCH_ASSOC)) {
$artistIdReady = true;
}
if ($artistIdReady) {
$relStmt = $db->prepare("
SELECT id, title, slug, release_date, cover_url, artist_name
FROM ac_releases
WHERE is_published = 1
AND (release_date IS NULL OR release_date <= :today)
AND (artist_id = :artist_id OR artist_name = :artist_name)
ORDER BY release_date DESC, created_at DESC
LIMIT 2
");
$relStmt->execute([
':today' => date('Y-m-d'),
':artist_id' => (int)($artist['id'] ?? 0),
':artist_name' => (string)($artist['name'] ?? ''),
]);
} else {
$relStmt = $db->prepare("
SELECT id, title, slug, release_date, cover_url, artist_name
FROM ac_releases
WHERE is_published = 1
AND (release_date IS NULL OR release_date <= :today)
AND artist_name = :artist_name
ORDER BY release_date DESC, created_at DESC
LIMIT 2
");
$relStmt->execute([
':today' => date('Y-m-d'),
':artist_name' => (string)($artist['name'] ?? ''),
]);
}
$artistReleases = $relStmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
} catch (Throwable $e) {
$artistReleases = [];
}
}
}
}
return new Response($this->view->render('site/show.php', [
'title' => $artist ? (string)$artist['name'] : 'Artist Profile',
'artist' => $artist,
'artist_releases' => $artistReleases,
]));
}
public function adminIndex(): Response
{
if (!Auth::check()) {
return new Response('', 302, ['Location' => '/admin/login']);
}
if (!Auth::hasRole(['admin', 'manager'])) {
return new Response('', 302, ['Location' => '/admin']);
}
$tableReady = $this->artistsTableReady();
$socialReady = $this->socialColumnReady();
$artists = [];
$pageId = 0;
$pagePublished = 0;
if ($tableReady) {
$db = Database::get();
if ($db instanceof PDO) {
$stmt = $db->query("SELECT id, name, slug, country, avatar_url, is_active FROM ac_artists ORDER BY created_at DESC");
$artists = $stmt ? $stmt->fetchAll(PDO::FETCH_ASSOC) : [];
$pageStmt = $db->prepare("SELECT id, is_published FROM ac_pages WHERE slug = 'artists' LIMIT 1");
$pageStmt->execute();
$pageRow = $pageStmt->fetch(PDO::FETCH_ASSOC);
if ($pageRow) {
$pageId = (int)($pageRow['id'] ?? 0);
$pagePublished = (int)($pageRow['is_published'] ?? 0);
}
}
}
return new Response($this->view->render('admin/index.php', [
'title' => 'Artists',
'table_ready' => $tableReady,
'social_ready' => $socialReady,
'artists' => $artists,
'page_id' => $pageId,
'page_published' => $pagePublished,
]));
}
public function adminNew(): Response
{
return $this->adminEdit(0);
}
public function adminEdit(int $id = 0): Response
{
if (!Auth::check()) {
return new Response('', 302, ['Location' => '/admin/login']);
}
if (!Auth::hasRole(['admin', 'manager'])) {
return new Response('', 302, ['Location' => '/admin']);
}
$artist = [
'id' => 0,
'name' => '',
'slug' => '',
'bio' => '',
'credits' => '',
'country' => '',
'avatar_url' => '',
'social_links' => '',
'is_active' => 1,
];
if ($id > 0) {
$db = Database::get();
if ($db instanceof PDO) {
$stmt = $db->prepare("SELECT * FROM ac_artists WHERE id = :id LIMIT 1");
$stmt->execute([':id' => $id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
$artist = array_merge($artist, $row);
}
}
}
if (!empty($_GET['avatar_url']) && $artist['avatar_url'] === '') {
$artist['avatar_url'] = (string)$_GET['avatar_url'];
}
return new Response($this->view->render('admin/edit.php', [
'title' => $id > 0 ? 'Edit Artist' : 'New Artist',
'artist' => $artist,
'error' => '',
]));
}
public function adminSave(): Response
{
if (!Auth::check()) {
return new Response('', 302, ['Location' => '/admin/login']);
}
if (!Auth::hasRole(['admin', 'manager'])) {
return new Response('', 302, ['Location' => '/admin']);
}
$id = (int)($_POST['id'] ?? 0);
$name = trim((string)($_POST['name'] ?? ''));
$slug = trim((string)($_POST['slug'] ?? ''));
$bio = trim((string)($_POST['bio'] ?? ''));
$credits = trim((string)($_POST['credits'] ?? ''));
$country = trim((string)($_POST['country'] ?? ''));
$avatarUrl = trim((string)($_POST['avatar_url'] ?? ''));
$socialLinks = [
'website' => $this->normalizeUrl(trim((string)($_POST['social_website'] ?? ''))),
'instagram' => $this->normalizeUrl(trim((string)($_POST['social_instagram'] ?? ''))),
'soundcloud' => $this->normalizeUrl(trim((string)($_POST['social_soundcloud'] ?? ''))),
'spotify' => $this->normalizeUrl(trim((string)($_POST['social_spotify'] ?? ''))),
'youtube' => $this->normalizeUrl(trim((string)($_POST['social_youtube'] ?? ''))),
'tiktok' => $this->normalizeUrl(trim((string)($_POST['social_tiktok'] ?? ''))),
'bandcamp' => $this->normalizeUrl(trim((string)($_POST['social_bandcamp'] ?? ''))),
'beatport' => $this->normalizeUrl(trim((string)($_POST['social_beatport'] ?? ''))),
'facebook' => $this->normalizeUrl(trim((string)($_POST['social_facebook'] ?? ''))),
'x' => $this->normalizeUrl(trim((string)($_POST['social_x'] ?? ''))),
];
$socialLinks = array_filter($socialLinks, static fn($value) => $value !== '');
$socialJson = $socialLinks ? json_encode($socialLinks, JSON_UNESCAPED_SLASHES) : null;
$socialReady = $this->socialColumnReady();
$creditsReady = $this->creditsColumnReady();
$isActive = isset($_POST['is_active']) ? 1 : 0;
if ($name === '') {
return $this->saveError($id, 'Name is required.');
}
$slug = $slug !== '' ? $this->slugify($slug) : $this->slugify($name);
$db = Database::get();
if (!$db instanceof PDO) {
return $this->saveError($id, 'Database unavailable.');
}
$dupStmt = $id > 0
? $db->prepare("SELECT id FROM ac_artists WHERE slug = :slug AND id != :id LIMIT 1")
: $db->prepare("SELECT id FROM ac_artists WHERE slug = :slug LIMIT 1");
$params = $id > 0 ? [':slug' => $slug, ':id' => $id] : [':slug' => $slug];
$dupStmt->execute($params);
if ($dupStmt->fetch()) {
return $this->saveError($id, 'Slug already exists.');
}
try {
if ($id > 0) {
$stmt = $db->prepare("
UPDATE ac_artists
SET name = :name, slug = :slug, bio = :bio" . ($creditsReady ? ", credits = :credits" : "") . ", country = :country,
avatar_url = :avatar_url" . ($socialReady ? ", social_links = :social_links" : "") . ",
is_active = :is_active
WHERE id = :id
");
$params = [
':name' => $name,
':slug' => $slug,
':bio' => $bio !== '' ? $bio : null,
':country' => $country !== '' ? $country : null,
':avatar_url' => $avatarUrl !== '' ? $avatarUrl : null,
':is_active' => $isActive,
':id' => $id,
];
if ($creditsReady) {
$params[':credits'] = $credits !== '' ? $credits : null;
}
if ($socialReady) {
$params[':social_links'] = $socialJson;
}
$stmt->execute($params);
} else {
$stmt = $db->prepare("
INSERT INTO ac_artists (name, slug, bio" . ($creditsReady ? ", credits" : "") . ", country, avatar_url" . ($socialReady ? ", social_links" : "") . ", is_active)
VALUES (:name, :slug, :bio" . ($creditsReady ? ", :credits" : "") . ", :country, :avatar_url" . ($socialReady ? ", :social_links" : "") . ", :is_active)
");
$params = [
':name' => $name,
':slug' => $slug,
':bio' => $bio !== '' ? $bio : null,
':country' => $country !== '' ? $country : null,
':avatar_url' => $avatarUrl !== '' ? $avatarUrl : null,
':is_active' => $isActive,
];
if ($creditsReady) {
$params[':credits'] = $credits !== '' ? $credits : null;
}
if ($socialReady) {
$params[':social_links'] = $socialJson;
}
$stmt->execute($params);
}
} catch (Throwable $e) {
error_log('AC artists save error: ' . $e->getMessage());
return $this->saveError($id, 'Unable to save artist. Check table columns and input.');
}
return new Response('', 302, ['Location' => '/admin/artists']);
}
public function adminDelete(): Response
{
if (!Auth::check()) {
return new Response('', 302, ['Location' => '/admin/login']);
}
if (!Auth::hasRole(['admin', 'manager'])) {
return new Response('', 302, ['Location' => '/admin']);
}
$id = (int)($_POST['id'] ?? 0);
$db = Database::get();
if ($db instanceof PDO && $id > 0) {
$stmt = $db->prepare("DELETE FROM ac_artists WHERE id = :id");
$stmt->execute([':id' => $id]);
}
return new Response('', 302, ['Location' => '/admin/artists']);
}
public function adminUpload(): Response
{
if (!Auth::check()) {
return new Response('', 302, ['Location' => '/admin/login']);
}
if (!Auth::hasRole(['admin', 'manager'])) {
return new Response('', 302, ['Location' => '/admin']);
}
$file = $_FILES['artist_avatar'] ?? null;
$artistId = (int)($_POST['artist_id'] ?? 0);
if (!$file || !isset($file['tmp_name'])) {
return $this->uploadRedirect($artistId);
}
if ($file['error'] !== UPLOAD_ERR_OK) {
return $this->uploadRedirect($artistId, $this->uploadErrorMessage((int)$file['error']));
}
$tmp = (string)$file['tmp_name'];
if ($tmp === '' || !is_uploaded_file($tmp)) {
return $this->uploadRedirect($artistId);
}
$info = getimagesize($tmp);
if ($info === false) {
return $this->uploadRedirect($artistId, 'Avatar must be an image.');
}
$ext = image_type_to_extension($info[2], false);
$allowed = ['jpg', 'jpeg', 'png', 'webp'];
if (!in_array($ext, $allowed, true)) {
return $this->uploadRedirect($artistId, 'Avatar must be JPG, PNG, or WEBP.');
}
$uploadDir = __DIR__ . '/../../uploads/media';
if (!is_dir($uploadDir) && !mkdir($uploadDir, 0755, true)) {
return $this->uploadRedirect($artistId, 'Upload directory is not writable.');
}
if (!is_writable($uploadDir)) {
return $this->uploadRedirect($artistId, 'Upload directory is not writable.');
}
$baseName = preg_replace('~[^a-z0-9]+~', '-', strtolower((string)$file['name'])) ?? 'avatar';
$baseName = trim($baseName, '-');
$fileName = ($baseName !== '' ? $baseName : 'avatar') . '-' . date('YmdHis') . '.' . $ext;
$dest = $uploadDir . '/' . $fileName;
if (!move_uploaded_file($tmp, $dest)) {
return $this->uploadRedirect($artistId, 'Upload failed.');
}
$fileUrl = '/uploads/media/' . $fileName;
$fileType = (string)($file['type'] ?? '');
$fileSize = (int)($file['size'] ?? 0);
$db = Database::get();
if ($db instanceof PDO) {
try {
$stmt = $db->prepare("
INSERT INTO ac_media (file_name, file_url, file_type, file_size, folder_id)
VALUES (:name, :url, :type, :size, NULL)
");
$stmt->execute([
':name' => (string)$file['name'],
':url' => $fileUrl,
':type' => $fileType,
':size' => $fileSize,
]);
} catch (Throwable $e) {
}
}
if ($artistId > 0 && $db instanceof PDO) {
$stmt = $db->prepare("UPDATE ac_artists SET avatar_url = :url WHERE id = :id");
$stmt->execute([':url' => $fileUrl, ':id' => $artistId]);
}
return $this->uploadRedirect($artistId, '', $fileUrl);
}
public function adminInstall(): Response
{
if (!Auth::check()) {
return new Response('', 302, ['Location' => '/admin/login']);
}
if (!Auth::hasRole(['admin', 'manager'])) {
return new Response('', 302, ['Location' => '/admin']);
}
$db = Database::get();
if ($db instanceof PDO) {
try {
$db->exec("
CREATE TABLE IF NOT EXISTS ac_artists (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(200) NOT NULL,
slug VARCHAR(200) NOT NULL UNIQUE,
bio MEDIUMTEXT NULL,
credits MEDIUMTEXT NULL,
country VARCHAR(120) NULL,
avatar_url VARCHAR(255) NULL,
social_links TEXT NULL,
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
$db->exec("ALTER TABLE ac_artists ADD COLUMN credits MEDIUMTEXT NULL");
$db->exec("ALTER TABLE ac_artists ADD COLUMN social_links TEXT NULL");
} catch (Throwable $e) {
}
}
return new Response('', 302, ['Location' => '/admin/artists']);
}
private function artistsTableReady(): bool
{
$db = Database::get();
if (!$db instanceof PDO) {
return false;
}
try {
$stmt = $db->query("SELECT 1 FROM ac_artists LIMIT 1");
return $stmt !== false;
} catch (Throwable $e) {
return false;
}
}
private function socialColumnReady(): bool
{
$db = Database::get();
if (!$db instanceof PDO) {
return false;
}
try {
$stmt = $db->query("SHOW COLUMNS FROM ac_artists LIKE 'social_links'");
$row = $stmt ? $stmt->fetch(PDO::FETCH_ASSOC) : null;
return (bool)$row;
} catch (Throwable $e) {
return false;
}
}
private function creditsColumnReady(): bool
{
$db = Database::get();
if (!$db instanceof PDO) {
return false;
}
try {
$stmt = $db->query("SHOW COLUMNS FROM ac_artists LIKE 'credits'");
$row = $stmt ? $stmt->fetch(PDO::FETCH_ASSOC) : null;
return (bool)$row;
} catch (Throwable $e) {
return false;
}
}
private function saveError(int $id, string $message): Response
{
return new Response($this->view->render('admin/edit.php', [
'title' => $id > 0 ? 'Edit Artist' : 'New Artist',
'artist' => [
'id' => $id,
'name' => (string)($_POST['name'] ?? ''),
'slug' => (string)($_POST['slug'] ?? ''),
'bio' => (string)($_POST['bio'] ?? ''),
'credits' => (string)($_POST['credits'] ?? ''),
'country' => (string)($_POST['country'] ?? ''),
'avatar_url' => (string)($_POST['avatar_url'] ?? ''),
'social_links' => '',
'is_active' => isset($_POST['is_active']) ? 1 : 0,
],
'error' => $message,
]));
}
private function slugify(string $value): string
{
$value = strtolower(trim($value));
$value = preg_replace('~[^a-z0-9]+~', '-', $value) ?? $value;
$value = trim($value, '-');
return $value !== '' ? $value : 'artist';
}
private function uploadRedirect(int $artistId, string $error = '', string $url = ''): Response
{
if ($artistId > 0) {
$target = '/admin/artists/edit?id=' . $artistId;
} else {
$target = '/admin/artists/new';
}
if ($url !== '') {
$target .= '&avatar_url=' . rawurlencode($url);
}
if ($error !== '') {
$target .= '&upload_error=' . rawurlencode($error);
}
return new Response('', 302, ['Location' => $target]);
}
private function uploadErrorMessage(int $code): string
{
$max = (string)ini_get('upload_max_filesize');
$map = [
UPLOAD_ERR_INI_SIZE => "File exceeds upload_max_filesize ({$max}).",
UPLOAD_ERR_FORM_SIZE => 'File exceeds form limit.',
UPLOAD_ERR_PARTIAL => 'File was only partially uploaded.',
UPLOAD_ERR_NO_TMP_DIR => 'Missing temp upload directory.',
UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk.',
UPLOAD_ERR_EXTENSION => 'Upload stopped by a PHP extension.',
UPLOAD_ERR_NO_FILE => 'No file uploaded.',
];
return $map[$code] ?? 'Upload failed.';
}
private function normalizeUrl(string $value): string
{
if ($value === '') {
return '';
}
if (preg_match('~^(https?://)~i', $value)) {
return $value;
}
return 'https://' . $value;
}
}