1262 lines
56 KiB
PHP
1262 lines
56 KiB
PHP
|
|
<?php
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
namespace Plugins\Releases;
|
||
|
|
|
||
|
|
use Core\Http\Response;
|
||
|
|
use Core\Services\Auth;
|
||
|
|
use Core\Services\Database;
|
||
|
|
use Core\Services\Plugins;
|
||
|
|
use Core\Services\Settings;
|
||
|
|
use Core\Views\View;
|
||
|
|
use PDO;
|
||
|
|
use Throwable;
|
||
|
|
|
||
|
|
class ReleasesController
|
||
|
|
{
|
||
|
|
private View $view;
|
||
|
|
|
||
|
|
public function __construct()
|
||
|
|
{
|
||
|
|
$this->view = new View(__DIR__ . '/views');
|
||
|
|
}
|
||
|
|
|
||
|
|
public function index(): Response
|
||
|
|
{
|
||
|
|
$this->ensureReleaseArtistColumn();
|
||
|
|
$db = Database::get();
|
||
|
|
$page = null;
|
||
|
|
$releases = [];
|
||
|
|
$artistOptions = [];
|
||
|
|
$artistFilter = trim((string)($_GET['artist'] ?? ''));
|
||
|
|
$search = trim((string)($_GET['q'] ?? ''));
|
||
|
|
$sort = trim((string)($_GET['sort'] ?? 'newest'));
|
||
|
|
$currentPage = max(1, (int)($_GET['p'] ?? 1));
|
||
|
|
$perPage = 20;
|
||
|
|
$totalReleases = 0;
|
||
|
|
$totalPages = 1;
|
||
|
|
$allowedSorts = [
|
||
|
|
'newest' => 'r.release_date DESC, r.created_at DESC',
|
||
|
|
'oldest' => 'r.release_date ASC, r.created_at ASC',
|
||
|
|
'title_asc' => 'r.title ASC',
|
||
|
|
'title_desc' => 'r.title DESC',
|
||
|
|
];
|
||
|
|
if (!isset($allowedSorts[$sort])) {
|
||
|
|
$sort = 'newest';
|
||
|
|
}
|
||
|
|
if ($db instanceof PDO) {
|
||
|
|
try {
|
||
|
|
$stmt = $db->prepare("SELECT title, content_html FROM ac_pages WHERE slug = 'releases' AND is_published = 1 LIMIT 1");
|
||
|
|
$stmt->execute();
|
||
|
|
$page = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
|
||
|
|
$artistJoinReady = false;
|
||
|
|
try {
|
||
|
|
$probe = $db->query("SHOW COLUMNS FROM ac_releases LIKE 'artist_id'");
|
||
|
|
$artistJoinReady = (bool)($probe && $probe->fetch(PDO::FETCH_ASSOC));
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
$artistJoinReady = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
$params = [];
|
||
|
|
$where = ["r.is_published = 1"];
|
||
|
|
if ($artistFilter !== '') {
|
||
|
|
if ($artistJoinReady) {
|
||
|
|
$where[] = "(r.artist_name = :artist OR a.name = :artist)";
|
||
|
|
} else {
|
||
|
|
$where[] = "r.artist_name = :artist";
|
||
|
|
}
|
||
|
|
$params[':artist'] = $artistFilter;
|
||
|
|
}
|
||
|
|
if ($search !== '') {
|
||
|
|
$where[] = "(r.title LIKE :search OR r.catalog_no LIKE :search OR r.slug LIKE :search OR r.artist_name LIKE :search" . ($artistJoinReady ? " OR a.name LIKE :search" : "") . ")";
|
||
|
|
$params[':search'] = '%' . $search . '%';
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($artistJoinReady) {
|
||
|
|
$countSql = "
|
||
|
|
SELECT COUNT(*) AS total_rows
|
||
|
|
FROM ac_releases r
|
||
|
|
LEFT JOIN ac_artists a ON a.id = r.artist_id
|
||
|
|
WHERE " . implode(' AND ', $where);
|
||
|
|
$countStmt = $db->prepare($countSql);
|
||
|
|
$countStmt->execute($params);
|
||
|
|
$totalReleases = (int)($countStmt->fetchColumn() ?: 0);
|
||
|
|
$totalPages = max(1, (int)ceil($totalReleases / $perPage));
|
||
|
|
if ($currentPage > $totalPages) {
|
||
|
|
$currentPage = $totalPages;
|
||
|
|
}
|
||
|
|
$offset = ($currentPage - 1) * $perPage;
|
||
|
|
|
||
|
|
$listSql = "
|
||
|
|
SELECT r.id, r.title, r.slug, r.release_date, r.cover_url,
|
||
|
|
COALESCE(r.artist_name, a.name) AS artist_name
|
||
|
|
FROM ac_releases r
|
||
|
|
LEFT JOIN ac_artists a ON a.id = r.artist_id
|
||
|
|
WHERE " . implode(' AND ', $where) . "
|
||
|
|
ORDER BY {$allowedSorts[$sort]}
|
||
|
|
LIMIT :limit OFFSET :offset
|
||
|
|
";
|
||
|
|
$listStmt = $db->prepare($listSql);
|
||
|
|
foreach ($params as $k => $v) {
|
||
|
|
$listStmt->bindValue($k, $v);
|
||
|
|
}
|
||
|
|
$listStmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
|
||
|
|
$listStmt->bindValue(':offset', $offset, PDO::PARAM_INT);
|
||
|
|
$listStmt->execute();
|
||
|
|
|
||
|
|
$artistStmt = $db->query("
|
||
|
|
SELECT DISTINCT TRIM(COALESCE(NULLIF(r.artist_name, ''), a.name)) AS artist_name
|
||
|
|
FROM ac_releases r
|
||
|
|
LEFT JOIN ac_artists a ON a.id = r.artist_id
|
||
|
|
WHERE r.is_published = 1
|
||
|
|
ORDER BY artist_name ASC
|
||
|
|
");
|
||
|
|
} else {
|
||
|
|
$countSql = "
|
||
|
|
SELECT COUNT(*) AS total_rows
|
||
|
|
FROM ac_releases r
|
||
|
|
WHERE " . implode(' AND ', $where);
|
||
|
|
$countStmt = $db->prepare($countSql);
|
||
|
|
$countStmt->execute($params);
|
||
|
|
$totalReleases = (int)($countStmt->fetchColumn() ?: 0);
|
||
|
|
$totalPages = max(1, (int)ceil($totalReleases / $perPage));
|
||
|
|
if ($currentPage > $totalPages) {
|
||
|
|
$currentPage = $totalPages;
|
||
|
|
}
|
||
|
|
$offset = ($currentPage - 1) * $perPage;
|
||
|
|
|
||
|
|
$listSql = "
|
||
|
|
SELECT r.id, r.title, r.slug, r.release_date, r.cover_url, r.artist_name
|
||
|
|
FROM ac_releases r
|
||
|
|
WHERE " . implode(' AND ', $where) . "
|
||
|
|
ORDER BY {$allowedSorts[$sort]}
|
||
|
|
LIMIT :limit OFFSET :offset
|
||
|
|
";
|
||
|
|
$listStmt = $db->prepare($listSql);
|
||
|
|
foreach ($params as $k => $v) {
|
||
|
|
$listStmt->bindValue($k, $v);
|
||
|
|
}
|
||
|
|
$listStmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
|
||
|
|
$listStmt->bindValue(':offset', $offset, PDO::PARAM_INT);
|
||
|
|
$listStmt->execute();
|
||
|
|
|
||
|
|
$artistStmt = $db->query("
|
||
|
|
SELECT DISTINCT TRIM(artist_name) AS artist_name
|
||
|
|
FROM ac_releases
|
||
|
|
WHERE is_published = 1
|
||
|
|
ORDER BY artist_name ASC
|
||
|
|
");
|
||
|
|
}
|
||
|
|
|
||
|
|
$releases = $listStmt->fetchAll(PDO::FETCH_ASSOC);
|
||
|
|
$rawArtistRows = $artistStmt ? $artistStmt->fetchAll(PDO::FETCH_ASSOC) : [];
|
||
|
|
foreach ($rawArtistRows as $row) {
|
||
|
|
$name = trim((string)($row['artist_name'] ?? ''));
|
||
|
|
if ($name !== '') {
|
||
|
|
$artistOptions[] = $name;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
$artistOptions = array_values(array_unique($artistOptions));
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return new Response($this->view->render('site/index.php', [
|
||
|
|
'title' => (string)($page['title'] ?? 'Releases'),
|
||
|
|
'content_html' => (string)($page['content_html'] ?? ''),
|
||
|
|
'releases' => $releases,
|
||
|
|
'total_releases' => $totalReleases,
|
||
|
|
'per_page' => $perPage,
|
||
|
|
'current_page' => $currentPage,
|
||
|
|
'total_pages' => $totalPages,
|
||
|
|
'artist_filter' => $artistFilter,
|
||
|
|
'artist_options' => $artistOptions,
|
||
|
|
'search' => $search,
|
||
|
|
'sort' => $sort,
|
||
|
|
]));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function show(): Response
|
||
|
|
{
|
||
|
|
$this->ensureReleaseArtistColumn();
|
||
|
|
$slug = trim((string)($_GET['slug'] ?? ''));
|
||
|
|
$release = null;
|
||
|
|
$tracks = [];
|
||
|
|
Plugins::sync();
|
||
|
|
$storePluginEnabled = Plugins::isEnabled('store');
|
||
|
|
if ($slug !== '') {
|
||
|
|
$db = Database::get();
|
||
|
|
if ($db instanceof PDO) {
|
||
|
|
try {
|
||
|
|
$stmt = $db->prepare("SELECT * FROM ac_releases WHERE slug = :slug AND is_published = 1 LIMIT 1");
|
||
|
|
$stmt->execute([':slug' => $slug]);
|
||
|
|
$release = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
|
||
|
|
if ($release) {
|
||
|
|
if ($storePluginEnabled) {
|
||
|
|
try {
|
||
|
|
$bundleStmt = $db->prepare("
|
||
|
|
SELECT is_enabled, bundle_price, currency, purchase_label
|
||
|
|
FROM ac_store_release_products
|
||
|
|
WHERE release_id = :release_id
|
||
|
|
LIMIT 1
|
||
|
|
");
|
||
|
|
$bundleStmt->execute([':release_id' => (int)$release['id']]);
|
||
|
|
$bundle = $bundleStmt->fetch(PDO::FETCH_ASSOC);
|
||
|
|
if ($bundle) {
|
||
|
|
$release['store_enabled'] = (int)($bundle['is_enabled'] ?? 0);
|
||
|
|
$release['bundle_price'] = (float)($bundle['bundle_price'] ?? 0);
|
||
|
|
$release['store_currency'] = (string)($bundle['currency'] ?? 'GBP');
|
||
|
|
$release['purchase_label'] = (string)($bundle['purchase_label'] ?? '');
|
||
|
|
}
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if ($storePluginEnabled) {
|
||
|
|
$trackStmt = $db->prepare("
|
||
|
|
SELECT t.id, t.track_no, t.title, t.mix_name, t.duration, t.bpm, t.key_signature, t.sample_url,
|
||
|
|
COALESCE(sp.is_enabled, 0) AS store_enabled,
|
||
|
|
COALESCE(sp.track_price, 0.00) AS track_price,
|
||
|
|
COALESCE(sp.currency, 'GBP') AS store_currency
|
||
|
|
FROM ac_release_tracks t
|
||
|
|
LEFT JOIN ac_store_track_products sp ON sp.release_track_id = t.id
|
||
|
|
WHERE t.release_id = :rid
|
||
|
|
ORDER BY t.track_no ASC, t.id ASC
|
||
|
|
");
|
||
|
|
} else {
|
||
|
|
$trackStmt = $db->prepare("
|
||
|
|
SELECT id, track_no, title, mix_name, duration, bpm, key_signature, sample_url
|
||
|
|
FROM ac_release_tracks
|
||
|
|
WHERE release_id = :rid
|
||
|
|
ORDER BY track_no ASC, id ASC
|
||
|
|
");
|
||
|
|
}
|
||
|
|
$trackStmt->execute([':rid' => (int)$release['id']]);
|
||
|
|
$tracks = $trackStmt->fetchAll(PDO::FETCH_ASSOC);
|
||
|
|
}
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
$tracks = [];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return new Response($this->view->render('site/show.php', [
|
||
|
|
'title' => $release ? (string)$release['title'] : 'Release',
|
||
|
|
'release' => $release,
|
||
|
|
'tracks' => $tracks,
|
||
|
|
'store_plugin_enabled' => $storePluginEnabled,
|
||
|
|
]));
|
||
|
|
}
|
||
|
|
|
||
|
|
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']);
|
||
|
|
}
|
||
|
|
$this->ensureReleaseArtistColumn();
|
||
|
|
$tableReady = $this->releasesTableReady();
|
||
|
|
$releases = [];
|
||
|
|
$pageId = 0;
|
||
|
|
$pagePublished = 0;
|
||
|
|
if ($tableReady) {
|
||
|
|
$db = Database::get();
|
||
|
|
if ($db instanceof PDO) {
|
||
|
|
$stmt = $db->query("SELECT id, title, slug, artist_name, release_date, cover_url, is_published FROM ac_releases ORDER BY created_at DESC");
|
||
|
|
$releases = $stmt ? $stmt->fetchAll(PDO::FETCH_ASSOC) : [];
|
||
|
|
$pageStmt = $db->prepare("SELECT id, is_published FROM ac_pages WHERE slug = 'releases' 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' => 'Releases',
|
||
|
|
'table_ready' => $tableReady,
|
||
|
|
'releases' => $releases,
|
||
|
|
'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']);
|
||
|
|
}
|
||
|
|
Plugins::sync();
|
||
|
|
$storePluginEnabled = Plugins::isEnabled('store');
|
||
|
|
|
||
|
|
$release = [
|
||
|
|
'id' => 0,
|
||
|
|
'title' => '',
|
||
|
|
'slug' => '',
|
||
|
|
'artist_name' => '',
|
||
|
|
'description' => '',
|
||
|
|
'credits' => '',
|
||
|
|
'catalog_no' => '',
|
||
|
|
'release_date' => '',
|
||
|
|
'cover_url' => '',
|
||
|
|
'sample_url' => '',
|
||
|
|
'is_published' => 1,
|
||
|
|
'store_enabled' => 0,
|
||
|
|
'bundle_price' => '',
|
||
|
|
'store_currency' => 'GBP',
|
||
|
|
'purchase_label' => '',
|
||
|
|
];
|
||
|
|
if ($id > 0) {
|
||
|
|
$db = Database::get();
|
||
|
|
if ($db instanceof PDO) {
|
||
|
|
$stmt = $db->prepare("SELECT * FROM ac_releases WHERE id = :id LIMIT 1");
|
||
|
|
$stmt->execute([':id' => $id]);
|
||
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||
|
|
if ($row) {
|
||
|
|
$release = array_merge($release, $row);
|
||
|
|
}
|
||
|
|
if ($storePluginEnabled) {
|
||
|
|
try {
|
||
|
|
$storeStmt = $db->prepare("
|
||
|
|
SELECT is_enabled, bundle_price, currency, purchase_label
|
||
|
|
FROM ac_store_release_products
|
||
|
|
WHERE release_id = :release_id
|
||
|
|
LIMIT 1
|
||
|
|
");
|
||
|
|
$storeStmt->execute([':release_id' => $id]);
|
||
|
|
$storeRow = $storeStmt->fetch(PDO::FETCH_ASSOC);
|
||
|
|
if ($storeRow) {
|
||
|
|
$release['store_enabled'] = (int)($storeRow['is_enabled'] ?? 0);
|
||
|
|
$release['bundle_price'] = (string)($storeRow['bundle_price'] ?? '');
|
||
|
|
$release['store_currency'] = (string)($storeRow['currency'] ?? 'GBP');
|
||
|
|
$release['purchase_label'] = (string)($storeRow['purchase_label'] ?? '');
|
||
|
|
}
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!empty($_GET['cover_url']) && $release['cover_url'] === '') {
|
||
|
|
$release['cover_url'] = (string)$_GET['cover_url'];
|
||
|
|
}
|
||
|
|
if (!empty($_GET['sample_url']) && $release['sample_url'] === '') {
|
||
|
|
$release['sample_url'] = (string)$_GET['sample_url'];
|
||
|
|
}
|
||
|
|
return new Response($this->view->render('admin/edit.php', [
|
||
|
|
'title' => $id > 0 ? 'Edit Release' : 'New Release',
|
||
|
|
'release' => $release,
|
||
|
|
'store_plugin_enabled' => $storePluginEnabled,
|
||
|
|
'error' => '',
|
||
|
|
]));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function adminTracks(int $releaseId = 0): Response
|
||
|
|
{
|
||
|
|
if (!Auth::check()) {
|
||
|
|
return new Response('', 302, ['Location' => '/admin/login']);
|
||
|
|
}
|
||
|
|
if (!Auth::hasRole(['admin', 'manager'])) {
|
||
|
|
return new Response('', 302, ['Location' => '/admin']);
|
||
|
|
}
|
||
|
|
|
||
|
|
$release = null;
|
||
|
|
$tracks = [];
|
||
|
|
$tableReady = $this->tracksTableReady();
|
||
|
|
|
||
|
|
if ($tableReady && $releaseId > 0) {
|
||
|
|
$db = Database::get();
|
||
|
|
if ($db instanceof PDO) {
|
||
|
|
$relStmt = $db->prepare("SELECT id, title, slug FROM ac_releases WHERE id = :id LIMIT 1");
|
||
|
|
$relStmt->execute([':id' => $releaseId]);
|
||
|
|
$release = $relStmt->fetch(PDO::FETCH_ASSOC) ?: null;
|
||
|
|
if ($release) {
|
||
|
|
$trackStmt = $db->prepare("
|
||
|
|
SELECT id, track_no, title, mix_name, duration, bpm, key_signature, sample_url
|
||
|
|
FROM ac_release_tracks
|
||
|
|
WHERE release_id = :rid
|
||
|
|
ORDER BY track_no ASC, id ASC
|
||
|
|
");
|
||
|
|
$trackStmt->execute([':rid' => $releaseId]);
|
||
|
|
$tracks = $trackStmt->fetchAll(PDO::FETCH_ASSOC);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return new Response($this->view->render('admin/tracks_index.php', [
|
||
|
|
'title' => 'Release Tracks',
|
||
|
|
'release' => $release,
|
||
|
|
'tracks' => $tracks,
|
||
|
|
'table_ready' => $tableReady,
|
||
|
|
'release_id' => $releaseId,
|
||
|
|
]));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function adminTrackEdit(int $id = 0, int $releaseId = 0): Response
|
||
|
|
{
|
||
|
|
if (!Auth::check()) {
|
||
|
|
return new Response('', 302, ['Location' => '/admin/login']);
|
||
|
|
}
|
||
|
|
if (!Auth::hasRole(['admin', 'manager'])) {
|
||
|
|
return new Response('', 302, ['Location' => '/admin']);
|
||
|
|
}
|
||
|
|
|
||
|
|
Plugins::sync();
|
||
|
|
$storePluginEnabled = Plugins::isEnabled('store');
|
||
|
|
|
||
|
|
$track = [
|
||
|
|
'id' => 0,
|
||
|
|
'release_id' => $releaseId,
|
||
|
|
'track_no' => '',
|
||
|
|
'title' => '',
|
||
|
|
'mix_name' => '',
|
||
|
|
'duration' => '',
|
||
|
|
'bpm' => '',
|
||
|
|
'key_signature' => '',
|
||
|
|
'sample_url' => '',
|
||
|
|
'store_enabled' => 0,
|
||
|
|
'track_price' => '',
|
||
|
|
'store_currency' => 'GBP',
|
||
|
|
'full_file_url' => '',
|
||
|
|
];
|
||
|
|
|
||
|
|
$release = null;
|
||
|
|
$db = Database::get();
|
||
|
|
if ($db instanceof PDO && $releaseId > 0) {
|
||
|
|
$relStmt = $db->prepare("SELECT id, title FROM ac_releases WHERE id = :id LIMIT 1");
|
||
|
|
$relStmt->execute([':id' => $releaseId]);
|
||
|
|
$release = $relStmt->fetch(PDO::FETCH_ASSOC) ?: null;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($id > 0 && $db instanceof PDO) {
|
||
|
|
$stmt = $db->prepare("SELECT * FROM ac_release_tracks WHERE id = :id LIMIT 1");
|
||
|
|
$stmt->execute([':id' => $id]);
|
||
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||
|
|
if ($row) {
|
||
|
|
$track = array_merge($track, $row);
|
||
|
|
$releaseId = (int)($track['release_id'] ?? $releaseId);
|
||
|
|
if ($storePluginEnabled) {
|
||
|
|
try {
|
||
|
|
$storeStmt = $db->prepare("
|
||
|
|
SELECT is_enabled, track_price, currency
|
||
|
|
FROM ac_store_track_products
|
||
|
|
WHERE release_track_id = :track_id
|
||
|
|
LIMIT 1
|
||
|
|
");
|
||
|
|
$storeStmt->execute([':track_id' => $id]);
|
||
|
|
$storeRow = $storeStmt->fetch(PDO::FETCH_ASSOC);
|
||
|
|
if ($storeRow) {
|
||
|
|
$track['store_enabled'] = (int)($storeRow['is_enabled'] ?? 0);
|
||
|
|
$track['track_price'] = (string)($storeRow['track_price'] ?? '');
|
||
|
|
$track['store_currency'] = (string)($storeRow['currency'] ?? 'GBP');
|
||
|
|
}
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
$fileStmt = $db->prepare("
|
||
|
|
SELECT file_url
|
||
|
|
FROM ac_store_files
|
||
|
|
WHERE scope_type = 'track' AND scope_id = :track_id AND is_active = 1
|
||
|
|
ORDER BY id DESC
|
||
|
|
LIMIT 1
|
||
|
|
");
|
||
|
|
$fileStmt->execute([':track_id' => $id]);
|
||
|
|
$fileRow = $fileStmt->fetch(PDO::FETCH_ASSOC);
|
||
|
|
if ($fileRow) {
|
||
|
|
$track['full_file_url'] = (string)($fileRow['file_url'] ?? '');
|
||
|
|
}
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!empty($_GET['sample_url']) && $track['sample_url'] === '') {
|
||
|
|
$track['sample_url'] = (string)$_GET['sample_url'];
|
||
|
|
}
|
||
|
|
if (!empty($_GET['full_file_url']) && $track['full_file_url'] === '') {
|
||
|
|
$track['full_file_url'] = (string)$_GET['full_file_url'];
|
||
|
|
}
|
||
|
|
|
||
|
|
return new Response($this->view->render('admin/track_edit.php', [
|
||
|
|
'title' => $id > 0 ? 'Edit Track' : 'New Track',
|
||
|
|
'track' => $track,
|
||
|
|
'release' => $release,
|
||
|
|
'store_plugin_enabled' => $storePluginEnabled,
|
||
|
|
'error' => '',
|
||
|
|
]));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function adminTrackSave(): Response
|
||
|
|
{
|
||
|
|
if (!Auth::check()) {
|
||
|
|
return new Response('', 302, ['Location' => '/admin/login']);
|
||
|
|
}
|
||
|
|
if (!Auth::hasRole(['admin', 'manager'])) {
|
||
|
|
return new Response('', 302, ['Location' => '/admin']);
|
||
|
|
}
|
||
|
|
Plugins::sync();
|
||
|
|
$storePluginEnabled = Plugins::isEnabled('store');
|
||
|
|
|
||
|
|
$id = (int)($_POST['id'] ?? 0);
|
||
|
|
$releaseId = (int)($_POST['release_id'] ?? 0);
|
||
|
|
$trackNo = (int)($_POST['track_no'] ?? 0);
|
||
|
|
$title = trim((string)($_POST['title'] ?? ''));
|
||
|
|
$mixName = trim((string)($_POST['mix_name'] ?? ''));
|
||
|
|
$duration = trim((string)($_POST['duration'] ?? ''));
|
||
|
|
$bpm = trim((string)($_POST['bpm'] ?? ''));
|
||
|
|
$keySig = trim((string)($_POST['key_signature'] ?? ''));
|
||
|
|
$sampleUrl = trim((string)($_POST['sample_url'] ?? ''));
|
||
|
|
$storeEnabled = isset($_POST['store_enabled']) ? 1 : 0;
|
||
|
|
$trackPrice = trim((string)($_POST['track_price'] ?? ''));
|
||
|
|
$storeCurrency = strtoupper(trim((string)($_POST['store_currency'] ?? 'GBP')));
|
||
|
|
$fullFileUrl = trim((string)($_POST['full_file_url'] ?? ''));
|
||
|
|
if (!preg_match('/^[A-Z]{3}$/', $storeCurrency)) {
|
||
|
|
$storeCurrency = 'GBP';
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($releaseId <= 0) {
|
||
|
|
return $this->trackSaveError($id, $releaseId, 'Release is required.');
|
||
|
|
}
|
||
|
|
if ($title === '') {
|
||
|
|
return $this->trackSaveError($id, $releaseId, 'Track title is required.');
|
||
|
|
}
|
||
|
|
|
||
|
|
$db = Database::get();
|
||
|
|
if (!$db instanceof PDO) {
|
||
|
|
return $this->trackSaveError($id, $releaseId, 'Database unavailable.');
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
if ($id > 0) {
|
||
|
|
$stmt = $db->prepare("
|
||
|
|
UPDATE ac_release_tracks
|
||
|
|
SET track_no = :track_no, title = :title, mix_name = :mix_name, duration = :duration,
|
||
|
|
bpm = :bpm, key_signature = :key_signature, sample_url = :sample_url
|
||
|
|
WHERE id = :id
|
||
|
|
");
|
||
|
|
$stmt->execute([
|
||
|
|
':track_no' => $trackNo > 0 ? $trackNo : null,
|
||
|
|
':title' => $title,
|
||
|
|
':mix_name' => $mixName !== '' ? $mixName : null,
|
||
|
|
':duration' => $duration !== '' ? $duration : null,
|
||
|
|
':bpm' => $bpm !== '' ? (int)$bpm : null,
|
||
|
|
':key_signature' => $keySig !== '' ? $keySig : null,
|
||
|
|
':sample_url' => $sampleUrl !== '' ? $sampleUrl : null,
|
||
|
|
':id' => $id,
|
||
|
|
]);
|
||
|
|
$trackId = $id;
|
||
|
|
} else {
|
||
|
|
$stmt = $db->prepare("
|
||
|
|
INSERT INTO ac_release_tracks
|
||
|
|
(release_id, track_no, title, mix_name, duration, bpm, key_signature, sample_url)
|
||
|
|
VALUES (:release_id, :track_no, :title, :mix_name, :duration, :bpm, :key_signature, :sample_url)
|
||
|
|
");
|
||
|
|
$stmt->execute([
|
||
|
|
':release_id' => $releaseId,
|
||
|
|
':track_no' => $trackNo > 0 ? $trackNo : null,
|
||
|
|
':title' => $title,
|
||
|
|
':mix_name' => $mixName !== '' ? $mixName : null,
|
||
|
|
':duration' => $duration !== '' ? $duration : null,
|
||
|
|
':bpm' => $bpm !== '' ? (int)$bpm : null,
|
||
|
|
':key_signature' => $keySig !== '' ? $keySig : null,
|
||
|
|
':sample_url' => $sampleUrl !== '' ? $sampleUrl : null,
|
||
|
|
]);
|
||
|
|
$trackId = (int)$db->lastInsertId();
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($storePluginEnabled) {
|
||
|
|
try {
|
||
|
|
$storeStmt = $db->prepare("
|
||
|
|
INSERT INTO ac_store_track_products (release_track_id, is_enabled, track_price, currency, created_at, updated_at)
|
||
|
|
VALUES (:track_id, :is_enabled, :track_price, :currency, NOW(), NOW())
|
||
|
|
ON DUPLICATE KEY UPDATE
|
||
|
|
is_enabled = VALUES(is_enabled),
|
||
|
|
track_price = VALUES(track_price),
|
||
|
|
currency = VALUES(currency),
|
||
|
|
updated_at = NOW()
|
||
|
|
");
|
||
|
|
$storeStmt->execute([
|
||
|
|
':track_id' => $trackId,
|
||
|
|
':is_enabled' => $storeEnabled,
|
||
|
|
':track_price' => $trackPrice !== '' ? (float)$trackPrice : 0.00,
|
||
|
|
':currency' => $storeCurrency,
|
||
|
|
]);
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($fullFileUrl !== '') {
|
||
|
|
try {
|
||
|
|
$db->prepare("
|
||
|
|
UPDATE ac_store_files
|
||
|
|
SET is_active = 0
|
||
|
|
WHERE scope_type = 'track' AND scope_id = :track_id
|
||
|
|
")->execute([':track_id' => $trackId]);
|
||
|
|
|
||
|
|
$fileName = basename(parse_url($fullFileUrl, PHP_URL_PATH) ?: $fullFileUrl);
|
||
|
|
$insFile = $db->prepare("
|
||
|
|
INSERT INTO ac_store_files
|
||
|
|
(scope_type, scope_id, file_url, file_name, file_size, mime_type, is_active, created_at)
|
||
|
|
VALUES ('track', :scope_id, :file_url, :file_name, NULL, NULL, 1, NOW())
|
||
|
|
");
|
||
|
|
$insFile->execute([
|
||
|
|
':scope_id' => $trackId,
|
||
|
|
':file_url' => $fullFileUrl,
|
||
|
|
':file_name' => $fileName !== '' ? $fileName : 'track-file',
|
||
|
|
]);
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
error_log('AC release tracks save error: ' . $e->getMessage());
|
||
|
|
return $this->trackSaveError($id, $releaseId, 'Unable to save track.');
|
||
|
|
}
|
||
|
|
|
||
|
|
return new Response('', 302, ['Location' => '/admin/releases/tracks?release_id=' . $releaseId]);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function adminTrackDelete(): 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);
|
||
|
|
$releaseId = (int)($_POST['release_id'] ?? 0);
|
||
|
|
$db = Database::get();
|
||
|
|
if ($db instanceof PDO && $id > 0) {
|
||
|
|
$stmt = $db->prepare("DELETE FROM ac_release_tracks WHERE id = :id");
|
||
|
|
$stmt->execute([':id' => $id]);
|
||
|
|
}
|
||
|
|
return new Response('', 302, ['Location' => '/admin/releases/tracks?release_id=' . $releaseId]);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function adminTrackUpload(): Response
|
||
|
|
{
|
||
|
|
if (!Auth::check()) {
|
||
|
|
return new Response('', 302, ['Location' => '/admin/login']);
|
||
|
|
}
|
||
|
|
if (!Auth::hasRole(['admin', 'manager'])) {
|
||
|
|
return new Response('', 302, ['Location' => '/admin']);
|
||
|
|
}
|
||
|
|
Plugins::sync();
|
||
|
|
$storePluginEnabled = Plugins::isEnabled('store');
|
||
|
|
$file = $_FILES['track_sample'] ?? null;
|
||
|
|
$trackId = (int)($_POST['track_id'] ?? ($_POST['id'] ?? 0));
|
||
|
|
$releaseId = (int)($_POST['release_id'] ?? 0);
|
||
|
|
$uploadKind = (string)($_POST['upload_kind'] ?? 'sample');
|
||
|
|
if ($uploadKind === 'full' && !$storePluginEnabled) {
|
||
|
|
return $this->trackUploadRedirect($releaseId, $trackId, 'Enable Store plugin to upload full track files.');
|
||
|
|
}
|
||
|
|
if (!$file || !isset($file['tmp_name'])) {
|
||
|
|
return $this->trackUploadRedirect($releaseId, $trackId);
|
||
|
|
}
|
||
|
|
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||
|
|
return $this->trackUploadRedirect($releaseId, $trackId, $this->uploadErrorMessage((int)$file['error']));
|
||
|
|
}
|
||
|
|
$tmp = (string)$file['tmp_name'];
|
||
|
|
if ($tmp === '' || !is_uploaded_file($tmp)) {
|
||
|
|
return $this->trackUploadRedirect($releaseId, $trackId);
|
||
|
|
}
|
||
|
|
|
||
|
|
$uploadDir = __DIR__ . '/../../uploads/media';
|
||
|
|
$trackFolder = 'tracks';
|
||
|
|
if ($uploadKind === 'full') {
|
||
|
|
$privateRoot = rtrim(Settings::get('store_private_root', '/home/audiocore.site/private_downloads'), '/');
|
||
|
|
if ($privateRoot === '') {
|
||
|
|
return $this->trackUploadRedirect($releaseId, $trackId, 'Private download root is not configured.');
|
||
|
|
}
|
||
|
|
$trackName = 'track-' . ($trackId > 0 ? (string)$trackId : date('YmdHis'));
|
||
|
|
$db = Database::get();
|
||
|
|
if ($db instanceof PDO && $trackId > 0) {
|
||
|
|
try {
|
||
|
|
$trackStmt = $db->prepare("SELECT title FROM ac_release_tracks WHERE id = :id LIMIT 1");
|
||
|
|
$trackStmt->execute([':id' => $trackId]);
|
||
|
|
$trackRow = $trackStmt->fetch(PDO::FETCH_ASSOC);
|
||
|
|
$candidate = trim((string)($trackRow['title'] ?? ''));
|
||
|
|
if ($candidate !== '') {
|
||
|
|
$trackName = $candidate;
|
||
|
|
}
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
}
|
||
|
|
}
|
||
|
|
$trackSlug = $this->slugify($trackName);
|
||
|
|
$trackFolder = 'tracks/' . $trackSlug;
|
||
|
|
$uploadDir = $privateRoot . '/' . $trackFolder;
|
||
|
|
}
|
||
|
|
if (!is_dir($uploadDir) && !mkdir($uploadDir, 0755, true)) {
|
||
|
|
return $this->trackUploadRedirect($releaseId, $trackId, 'Upload directory is not writable.');
|
||
|
|
}
|
||
|
|
if (!is_writable($uploadDir)) {
|
||
|
|
return $this->trackUploadRedirect($releaseId, $trackId, 'Upload directory is not writable.');
|
||
|
|
}
|
||
|
|
|
||
|
|
$ext = strtolower(pathinfo((string)$file['name'], PATHINFO_EXTENSION));
|
||
|
|
if ($ext !== 'mp3') {
|
||
|
|
$msg = $uploadKind === 'full' ? 'Full track must be an MP3.' : 'Sample must be an MP3.';
|
||
|
|
return $this->trackUploadRedirect($releaseId, $trackId, $msg);
|
||
|
|
}
|
||
|
|
|
||
|
|
$baseName = preg_replace('~[^a-z0-9]+~', '-', strtolower((string)$file['name'])) ?? 'sample';
|
||
|
|
$baseName = trim($baseName, '-');
|
||
|
|
$fileName = ($baseName !== '' ? $baseName : 'sample') . '-' . date('YmdHis') . '.' . $ext;
|
||
|
|
$dest = $uploadDir . '/' . $fileName;
|
||
|
|
if (!move_uploaded_file($tmp, $dest)) {
|
||
|
|
return $this->trackUploadRedirect($releaseId, $trackId, 'Upload failed.');
|
||
|
|
}
|
||
|
|
|
||
|
|
$fileUrl = '/uploads/media/' . $fileName;
|
||
|
|
if ($uploadKind === 'full') {
|
||
|
|
$fileUrl = $trackFolder . '/' . $fileName;
|
||
|
|
}
|
||
|
|
$fileType = (string)($file['type'] ?? '');
|
||
|
|
$fileSize = (int)($file['size'] ?? 0);
|
||
|
|
|
||
|
|
$db = Database::get();
|
||
|
|
if ($db instanceof PDO && $uploadKind !== 'full') {
|
||
|
|
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 ($trackId > 0 && $db instanceof PDO) {
|
||
|
|
if ($uploadKind === 'full') {
|
||
|
|
try {
|
||
|
|
$db->prepare("
|
||
|
|
UPDATE ac_store_files
|
||
|
|
SET is_active = 0
|
||
|
|
WHERE scope_type = 'track' AND scope_id = :track_id
|
||
|
|
")->execute([':track_id' => $trackId]);
|
||
|
|
|
||
|
|
$insFile = $db->prepare("
|
||
|
|
INSERT INTO ac_store_files
|
||
|
|
(scope_type, scope_id, file_url, file_name, file_size, mime_type, is_active, created_at)
|
||
|
|
VALUES ('track', :scope_id, :file_url, :file_name, :file_size, :mime_type, 1, NOW())
|
||
|
|
");
|
||
|
|
$insFile->execute([
|
||
|
|
':scope_id' => $trackId,
|
||
|
|
':file_url' => $fileUrl,
|
||
|
|
':file_name' => (string)$file['name'],
|
||
|
|
':file_size' => $fileSize > 0 ? $fileSize : null,
|
||
|
|
':mime_type' => $fileType !== '' ? $fileType : null,
|
||
|
|
]);
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
$stmt = $db->prepare("UPDATE ac_release_tracks SET sample_url = :url WHERE id = :id");
|
||
|
|
$stmt->execute([':url' => $fileUrl, ':id' => $trackId]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $this->trackUploadRedirect($releaseId, $trackId, '', $fileUrl, $uploadKind);
|
||
|
|
}
|
||
|
|
|
||
|
|
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']);
|
||
|
|
}
|
||
|
|
Plugins::sync();
|
||
|
|
$storePluginEnabled = Plugins::isEnabled('store');
|
||
|
|
$id = (int)($_POST['id'] ?? 0);
|
||
|
|
$this->ensureReleaseArtistColumn();
|
||
|
|
$title = trim((string)($_POST['title'] ?? ''));
|
||
|
|
$slug = trim((string)($_POST['slug'] ?? ''));
|
||
|
|
$artistName = trim((string)($_POST['artist_name'] ?? ''));
|
||
|
|
$description = trim((string)($_POST['description'] ?? ''));
|
||
|
|
$credits = trim((string)($_POST['credits'] ?? ''));
|
||
|
|
$catalogNo = trim((string)($_POST['catalog_no'] ?? ''));
|
||
|
|
$releaseDate = trim((string)($_POST['release_date'] ?? ''));
|
||
|
|
$coverUrl = trim((string)($_POST['cover_url'] ?? ''));
|
||
|
|
$sampleUrl = trim((string)($_POST['sample_url'] ?? ''));
|
||
|
|
$isPublished = isset($_POST['is_published']) ? 1 : 0;
|
||
|
|
$storeEnabled = isset($_POST['store_enabled']) ? 1 : 0;
|
||
|
|
$bundlePrice = trim((string)($_POST['bundle_price'] ?? ''));
|
||
|
|
$storeCurrency = strtoupper(trim((string)($_POST['store_currency'] ?? 'GBP')));
|
||
|
|
$purchaseLabel = trim((string)($_POST['purchase_label'] ?? ''));
|
||
|
|
if (!preg_match('/^[A-Z]{3}$/', $storeCurrency)) {
|
||
|
|
$storeCurrency = 'GBP';
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($title === '') {
|
||
|
|
return $this->saveError($id, 'Title is required.');
|
||
|
|
}
|
||
|
|
$slug = $slug !== '' ? $this->slugify($slug) : $this->slugify($title);
|
||
|
|
|
||
|
|
$db = Database::get();
|
||
|
|
if (!$db instanceof PDO) {
|
||
|
|
return $this->saveError($id, 'Database unavailable.');
|
||
|
|
}
|
||
|
|
|
||
|
|
$dupStmt = $id > 0
|
||
|
|
? $db->prepare("SELECT id FROM ac_releases WHERE slug = :slug AND id != :id LIMIT 1")
|
||
|
|
: $db->prepare("SELECT id FROM ac_releases 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_releases
|
||
|
|
SET title = :title, slug = :slug, artist_name = :artist_name, description = :description, credits = :credits, catalog_no = :catalog_no, release_date = :release_date,
|
||
|
|
cover_url = :cover_url, sample_url = :sample_url, is_published = :is_published
|
||
|
|
WHERE id = :id
|
||
|
|
");
|
||
|
|
$stmt->execute([
|
||
|
|
':title' => $title,
|
||
|
|
':slug' => $slug,
|
||
|
|
':artist_name' => $artistName !== '' ? $artistName : null,
|
||
|
|
':description' => $description !== '' ? $description : null,
|
||
|
|
':credits' => $credits !== '' ? $credits : null,
|
||
|
|
':catalog_no' => $catalogNo !== '' ? $catalogNo : null,
|
||
|
|
':release_date' => $releaseDate !== '' ? $releaseDate : null,
|
||
|
|
':cover_url' => $coverUrl !== '' ? $coverUrl : null,
|
||
|
|
':sample_url' => $sampleUrl !== '' ? $sampleUrl : null,
|
||
|
|
':is_published' => $isPublished,
|
||
|
|
':id' => $id,
|
||
|
|
]);
|
||
|
|
$releaseId = $id;
|
||
|
|
} else {
|
||
|
|
$stmt = $db->prepare("
|
||
|
|
INSERT INTO ac_releases (title, slug, artist_name, description, credits, catalog_no, release_date, cover_url, sample_url, is_published)
|
||
|
|
VALUES (:title, :slug, :artist_name, :description, :credits, :catalog_no, :release_date, :cover_url, :sample_url, :is_published)
|
||
|
|
");
|
||
|
|
$stmt->execute([
|
||
|
|
':title' => $title,
|
||
|
|
':slug' => $slug,
|
||
|
|
':artist_name' => $artistName !== '' ? $artistName : null,
|
||
|
|
':description' => $description !== '' ? $description : null,
|
||
|
|
':credits' => $credits !== '' ? $credits : null,
|
||
|
|
':catalog_no' => $catalogNo !== '' ? $catalogNo : null,
|
||
|
|
':release_date' => $releaseDate !== '' ? $releaseDate : null,
|
||
|
|
':cover_url' => $coverUrl !== '' ? $coverUrl : null,
|
||
|
|
':sample_url' => $sampleUrl !== '' ? $sampleUrl : null,
|
||
|
|
':is_published' => $isPublished,
|
||
|
|
]);
|
||
|
|
$releaseId = (int)$db->lastInsertId();
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($storePluginEnabled) {
|
||
|
|
try {
|
||
|
|
$storeStmt = $db->prepare("
|
||
|
|
INSERT INTO ac_store_release_products (release_id, is_enabled, bundle_price, currency, purchase_label, created_at, updated_at)
|
||
|
|
VALUES (:release_id, :is_enabled, :bundle_price, :currency, :purchase_label, NOW(), NOW())
|
||
|
|
ON DUPLICATE KEY UPDATE
|
||
|
|
is_enabled = VALUES(is_enabled),
|
||
|
|
bundle_price = VALUES(bundle_price),
|
||
|
|
currency = VALUES(currency),
|
||
|
|
purchase_label = VALUES(purchase_label),
|
||
|
|
updated_at = NOW()
|
||
|
|
");
|
||
|
|
$storeStmt->execute([
|
||
|
|
':release_id' => $releaseId,
|
||
|
|
':is_enabled' => $storeEnabled,
|
||
|
|
':bundle_price' => $bundlePrice !== '' ? (float)$bundlePrice : 0.00,
|
||
|
|
':currency' => $storeCurrency,
|
||
|
|
':purchase_label' => $purchaseLabel !== '' ? $purchaseLabel : null,
|
||
|
|
]);
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
error_log('AC releases save error: ' . $e->getMessage());
|
||
|
|
return $this->saveError($id, 'Unable to save release. Check table columns and input.');
|
||
|
|
}
|
||
|
|
|
||
|
|
return new Response('', 302, ['Location' => '/admin/releases']);
|
||
|
|
}
|
||
|
|
|
||
|
|
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_releases WHERE id = :id");
|
||
|
|
$stmt->execute([':id' => $id]);
|
||
|
|
}
|
||
|
|
return new Response('', 302, ['Location' => '/admin/releases']);
|
||
|
|
}
|
||
|
|
|
||
|
|
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']);
|
||
|
|
}
|
||
|
|
$type = (string)($_POST['upload_type'] ?? 'cover');
|
||
|
|
$file = $type === 'sample' ? ($_FILES['release_sample'] ?? null) : ($_FILES['release_cover'] ?? null);
|
||
|
|
$releaseId = (int)($_POST['release_id'] ?? 0);
|
||
|
|
if (!$file || !isset($file['tmp_name'])) {
|
||
|
|
return $this->uploadRedirect($releaseId);
|
||
|
|
}
|
||
|
|
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||
|
|
return $this->uploadRedirect($releaseId, $this->uploadErrorMessage((int)$file['error']));
|
||
|
|
}
|
||
|
|
|
||
|
|
$tmp = (string)$file['tmp_name'];
|
||
|
|
if ($tmp === '' || !is_uploaded_file($tmp)) {
|
||
|
|
return $this->uploadRedirect($releaseId);
|
||
|
|
}
|
||
|
|
|
||
|
|
$uploadDir = __DIR__ . '/../../uploads/media';
|
||
|
|
if (!is_dir($uploadDir) && !mkdir($uploadDir, 0755, true)) {
|
||
|
|
return $this->uploadRedirect($releaseId, 'Upload directory is not writable.');
|
||
|
|
}
|
||
|
|
if (!is_writable($uploadDir)) {
|
||
|
|
return $this->uploadRedirect($releaseId, 'Upload directory is not writable.');
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($type === 'sample') {
|
||
|
|
$ext = strtolower(pathinfo((string)$file['name'], PATHINFO_EXTENSION));
|
||
|
|
if ($ext !== 'mp3') {
|
||
|
|
return $this->uploadRedirect($releaseId, 'Sample must be an MP3.');
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
$info = getimagesize($tmp);
|
||
|
|
if ($info === false) {
|
||
|
|
return $this->uploadRedirect($releaseId, 'Cover 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($releaseId, 'Cover must be JPG, PNG, or WEBP.');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$baseName = preg_replace('~[^a-z0-9]+~', '-', strtolower((string)$file['name'])) ?? 'file';
|
||
|
|
$baseName = trim($baseName, '-');
|
||
|
|
$fileName = ($baseName !== '' ? $baseName : 'file') . '-' . date('YmdHis') . '.' . $ext;
|
||
|
|
$dest = $uploadDir . '/' . $fileName;
|
||
|
|
if (!move_uploaded_file($tmp, $dest)) {
|
||
|
|
return $this->uploadRedirect($releaseId, '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 ($releaseId > 0 && $db instanceof PDO) {
|
||
|
|
if ($type === 'sample') {
|
||
|
|
$stmt = $db->prepare("UPDATE ac_releases SET sample_url = :url WHERE id = :id");
|
||
|
|
} else {
|
||
|
|
$stmt = $db->prepare("UPDATE ac_releases SET cover_url = :url WHERE id = :id");
|
||
|
|
}
|
||
|
|
$stmt->execute([':url' => $fileUrl, ':id' => $releaseId]);
|
||
|
|
}
|
||
|
|
|
||
|
|
return $this->uploadRedirect($releaseId, '', $fileUrl, $type);
|
||
|
|
}
|
||
|
|
|
||
|
|
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_releases (
|
||
|
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||
|
|
title VARCHAR(200) NOT NULL,
|
||
|
|
slug VARCHAR(200) NOT NULL UNIQUE,
|
||
|
|
artist_name VARCHAR(200) NULL,
|
||
|
|
description MEDIUMTEXT NULL,
|
||
|
|
credits MEDIUMTEXT NULL,
|
||
|
|
catalog_no VARCHAR(120) NULL,
|
||
|
|
release_date DATE NULL,
|
||
|
|
cover_url VARCHAR(255) NULL,
|
||
|
|
sample_url VARCHAR(255) NULL,
|
||
|
|
is_published 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_releases ADD COLUMN sample_url VARCHAR(255) NULL");
|
||
|
|
$db->exec("ALTER TABLE ac_releases ADD COLUMN cover_url VARCHAR(255) NULL");
|
||
|
|
$db->exec("ALTER TABLE ac_releases ADD COLUMN release_date DATE NULL");
|
||
|
|
$db->exec("ALTER TABLE ac_releases ADD COLUMN description MEDIUMTEXT NULL");
|
||
|
|
$db->exec("ALTER TABLE ac_releases ADD COLUMN credits MEDIUMTEXT NULL");
|
||
|
|
$db->exec("ALTER TABLE ac_releases ADD COLUMN catalog_no VARCHAR(120) NULL");
|
||
|
|
$db->exec("ALTER TABLE ac_releases ADD COLUMN artist_name VARCHAR(200) NULL");
|
||
|
|
$db->exec("
|
||
|
|
CREATE TABLE IF NOT EXISTS ac_release_tracks (
|
||
|
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||
|
|
release_id INT UNSIGNED NOT NULL,
|
||
|
|
track_no INT NULL,
|
||
|
|
title VARCHAR(200) NOT NULL,
|
||
|
|
mix_name VARCHAR(200) NULL,
|
||
|
|
duration VARCHAR(20) NULL,
|
||
|
|
bpm INT NULL,
|
||
|
|
key_signature VARCHAR(50) NULL,
|
||
|
|
sample_url VARCHAR(255) NULL,
|
||
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
|
|
KEY idx_release_tracks_release (release_id),
|
||
|
|
CONSTRAINT fk_release_tracks_release
|
||
|
|
FOREIGN KEY (release_id) REFERENCES ac_releases(id)
|
||
|
|
ON DELETE CASCADE
|
||
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||
|
|
");
|
||
|
|
$db->exec("ALTER TABLE ac_release_tracks ADD COLUMN track_no INT NULL");
|
||
|
|
$db->exec("ALTER TABLE ac_release_tracks ADD COLUMN mix_name VARCHAR(200) NULL");
|
||
|
|
$db->exec("ALTER TABLE ac_release_tracks ADD COLUMN duration VARCHAR(20) NULL");
|
||
|
|
$db->exec("ALTER TABLE ac_release_tracks ADD COLUMN bpm INT NULL");
|
||
|
|
$db->exec("ALTER TABLE ac_release_tracks ADD COLUMN key_signature VARCHAR(50) NULL");
|
||
|
|
$db->exec("ALTER TABLE ac_release_tracks ADD COLUMN sample_url VARCHAR(255) NULL");
|
||
|
|
$db->exec("
|
||
|
|
CREATE TABLE IF NOT EXISTS ac_store_release_products (
|
||
|
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||
|
|
release_id INT UNSIGNED NOT NULL UNIQUE,
|
||
|
|
is_enabled TINYINT(1) NOT NULL DEFAULT 0,
|
||
|
|
bundle_price DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||
|
|
currency CHAR(3) NOT NULL DEFAULT 'GBP',
|
||
|
|
purchase_label VARCHAR(120) NULL,
|
||
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||
|
|
");
|
||
|
|
$db->exec("
|
||
|
|
CREATE TABLE IF NOT EXISTS ac_store_track_products (
|
||
|
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||
|
|
release_track_id INT UNSIGNED NOT NULL UNIQUE,
|
||
|
|
is_enabled TINYINT(1) NOT NULL DEFAULT 0,
|
||
|
|
track_price DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||
|
|
currency CHAR(3) NOT NULL DEFAULT 'GBP',
|
||
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||
|
|
");
|
||
|
|
$db->exec("
|
||
|
|
CREATE TABLE IF NOT EXISTS ac_store_files (
|
||
|
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||
|
|
scope_type ENUM('release','track') NOT NULL,
|
||
|
|
scope_id INT UNSIGNED NOT NULL,
|
||
|
|
file_url VARCHAR(1024) NOT NULL,
|
||
|
|
file_name VARCHAR(255) NOT NULL,
|
||
|
|
file_size BIGINT UNSIGNED NULL,
|
||
|
|
mime_type VARCHAR(128) NULL,
|
||
|
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||
|
|
");
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return new Response('', 302, ['Location' => '/admin/releases']);
|
||
|
|
}
|
||
|
|
|
||
|
|
private function ensureReleaseArtistColumn(): void
|
||
|
|
{
|
||
|
|
$db = Database::get();
|
||
|
|
if (!($db instanceof PDO)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
$probe = $db->query("SHOW COLUMNS FROM ac_releases LIKE 'artist_name'");
|
||
|
|
$exists = (bool)($probe && $probe->fetch(PDO::FETCH_ASSOC));
|
||
|
|
if (!$exists) {
|
||
|
|
$db->exec("ALTER TABLE ac_releases ADD COLUMN artist_name VARCHAR(200) NULL AFTER slug");
|
||
|
|
}
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private function releasesTableReady(): bool
|
||
|
|
{
|
||
|
|
$db = Database::get();
|
||
|
|
if (!$db instanceof PDO) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
$stmt = $db->query("SELECT 1 FROM ac_releases LIMIT 1");
|
||
|
|
return $stmt !== false;
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private function saveError(int $id, string $message): Response
|
||
|
|
{
|
||
|
|
Plugins::sync();
|
||
|
|
$storePluginEnabled = Plugins::isEnabled('store');
|
||
|
|
return new Response($this->view->render('admin/edit.php', [
|
||
|
|
'title' => $id > 0 ? 'Edit Release' : 'New Release',
|
||
|
|
'release' => [
|
||
|
|
'id' => $id,
|
||
|
|
'title' => (string)($_POST['title'] ?? ''),
|
||
|
|
'slug' => (string)($_POST['slug'] ?? ''),
|
||
|
|
'artist_name' => (string)($_POST['artist_name'] ?? ''),
|
||
|
|
'description' => (string)($_POST['description'] ?? ''),
|
||
|
|
'credits' => (string)($_POST['credits'] ?? ''),
|
||
|
|
'catalog_no' => (string)($_POST['catalog_no'] ?? ''),
|
||
|
|
'release_date' => (string)($_POST['release_date'] ?? ''),
|
||
|
|
'cover_url' => (string)($_POST['cover_url'] ?? ''),
|
||
|
|
'sample_url' => (string)($_POST['sample_url'] ?? ''),
|
||
|
|
'is_published' => isset($_POST['is_published']) ? 1 : 0,
|
||
|
|
'store_enabled' => isset($_POST['store_enabled']) ? 1 : 0,
|
||
|
|
'bundle_price' => (string)($_POST['bundle_price'] ?? ''),
|
||
|
|
'store_currency' => (string)($_POST['store_currency'] ?? 'GBP'),
|
||
|
|
'purchase_label' => (string)($_POST['purchase_label'] ?? ''),
|
||
|
|
],
|
||
|
|
'store_plugin_enabled' => $storePluginEnabled,
|
||
|
|
'error' => $message,
|
||
|
|
]));
|
||
|
|
}
|
||
|
|
|
||
|
|
private function tracksTableReady(): bool
|
||
|
|
{
|
||
|
|
$db = Database::get();
|
||
|
|
if (!$db instanceof PDO) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
$stmt = $db->query("SELECT 1 FROM ac_release_tracks LIMIT 1");
|
||
|
|
return $stmt !== false;
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private function trackSaveError(int $id, int $releaseId, string $message): Response
|
||
|
|
{
|
||
|
|
Plugins::sync();
|
||
|
|
$storePluginEnabled = Plugins::isEnabled('store');
|
||
|
|
return new Response($this->view->render('admin/track_edit.php', [
|
||
|
|
'title' => $id > 0 ? 'Edit Track' : 'New Track',
|
||
|
|
'track' => [
|
||
|
|
'id' => $id,
|
||
|
|
'release_id' => $releaseId,
|
||
|
|
'track_no' => (string)($_POST['track_no'] ?? ''),
|
||
|
|
'title' => (string)($_POST['title'] ?? ''),
|
||
|
|
'mix_name' => (string)($_POST['mix_name'] ?? ''),
|
||
|
|
'duration' => (string)($_POST['duration'] ?? ''),
|
||
|
|
'bpm' => (string)($_POST['bpm'] ?? ''),
|
||
|
|
'key_signature' => (string)($_POST['key_signature'] ?? ''),
|
||
|
|
'sample_url' => (string)($_POST['sample_url'] ?? ''),
|
||
|
|
'store_enabled' => isset($_POST['store_enabled']) ? 1 : 0,
|
||
|
|
'track_price' => (string)($_POST['track_price'] ?? ''),
|
||
|
|
'store_currency' => (string)($_POST['store_currency'] ?? 'GBP'),
|
||
|
|
'full_file_url' => (string)($_POST['full_file_url'] ?? ''),
|
||
|
|
],
|
||
|
|
'release' => null,
|
||
|
|
'store_plugin_enabled' => $storePluginEnabled,
|
||
|
|
'error' => $message,
|
||
|
|
]));
|
||
|
|
}
|
||
|
|
|
||
|
|
private function trackUploadRedirect(int $releaseId, int $trackId, string $error = '', string $url = '', string $uploadKind = 'sample'): Response
|
||
|
|
{
|
||
|
|
$target = '/admin/releases/tracks/edit?release_id=' . $releaseId;
|
||
|
|
if ($trackId > 0) {
|
||
|
|
$target .= '&id=' . $trackId;
|
||
|
|
}
|
||
|
|
if ($url !== '') {
|
||
|
|
$param = $uploadKind === 'full' ? 'full_file_url' : 'sample_url';
|
||
|
|
$target .= '&' . $param . '=' . rawurlencode($url);
|
||
|
|
}
|
||
|
|
if ($error !== '') {
|
||
|
|
$target .= '&upload_error=' . rawurlencode($error);
|
||
|
|
}
|
||
|
|
return new Response('', 302, ['Location' => $target]);
|
||
|
|
}
|
||
|
|
|
||
|
|
private function slugify(string $value): string
|
||
|
|
{
|
||
|
|
$value = strtolower(trim($value));
|
||
|
|
$value = preg_replace('~[^a-z0-9]+~', '-', $value) ?? $value;
|
||
|
|
$value = trim($value, '-');
|
||
|
|
return $value !== '' ? $value : 'release';
|
||
|
|
}
|
||
|
|
|
||
|
|
private function uploadRedirect(int $releaseId, string $error = '', string $url = '', string $type = ''): Response
|
||
|
|
{
|
||
|
|
$target = $releaseId > 0
|
||
|
|
? '/admin/releases/edit?id=' . $releaseId
|
||
|
|
: '/admin/releases/new';
|
||
|
|
|
||
|
|
$queryPrefix = strpos($target, '?') === false ? '?' : '&';
|
||
|
|
if ($url !== '') {
|
||
|
|
$param = $type === 'sample' ? 'sample_url' : 'cover_url';
|
||
|
|
$target .= $queryPrefix . $param . '=' . rawurlencode($url);
|
||
|
|
$queryPrefix = '&';
|
||
|
|
}
|
||
|
|
if ($error !== '') {
|
||
|
|
$target .= $queryPrefix . '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.';
|
||
|
|
}
|
||
|
|
}
|