761 lines
27 KiB
PHP
761 lines
27 KiB
PHP
|
|
<?php
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
namespace Modules\Api;
|
||
|
|
|
||
|
|
use Core\Http\Response;
|
||
|
|
use Core\Services\ApiLayer;
|
||
|
|
use Core\Services\Auth;
|
||
|
|
use Core\Services\Database;
|
||
|
|
use Core\Views\View;
|
||
|
|
use PDO;
|
||
|
|
use Throwable;
|
||
|
|
|
||
|
|
class ApiController
|
||
|
|
{
|
||
|
|
private View $view;
|
||
|
|
|
||
|
|
public function __construct()
|
||
|
|
{
|
||
|
|
$this->view = new View(__DIR__ . '/views');
|
||
|
|
}
|
||
|
|
|
||
|
|
public function adminIndex(): Response
|
||
|
|
{
|
||
|
|
if (!Auth::check()) {
|
||
|
|
return new Response('', 302, ['Location' => '/admin/login']);
|
||
|
|
}
|
||
|
|
if (!Auth::hasRole(['admin'])) {
|
||
|
|
return new Response('', 302, ['Location' => '/admin']);
|
||
|
|
}
|
||
|
|
|
||
|
|
$db = Database::get();
|
||
|
|
$clients = [];
|
||
|
|
$createdKey = (string)($_SESSION['ac_api_created_key'] ?? '');
|
||
|
|
unset($_SESSION['ac_api_created_key']);
|
||
|
|
|
||
|
|
if ($db instanceof PDO) {
|
||
|
|
ApiLayer::ensureSchema($db);
|
||
|
|
try {
|
||
|
|
$stmt = $db->query("
|
||
|
|
SELECT id, name, api_key_prefix, webhook_url, is_active, last_used_at, last_used_ip, created_at
|
||
|
|
FROM ac_api_clients
|
||
|
|
ORDER BY created_at DESC, id DESC
|
||
|
|
");
|
||
|
|
$clients = $stmt ? ($stmt->fetchAll(PDO::FETCH_ASSOC) ?: []) : [];
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
$clients = [];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return new Response($this->view->render('admin/index.php', [
|
||
|
|
'title' => 'API',
|
||
|
|
'clients' => $clients,
|
||
|
|
'created_key' => $createdKey,
|
||
|
|
'status' => trim((string)($_GET['status'] ?? '')),
|
||
|
|
'message' => trim((string)($_GET['message'] ?? '')),
|
||
|
|
'base_url' => $this->baseUrl(),
|
||
|
|
]));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function adminCreateClient(): Response
|
||
|
|
{
|
||
|
|
if ($guard = $this->guardAdmin()) {
|
||
|
|
return $guard;
|
||
|
|
}
|
||
|
|
|
||
|
|
$name = trim((string)($_POST['name'] ?? ''));
|
||
|
|
$webhookUrl = trim((string)($_POST['webhook_url'] ?? ''));
|
||
|
|
if ($name === '') {
|
||
|
|
return $this->adminRedirect('error', 'Client name is required.');
|
||
|
|
}
|
||
|
|
|
||
|
|
$db = Database::get();
|
||
|
|
if (!($db instanceof PDO)) {
|
||
|
|
return $this->adminRedirect('error', 'Database unavailable.');
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
$plainKey = ApiLayer::issueClient($db, $name, $webhookUrl);
|
||
|
|
$_SESSION['ac_api_created_key'] = $plainKey;
|
||
|
|
return $this->adminRedirect('ok', 'API client created. Copy the key now.');
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
return $this->adminRedirect('error', 'Unable to create API client.');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public function adminToggleClient(): Response
|
||
|
|
{
|
||
|
|
if ($guard = $this->guardAdmin()) {
|
||
|
|
return $guard;
|
||
|
|
}
|
||
|
|
|
||
|
|
$id = (int)($_POST['id'] ?? 0);
|
||
|
|
if ($id <= 0) {
|
||
|
|
return $this->adminRedirect('error', 'Invalid client.');
|
||
|
|
}
|
||
|
|
|
||
|
|
$db = Database::get();
|
||
|
|
if (!($db instanceof PDO)) {
|
||
|
|
return $this->adminRedirect('error', 'Database unavailable.');
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
$stmt = $db->prepare("
|
||
|
|
UPDATE ac_api_clients
|
||
|
|
SET is_active = CASE WHEN is_active = 1 THEN 0 ELSE 1 END
|
||
|
|
WHERE id = :id
|
||
|
|
");
|
||
|
|
$stmt->execute([':id' => $id]);
|
||
|
|
return $this->adminRedirect('ok', 'API client updated.');
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
return $this->adminRedirect('error', 'Unable to update API client.');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public function adminDeleteClient(): Response
|
||
|
|
{
|
||
|
|
if ($guard = $this->guardAdmin()) {
|
||
|
|
return $guard;
|
||
|
|
}
|
||
|
|
|
||
|
|
$id = (int)($_POST['id'] ?? 0);
|
||
|
|
if ($id <= 0) {
|
||
|
|
return $this->adminRedirect('error', 'Invalid client.');
|
||
|
|
}
|
||
|
|
|
||
|
|
$db = Database::get();
|
||
|
|
if (!($db instanceof PDO)) {
|
||
|
|
return $this->adminRedirect('error', 'Database unavailable.');
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
$stmt = $db->prepare("DELETE FROM ac_api_clients WHERE id = :id LIMIT 1");
|
||
|
|
$stmt->execute([':id' => $id]);
|
||
|
|
return $this->adminRedirect('ok', 'API client deleted.');
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
return $this->adminRedirect('error', 'Unable to delete API client.');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public function authVerify(): Response
|
||
|
|
{
|
||
|
|
$client = $this->requireClient();
|
||
|
|
if ($client instanceof Response) {
|
||
|
|
return $client;
|
||
|
|
}
|
||
|
|
|
||
|
|
return $this->json([
|
||
|
|
'ok' => true,
|
||
|
|
'client' => [
|
||
|
|
'id' => (int)($client['id'] ?? 0),
|
||
|
|
'name' => (string)($client['name'] ?? ''),
|
||
|
|
'prefix' => (string)($client['api_key_prefix'] ?? ''),
|
||
|
|
],
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function artistSales(): Response
|
||
|
|
{
|
||
|
|
$client = $this->requireClient();
|
||
|
|
if ($client instanceof Response) {
|
||
|
|
return $client;
|
||
|
|
}
|
||
|
|
|
||
|
|
$artistId = (int)($_GET['artist_id'] ?? 0);
|
||
|
|
if ($artistId <= 0) {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'artist_id is required.'], 422);
|
||
|
|
}
|
||
|
|
|
||
|
|
$db = Database::get();
|
||
|
|
if (!($db instanceof PDO)) {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'Database unavailable.'], 500);
|
||
|
|
}
|
||
|
|
ApiLayer::ensureSchema($db);
|
||
|
|
|
||
|
|
try {
|
||
|
|
$stmt = $db->prepare("
|
||
|
|
SELECT
|
||
|
|
a.id,
|
||
|
|
a.order_id,
|
||
|
|
o.order_no,
|
||
|
|
a.order_item_id,
|
||
|
|
a.source_item_type,
|
||
|
|
a.source_item_id,
|
||
|
|
a.release_id,
|
||
|
|
a.track_id,
|
||
|
|
a.title_snapshot,
|
||
|
|
a.qty,
|
||
|
|
a.gross_amount,
|
||
|
|
a.currency_snapshot,
|
||
|
|
a.created_at,
|
||
|
|
o.email AS customer_email,
|
||
|
|
o.payment_provider,
|
||
|
|
o.payment_ref
|
||
|
|
FROM ac_store_order_item_allocations a
|
||
|
|
JOIN ac_store_orders o ON o.id = a.order_id
|
||
|
|
WHERE a.artist_id = :artist_id
|
||
|
|
AND o.status = 'paid'
|
||
|
|
ORDER BY a.id DESC
|
||
|
|
LIMIT 500
|
||
|
|
");
|
||
|
|
$stmt->execute([':artist_id' => $artistId]);
|
||
|
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
||
|
|
return $this->json([
|
||
|
|
'ok' => true,
|
||
|
|
'artist_id' => $artistId,
|
||
|
|
'count' => count($rows),
|
||
|
|
'rows' => $rows,
|
||
|
|
]);
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'Unable to load artist sales.'], 500);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public function salesSince(): Response
|
||
|
|
{
|
||
|
|
$client = $this->requireClient();
|
||
|
|
if ($client instanceof Response) {
|
||
|
|
return $client;
|
||
|
|
}
|
||
|
|
|
||
|
|
$artistId = (int)($_GET['artist_id'] ?? 0);
|
||
|
|
$afterId = (int)($_GET['after_id'] ?? 0);
|
||
|
|
$since = trim((string)($_GET['since'] ?? ''));
|
||
|
|
if ($artistId <= 0) {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'artist_id is required.'], 422);
|
||
|
|
}
|
||
|
|
if ($afterId <= 0 && $since === '') {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'after_id or since is required.'], 422);
|
||
|
|
}
|
||
|
|
|
||
|
|
$db = Database::get();
|
||
|
|
if (!($db instanceof PDO)) {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'Database unavailable.'], 500);
|
||
|
|
}
|
||
|
|
ApiLayer::ensureSchema($db);
|
||
|
|
|
||
|
|
try {
|
||
|
|
$where = [
|
||
|
|
'a.artist_id = :artist_id',
|
||
|
|
"o.status = 'paid'",
|
||
|
|
];
|
||
|
|
$params = [':artist_id' => $artistId];
|
||
|
|
if ($afterId > 0) {
|
||
|
|
$where[] = 'a.id > :after_id';
|
||
|
|
$params[':after_id'] = $afterId;
|
||
|
|
}
|
||
|
|
if ($since !== '') {
|
||
|
|
$where[] = 'a.created_at >= :since';
|
||
|
|
$params[':since'] = $since;
|
||
|
|
}
|
||
|
|
|
||
|
|
$stmt = $db->prepare("
|
||
|
|
SELECT
|
||
|
|
a.id,
|
||
|
|
a.order_id,
|
||
|
|
o.order_no,
|
||
|
|
a.order_item_id,
|
||
|
|
a.source_item_type,
|
||
|
|
a.source_item_id,
|
||
|
|
a.release_id,
|
||
|
|
a.track_id,
|
||
|
|
a.title_snapshot,
|
||
|
|
a.qty,
|
||
|
|
a.gross_amount,
|
||
|
|
a.currency_snapshot,
|
||
|
|
a.created_at
|
||
|
|
FROM ac_store_order_item_allocations a
|
||
|
|
JOIN ac_store_orders o ON o.id = a.order_id
|
||
|
|
WHERE " . implode(' AND ', $where) . "
|
||
|
|
ORDER BY a.id ASC
|
||
|
|
LIMIT 500
|
||
|
|
");
|
||
|
|
$stmt->execute($params);
|
||
|
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
||
|
|
return $this->json([
|
||
|
|
'ok' => true,
|
||
|
|
'artist_id' => $artistId,
|
||
|
|
'count' => count($rows),
|
||
|
|
'rows' => $rows,
|
||
|
|
]);
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'Unable to load incremental sales.'], 500);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public function artistTracks(): Response
|
||
|
|
{
|
||
|
|
$client = $this->requireClient();
|
||
|
|
if ($client instanceof Response) {
|
||
|
|
return $client;
|
||
|
|
}
|
||
|
|
|
||
|
|
$artistId = (int)($_GET['artist_id'] ?? 0);
|
||
|
|
if ($artistId <= 0) {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'artist_id is required.'], 422);
|
||
|
|
}
|
||
|
|
|
||
|
|
$db = Database::get();
|
||
|
|
if (!($db instanceof PDO)) {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'Database unavailable.'], 500);
|
||
|
|
}
|
||
|
|
ApiLayer::ensureSchema($db);
|
||
|
|
|
||
|
|
try {
|
||
|
|
$stmt = $db->prepare("
|
||
|
|
SELECT
|
||
|
|
t.id,
|
||
|
|
t.release_id,
|
||
|
|
r.title AS release_title,
|
||
|
|
r.slug AS release_slug,
|
||
|
|
r.catalog_no,
|
||
|
|
r.release_date,
|
||
|
|
t.track_no,
|
||
|
|
t.title,
|
||
|
|
t.mix_name,
|
||
|
|
t.duration,
|
||
|
|
t.bpm,
|
||
|
|
t.key_signature,
|
||
|
|
t.sample_url,
|
||
|
|
sp.is_enabled AS store_enabled,
|
||
|
|
sp.track_price,
|
||
|
|
sp.currency
|
||
|
|
FROM ac_release_tracks t
|
||
|
|
JOIN ac_releases r ON r.id = t.release_id
|
||
|
|
LEFT JOIN ac_store_track_products sp ON sp.release_track_id = t.id
|
||
|
|
WHERE r.artist_id = :artist_id
|
||
|
|
ORDER BY r.release_date DESC, r.id DESC, t.track_no ASC, t.id ASC
|
||
|
|
");
|
||
|
|
$stmt->execute([':artist_id' => $artistId]);
|
||
|
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
||
|
|
return $this->json([
|
||
|
|
'ok' => true,
|
||
|
|
'artist_id' => $artistId,
|
||
|
|
'count' => count($rows),
|
||
|
|
'tracks' => $rows,
|
||
|
|
]);
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'Unable to load artist tracks.'], 500);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public function submitRelease(): Response
|
||
|
|
{
|
||
|
|
$client = $this->requireClient();
|
||
|
|
if ($client instanceof Response) {
|
||
|
|
return $client;
|
||
|
|
}
|
||
|
|
|
||
|
|
$db = Database::get();
|
||
|
|
if (!($db instanceof PDO)) {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'Database unavailable.'], 500);
|
||
|
|
}
|
||
|
|
ApiLayer::ensureSchema($db);
|
||
|
|
|
||
|
|
$data = $this->requestData();
|
||
|
|
$title = trim((string)($data['title'] ?? ''));
|
||
|
|
if ($title === '') {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'title is required.'], 422);
|
||
|
|
}
|
||
|
|
|
||
|
|
$artistId = (int)($data['artist_id'] ?? 0);
|
||
|
|
$artistName = trim((string)($data['artist_name'] ?? ''));
|
||
|
|
if ($artistId > 0) {
|
||
|
|
$artistName = $this->artistNameById($db, $artistId) ?: $artistName;
|
||
|
|
}
|
||
|
|
|
||
|
|
$slug = $this->slugify((string)($data['slug'] ?? $title));
|
||
|
|
try {
|
||
|
|
$stmt = $db->prepare("
|
||
|
|
INSERT INTO ac_releases
|
||
|
|
(title, slug, artist_id, artist_name, description, credits, catalog_no, release_date, cover_url, sample_url, is_published)
|
||
|
|
VALUES (:title, :slug, :artist_id, :artist_name, :description, :credits, :catalog_no, :release_date, :cover_url, :sample_url, :is_published)
|
||
|
|
");
|
||
|
|
$stmt->execute([
|
||
|
|
':title' => $title,
|
||
|
|
':slug' => $slug,
|
||
|
|
':artist_id' => $artistId > 0 ? $artistId : null,
|
||
|
|
':artist_name' => $artistName !== '' ? $artistName : null,
|
||
|
|
':description' => $this->nullableText($data['description'] ?? null),
|
||
|
|
':credits' => $this->nullableText($data['credits'] ?? null),
|
||
|
|
':catalog_no' => $this->nullableText($data['catalog_no'] ?? null),
|
||
|
|
':release_date' => $this->nullableText($data['release_date'] ?? null),
|
||
|
|
':cover_url' => $this->nullableText($data['cover_url'] ?? null),
|
||
|
|
':sample_url' => $this->nullableText($data['sample_url'] ?? null),
|
||
|
|
':is_published' => !empty($data['is_published']) ? 1 : 0,
|
||
|
|
]);
|
||
|
|
$releaseId = (int)$db->lastInsertId();
|
||
|
|
return $this->json([
|
||
|
|
'ok' => true,
|
||
|
|
'release_id' => $releaseId,
|
||
|
|
'slug' => $slug,
|
||
|
|
], 201);
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'Unable to create release.'], 500);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public function submitTracks(): Response
|
||
|
|
{
|
||
|
|
$client = $this->requireClient();
|
||
|
|
if ($client instanceof Response) {
|
||
|
|
return $client;
|
||
|
|
}
|
||
|
|
|
||
|
|
$db = Database::get();
|
||
|
|
if (!($db instanceof PDO)) {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'Database unavailable.'], 500);
|
||
|
|
}
|
||
|
|
ApiLayer::ensureSchema($db);
|
||
|
|
|
||
|
|
$data = $this->requestData();
|
||
|
|
$releaseId = (int)($data['release_id'] ?? 0);
|
||
|
|
$tracks = is_array($data['tracks'] ?? null) ? $data['tracks'] : [];
|
||
|
|
if ($releaseId <= 0 || !$tracks) {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'release_id and tracks are required.'], 422);
|
||
|
|
}
|
||
|
|
|
||
|
|
$created = [];
|
||
|
|
try {
|
||
|
|
foreach ($tracks as $track) {
|
||
|
|
if (!is_array($track)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
$trackId = (int)($track['id'] ?? 0);
|
||
|
|
$title = trim((string)($track['title'] ?? ''));
|
||
|
|
if ($title === '') {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if ($trackId > 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 AND release_id = :release_id
|
||
|
|
");
|
||
|
|
$stmt->execute([
|
||
|
|
':track_no' => isset($track['track_no']) ? (int)$track['track_no'] : null,
|
||
|
|
':title' => $title,
|
||
|
|
':mix_name' => $this->nullableText($track['mix_name'] ?? null),
|
||
|
|
':duration' => $this->nullableText($track['duration'] ?? null),
|
||
|
|
':bpm' => isset($track['bpm']) && $track['bpm'] !== '' ? (int)$track['bpm'] : null,
|
||
|
|
':key_signature' => $this->nullableText($track['key_signature'] ?? null),
|
||
|
|
':sample_url' => $this->nullableText($track['sample_url'] ?? null),
|
||
|
|
':id' => $trackId,
|
||
|
|
':release_id' => $releaseId,
|
||
|
|
]);
|
||
|
|
$created[] = ['id' => $trackId, 'updated' => true];
|
||
|
|
} 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' => isset($track['track_no']) ? (int)$track['track_no'] : null,
|
||
|
|
':title' => $title,
|
||
|
|
':mix_name' => $this->nullableText($track['mix_name'] ?? null),
|
||
|
|
':duration' => $this->nullableText($track['duration'] ?? null),
|
||
|
|
':bpm' => isset($track['bpm']) && $track['bpm'] !== '' ? (int)$track['bpm'] : null,
|
||
|
|
':key_signature' => $this->nullableText($track['key_signature'] ?? null),
|
||
|
|
':sample_url' => $this->nullableText($track['sample_url'] ?? null),
|
||
|
|
]);
|
||
|
|
$created[] = ['id' => (int)$db->lastInsertId(), 'created' => true];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return $this->json([
|
||
|
|
'ok' => true,
|
||
|
|
'release_id' => $releaseId,
|
||
|
|
'tracks' => $created,
|
||
|
|
], 201);
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'Unable to submit tracks.'], 500);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public function updateRelease(): Response
|
||
|
|
{
|
||
|
|
$client = $this->requireClient();
|
||
|
|
if ($client instanceof Response) {
|
||
|
|
return $client;
|
||
|
|
}
|
||
|
|
|
||
|
|
$db = Database::get();
|
||
|
|
if (!($db instanceof PDO)) {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'Database unavailable.'], 500);
|
||
|
|
}
|
||
|
|
ApiLayer::ensureSchema($db);
|
||
|
|
$data = $this->requestData();
|
||
|
|
$releaseId = (int)($data['release_id'] ?? 0);
|
||
|
|
if ($releaseId <= 0) {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'release_id is required.'], 422);
|
||
|
|
}
|
||
|
|
|
||
|
|
$fields = [];
|
||
|
|
$params = [':id' => $releaseId];
|
||
|
|
$map = [
|
||
|
|
'title' => 'title',
|
||
|
|
'description' => 'description',
|
||
|
|
'credits' => 'credits',
|
||
|
|
'catalog_no' => 'catalog_no',
|
||
|
|
'release_date' => 'release_date',
|
||
|
|
'cover_url' => 'cover_url',
|
||
|
|
'sample_url' => 'sample_url',
|
||
|
|
'is_published' => 'is_published',
|
||
|
|
];
|
||
|
|
foreach ($map as $input => $column) {
|
||
|
|
if (!array_key_exists($input, $data)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
$fields[] = "{$column} = :{$input}";
|
||
|
|
$params[":{$input}"] = $input === 'is_published'
|
||
|
|
? (!empty($data[$input]) ? 1 : 0)
|
||
|
|
: $this->nullableText($data[$input]);
|
||
|
|
}
|
||
|
|
if (array_key_exists('slug', $data)) {
|
||
|
|
$fields[] = "slug = :slug";
|
||
|
|
$params[':slug'] = $this->slugify((string)$data['slug']);
|
||
|
|
}
|
||
|
|
if (array_key_exists('artist_id', $data) || array_key_exists('artist_name', $data)) {
|
||
|
|
$artistId = (int)($data['artist_id'] ?? 0);
|
||
|
|
$artistName = trim((string)($data['artist_name'] ?? ''));
|
||
|
|
if ($artistId > 0) {
|
||
|
|
$artistName = $this->artistNameById($db, $artistId) ?: $artistName;
|
||
|
|
}
|
||
|
|
$fields[] = "artist_id = :artist_id";
|
||
|
|
$fields[] = "artist_name = :artist_name";
|
||
|
|
$params[':artist_id'] = $artistId > 0 ? $artistId : null;
|
||
|
|
$params[':artist_name'] = $artistName !== '' ? $artistName : null;
|
||
|
|
}
|
||
|
|
if (!$fields) {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'No fields to update.'], 422);
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
$stmt = $db->prepare("UPDATE ac_releases SET " . implode(', ', $fields) . " WHERE id = :id");
|
||
|
|
$stmt->execute($params);
|
||
|
|
return $this->json(['ok' => true, 'release_id' => $releaseId]);
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'Unable to update release.'], 500);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public function updateTrack(): Response
|
||
|
|
{
|
||
|
|
$client = $this->requireClient();
|
||
|
|
if ($client instanceof Response) {
|
||
|
|
return $client;
|
||
|
|
}
|
||
|
|
|
||
|
|
$db = Database::get();
|
||
|
|
if (!($db instanceof PDO)) {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'Database unavailable.'], 500);
|
||
|
|
}
|
||
|
|
|
||
|
|
$data = $this->requestData();
|
||
|
|
$trackId = (int)($data['track_id'] ?? 0);
|
||
|
|
if ($trackId <= 0) {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'track_id is required.'], 422);
|
||
|
|
}
|
||
|
|
|
||
|
|
$fields = [];
|
||
|
|
$params = [':id' => $trackId];
|
||
|
|
$map = [
|
||
|
|
'track_no' => 'track_no',
|
||
|
|
'title' => 'title',
|
||
|
|
'mix_name' => 'mix_name',
|
||
|
|
'duration' => 'duration',
|
||
|
|
'bpm' => 'bpm',
|
||
|
|
'key_signature' => 'key_signature',
|
||
|
|
'sample_url' => 'sample_url',
|
||
|
|
];
|
||
|
|
foreach ($map as $input => $column) {
|
||
|
|
if (!array_key_exists($input, $data)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
$fields[] = "{$column} = :{$input}";
|
||
|
|
if ($input === 'bpm' || $input === 'track_no') {
|
||
|
|
$params[":{$input}"] = $data[$input] !== '' ? (int)$data[$input] : null;
|
||
|
|
} else {
|
||
|
|
$params[":{$input}"] = $this->nullableText($data[$input]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!$fields) {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'No fields to update.'], 422);
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
$stmt = $db->prepare("UPDATE ac_release_tracks SET " . implode(', ', $fields) . " WHERE id = :id");
|
||
|
|
$stmt->execute($params);
|
||
|
|
return $this->json(['ok' => true, 'track_id' => $trackId]);
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'Unable to update track.'], 500);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public function orderItemData(): Response
|
||
|
|
{
|
||
|
|
$client = $this->requireClient();
|
||
|
|
if ($client instanceof Response) {
|
||
|
|
return $client;
|
||
|
|
}
|
||
|
|
|
||
|
|
$db = Database::get();
|
||
|
|
if (!($db instanceof PDO)) {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'Database unavailable.'], 500);
|
||
|
|
}
|
||
|
|
ApiLayer::ensureSchema($db);
|
||
|
|
|
||
|
|
$artistId = (int)($_GET['artist_id'] ?? 0);
|
||
|
|
$orderId = (int)($_GET['order_id'] ?? 0);
|
||
|
|
$afterId = (int)($_GET['after_id'] ?? 0);
|
||
|
|
$since = trim((string)($_GET['since'] ?? ''));
|
||
|
|
|
||
|
|
try {
|
||
|
|
$where = ['1=1'];
|
||
|
|
$params = [];
|
||
|
|
if ($artistId > 0) {
|
||
|
|
$where[] = 'a.artist_id = :artist_id';
|
||
|
|
$params[':artist_id'] = $artistId;
|
||
|
|
}
|
||
|
|
if ($orderId > 0) {
|
||
|
|
$where[] = 'a.order_id = :order_id';
|
||
|
|
$params[':order_id'] = $orderId;
|
||
|
|
}
|
||
|
|
if ($afterId > 0) {
|
||
|
|
$where[] = 'a.id > :after_id';
|
||
|
|
$params[':after_id'] = $afterId;
|
||
|
|
}
|
||
|
|
if ($since !== '') {
|
||
|
|
$where[] = 'a.created_at >= :since';
|
||
|
|
$params[':since'] = $since;
|
||
|
|
}
|
||
|
|
|
||
|
|
$stmt = $db->prepare("
|
||
|
|
SELECT
|
||
|
|
a.id,
|
||
|
|
a.order_id,
|
||
|
|
o.order_no,
|
||
|
|
o.status AS order_status,
|
||
|
|
o.email AS customer_email,
|
||
|
|
a.order_item_id,
|
||
|
|
a.artist_id,
|
||
|
|
a.release_id,
|
||
|
|
a.track_id,
|
||
|
|
a.source_item_type,
|
||
|
|
a.source_item_id,
|
||
|
|
a.title_snapshot,
|
||
|
|
a.qty,
|
||
|
|
a.gross_amount,
|
||
|
|
a.currency_snapshot,
|
||
|
|
a.created_at
|
||
|
|
FROM ac_store_order_item_allocations a
|
||
|
|
JOIN ac_store_orders o ON o.id = a.order_id
|
||
|
|
WHERE " . implode(' AND ', $where) . "
|
||
|
|
ORDER BY a.id DESC
|
||
|
|
LIMIT 1000
|
||
|
|
");
|
||
|
|
$stmt->execute($params);
|
||
|
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
||
|
|
return $this->json([
|
||
|
|
'ok' => true,
|
||
|
|
'count' => count($rows),
|
||
|
|
'rows' => $rows,
|
||
|
|
]);
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'Unable to load order item data.'], 500);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private function requestData(): array
|
||
|
|
{
|
||
|
|
$contentType = strtolower(trim((string)($_SERVER['CONTENT_TYPE'] ?? '')));
|
||
|
|
if (str_contains($contentType, 'application/json')) {
|
||
|
|
$raw = file_get_contents('php://input');
|
||
|
|
$decoded = json_decode(is_string($raw) ? $raw : '', true);
|
||
|
|
return is_array($decoded) ? $decoded : [];
|
||
|
|
}
|
||
|
|
return is_array($_POST) ? $_POST : [];
|
||
|
|
}
|
||
|
|
|
||
|
|
private function requireClient(): array|Response
|
||
|
|
{
|
||
|
|
$db = Database::get();
|
||
|
|
if (!($db instanceof PDO)) {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'Database unavailable.'], 500);
|
||
|
|
}
|
||
|
|
$client = ApiLayer::verifyRequest($db);
|
||
|
|
if (!$client) {
|
||
|
|
return $this->json(['ok' => false, 'error' => 'Invalid API key.'], 401);
|
||
|
|
}
|
||
|
|
return $client;
|
||
|
|
}
|
||
|
|
|
||
|
|
private function json(array $data, int $status = 200): Response
|
||
|
|
{
|
||
|
|
$json = json_encode($data, JSON_UNESCAPED_SLASHES);
|
||
|
|
return new Response(
|
||
|
|
is_string($json) ? $json : '{"ok":false,"error":"json_encode_failed"}',
|
||
|
|
$status,
|
||
|
|
['Content-Type' => 'application/json; charset=utf-8']
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
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 nullableText(mixed $value): ?string
|
||
|
|
{
|
||
|
|
$text = trim((string)$value);
|
||
|
|
return $text !== '' ? $text : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
private function artistNameById(PDO $db, int $artistId): string
|
||
|
|
{
|
||
|
|
if ($artistId <= 0) {
|
||
|
|
return '';
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
$stmt = $db->prepare("SELECT name FROM ac_artists WHERE id = :id LIMIT 1");
|
||
|
|
$stmt->execute([':id' => $artistId]);
|
||
|
|
return trim((string)($stmt->fetchColumn() ?: ''));
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
return '';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private function adminRedirect(string $status, string $message): Response
|
||
|
|
{
|
||
|
|
return new Response('', 302, [
|
||
|
|
'Location' => '/admin/api?status=' . rawurlencode($status) . '&message=' . rawurlencode($message),
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
private function baseUrl(): string
|
||
|
|
{
|
||
|
|
$https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|
||
|
|
|| ((string)($_SERVER['SERVER_PORT'] ?? '') === '443');
|
||
|
|
$scheme = $https ? 'https' : 'http';
|
||
|
|
$host = trim((string)($_SERVER['HTTP_HOST'] ?? ''));
|
||
|
|
return $host !== '' ? ($scheme . '://' . $host) : '';
|
||
|
|
}
|
||
|
|
|
||
|
|
private function guardAdmin(): ?Response
|
||
|
|
{
|
||
|
|
if (!Auth::check()) {
|
||
|
|
return new Response('', 302, ['Location' => '/admin/login']);
|
||
|
|
}
|
||
|
|
if (!Auth::hasRole(['admin'])) {
|
||
|
|
return new Response('', 302, ['Location' => '/admin']);
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|