Improve track sample generator UI and metadata-based sample filenames
This commit is contained in:
@@ -27,8 +27,18 @@ class ReleasesController
|
|||||||
$db = Database::get();
|
$db = Database::get();
|
||||||
$page = null;
|
$page = null;
|
||||||
$releases = [];
|
$releases = [];
|
||||||
|
$topTracks = [];
|
||||||
|
$featuredArtists = [];
|
||||||
|
$labels = [];
|
||||||
$artistOptions = [];
|
$artistOptions = [];
|
||||||
|
$labelOptions = [];
|
||||||
|
$keyOptions = [];
|
||||||
$artistFilter = trim((string)($_GET['artist'] ?? ''));
|
$artistFilter = trim((string)($_GET['artist'] ?? ''));
|
||||||
|
$labelFilter = trim((string)($_GET['label'] ?? ''));
|
||||||
|
$keyFilter = trim((string)($_GET['musical_key'] ?? ''));
|
||||||
|
$bpmFilter = trim((string)($_GET['bpm'] ?? ''));
|
||||||
|
$dateFilter = trim((string)($_GET['date_range'] ?? 'all'));
|
||||||
|
$formatFilter = trim((string)($_GET['format'] ?? 'all'));
|
||||||
$search = trim((string)($_GET['q'] ?? ''));
|
$search = trim((string)($_GET['q'] ?? ''));
|
||||||
$sort = trim((string)($_GET['sort'] ?? 'newest'));
|
$sort = trim((string)($_GET['sort'] ?? 'newest'));
|
||||||
$currentPage = max(1, (int)($_GET['p'] ?? 1));
|
$currentPage = max(1, (int)($_GET['p'] ?? 1));
|
||||||
@@ -44,19 +54,29 @@ class ReleasesController
|
|||||||
if (!isset($allowedSorts[$sort])) {
|
if (!isset($allowedSorts[$sort])) {
|
||||||
$sort = 'newest';
|
$sort = 'newest';
|
||||||
}
|
}
|
||||||
|
$allowedFormats = ['all', 'digital', 'mp3', 'wav', 'flac'];
|
||||||
|
if (!in_array($formatFilter, $allowedFormats, true)) {
|
||||||
|
$formatFilter = 'all';
|
||||||
|
}
|
||||||
|
$allowedDateFilters = ['all', '7d', '30d', '90d', 'year'];
|
||||||
|
if (!in_array($dateFilter, $allowedDateFilters, true)) {
|
||||||
|
$dateFilter = 'all';
|
||||||
|
}
|
||||||
|
|
||||||
if ($db instanceof PDO) {
|
if ($db instanceof PDO) {
|
||||||
try {
|
try {
|
||||||
$today = date('Y-m-d');
|
$today = date('Y-m-d');
|
||||||
|
$releaseColumns = $this->tableColumns($db, 'ac_releases');
|
||||||
|
$trackColumns = $this->tableColumns($db, 'ac_release_tracks');
|
||||||
|
$artistJoinReady = in_array('artist_id', $releaseColumns, true);
|
||||||
|
$labelColumn = in_array('label_name', $releaseColumns, true) ? 'label_name' : (in_array('label', $releaseColumns, true) ? 'label' : '');
|
||||||
|
$hasTracks = $this->tableExists($db, 'ac_release_tracks');
|
||||||
|
$hasStoreRelease = $this->tableExists($db, 'ac_store_release_products');
|
||||||
|
$hasStoreTracks = $this->tableExists($db, 'ac_store_track_products');
|
||||||
|
|
||||||
$stmt = $db->prepare("SELECT title, content_html FROM ac_pages WHERE slug = 'releases' AND is_published = 1 LIMIT 1");
|
$stmt = $db->prepare("SELECT title, content_html FROM ac_pages WHERE slug = 'releases' AND is_published = 1 LIMIT 1");
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
$page = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
|
$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 = [];
|
$params = [];
|
||||||
$where = ["r.is_published = 1", "(r.release_date IS NULL OR r.release_date <= :today)"];
|
$where = ["r.is_published = 1", "(r.release_date IS NULL OR r.release_date <= :today)"];
|
||||||
@@ -69,10 +89,65 @@ class ReleasesController
|
|||||||
}
|
}
|
||||||
$params[':artist'] = $artistFilter;
|
$params[':artist'] = $artistFilter;
|
||||||
}
|
}
|
||||||
|
if ($labelFilter !== '' && $labelColumn !== '') {
|
||||||
|
$where[] = "r.{$labelColumn} = :label";
|
||||||
|
$params[':label'] = $labelFilter;
|
||||||
|
}
|
||||||
if ($search !== '') {
|
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" : "") . ")";
|
$searchParts = [
|
||||||
|
"r.title LIKE :search",
|
||||||
|
"r.slug LIKE :search",
|
||||||
|
"r.artist_name LIKE :search",
|
||||||
|
"r.catalog_no LIKE :search",
|
||||||
|
];
|
||||||
|
if ($labelColumn !== '') {
|
||||||
|
$searchParts[] = "r.{$labelColumn} LIKE :search";
|
||||||
|
}
|
||||||
|
if ($artistJoinReady) {
|
||||||
|
$searchParts[] = "a.name LIKE :search";
|
||||||
|
}
|
||||||
|
$where[] = '(' . implode(' OR ', $searchParts) . ')';
|
||||||
$params[':search'] = '%' . $search . '%';
|
$params[':search'] = '%' . $search . '%';
|
||||||
}
|
}
|
||||||
|
if ($dateFilter !== 'all') {
|
||||||
|
$days = 0;
|
||||||
|
if ($dateFilter === '7d') {
|
||||||
|
$days = 7;
|
||||||
|
} elseif ($dateFilter === '30d') {
|
||||||
|
$days = 30;
|
||||||
|
} elseif ($dateFilter === '90d') {
|
||||||
|
$days = 90;
|
||||||
|
} elseif ($dateFilter === 'year') {
|
||||||
|
$days = 365;
|
||||||
|
}
|
||||||
|
if ($days > 0) {
|
||||||
|
$where[] = "r.release_date >= :date_from";
|
||||||
|
$params[':date_from'] = date('Y-m-d', strtotime('-' . $days . ' days'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($hasTracks && $keyFilter !== '' && in_array('key_signature', $trackColumns, true)) {
|
||||||
|
$where[] = "EXISTS (
|
||||||
|
SELECT 1 FROM ac_release_tracks tkey
|
||||||
|
WHERE tkey.release_id = r.id AND tkey.key_signature = :key_filter
|
||||||
|
)";
|
||||||
|
$params[':key_filter'] = $keyFilter;
|
||||||
|
}
|
||||||
|
if ($hasTracks && $bpmFilter !== '' && in_array('bpm', $trackColumns, true)) {
|
||||||
|
if ($bpmFilter === '150+') {
|
||||||
|
$where[] = "EXISTS (
|
||||||
|
SELECT 1 FROM ac_release_tracks tbpm
|
||||||
|
WHERE tbpm.release_id = r.id AND tbpm.bpm >= :bpm_min
|
||||||
|
)";
|
||||||
|
$params[':bpm_min'] = 150;
|
||||||
|
} elseif (preg_match('/^(\d{2,3})-(\d{2,3})$/', $bpmFilter, $m)) {
|
||||||
|
$where[] = "EXISTS (
|
||||||
|
SELECT 1 FROM ac_release_tracks tbpm
|
||||||
|
WHERE tbpm.release_id = r.id AND tbpm.bpm BETWEEN :bpm_min AND :bpm_max
|
||||||
|
)";
|
||||||
|
$params[':bpm_min'] = (int)$m[1];
|
||||||
|
$params[':bpm_max'] = (int)$m[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($artistJoinReady) {
|
if ($artistJoinReady) {
|
||||||
$countSql = "
|
$countSql = "
|
||||||
@@ -91,6 +166,8 @@ class ReleasesController
|
|||||||
|
|
||||||
$listSql = "
|
$listSql = "
|
||||||
SELECT r.id, r.title, r.slug, r.release_date, r.cover_url,
|
SELECT r.id, r.title, r.slug, r.release_date, r.cover_url,
|
||||||
|
" . ($labelColumn !== '' ? "r.{$labelColumn} AS label_name," : "'' AS label_name,") . "
|
||||||
|
" . (in_array('sample_url', $releaseColumns, true) ? "r.sample_url," : "'' AS sample_url,") . "
|
||||||
COALESCE(r.artist_name, a.name) AS artist_name
|
COALESCE(r.artist_name, a.name) AS artist_name
|
||||||
FROM ac_releases r
|
FROM ac_releases r
|
||||||
LEFT JOIN ac_artists a ON a.id = r.artist_id
|
LEFT JOIN ac_artists a ON a.id = r.artist_id
|
||||||
@@ -102,10 +179,11 @@ class ReleasesController
|
|||||||
foreach ($params as $k => $v) {
|
foreach ($params as $k => $v) {
|
||||||
$listStmt->bindValue($k, $v);
|
$listStmt->bindValue($k, $v);
|
||||||
}
|
}
|
||||||
$listStmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
|
$listStmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
|
||||||
$listStmt->bindValue(':offset', $offset, PDO::PARAM_INT);
|
$listStmt->bindValue(':offset', $offset, PDO::PARAM_INT);
|
||||||
$listStmt->execute();
|
$listStmt->execute();
|
||||||
|
|
||||||
|
$labelSelect = $labelColumn !== '' ? "TRIM(r.{$labelColumn}) AS label_name," : "'' AS label_name,";
|
||||||
$artistStmt = $db->query("
|
$artistStmt = $db->query("
|
||||||
SELECT DISTINCT TRIM(COALESCE(NULLIF(r.artist_name, ''), a.name)) AS artist_name
|
SELECT DISTINCT TRIM(COALESCE(NULLIF(r.artist_name, ''), a.name)) AS artist_name
|
||||||
FROM ac_releases r
|
FROM ac_releases r
|
||||||
@@ -114,6 +192,14 @@ class ReleasesController
|
|||||||
AND (r.release_date IS NULL OR r.release_date <= CURDATE())
|
AND (r.release_date IS NULL OR r.release_date <= CURDATE())
|
||||||
ORDER BY artist_name ASC
|
ORDER BY artist_name ASC
|
||||||
");
|
");
|
||||||
|
$labelStmt = $db->query("
|
||||||
|
SELECT DISTINCT {$labelSelect}
|
||||||
|
1
|
||||||
|
FROM ac_releases r
|
||||||
|
WHERE r.is_published = 1
|
||||||
|
AND (r.release_date IS NULL OR r.release_date <= CURDATE())
|
||||||
|
ORDER BY label_name ASC
|
||||||
|
");
|
||||||
} else {
|
} else {
|
||||||
$countSql = "
|
$countSql = "
|
||||||
SELECT COUNT(*) AS total_rows
|
SELECT COUNT(*) AS total_rows
|
||||||
@@ -129,7 +215,10 @@ class ReleasesController
|
|||||||
$offset = ($currentPage - 1) * $perPage;
|
$offset = ($currentPage - 1) * $perPage;
|
||||||
|
|
||||||
$listSql = "
|
$listSql = "
|
||||||
SELECT r.id, r.title, r.slug, r.release_date, r.cover_url, r.artist_name
|
SELECT r.id, r.title, r.slug, r.release_date, r.cover_url,
|
||||||
|
" . ($labelColumn !== '' ? "r.{$labelColumn} AS label_name," : "'' AS label_name,") . "
|
||||||
|
" . (in_array('sample_url', $releaseColumns, true) ? "r.sample_url," : "'' AS sample_url,") . "
|
||||||
|
r.artist_name
|
||||||
FROM ac_releases r
|
FROM ac_releases r
|
||||||
WHERE " . implode(' AND ', $where) . "
|
WHERE " . implode(' AND ', $where) . "
|
||||||
ORDER BY {$allowedSorts[$sort]}
|
ORDER BY {$allowedSorts[$sort]}
|
||||||
@@ -150,6 +239,16 @@ class ReleasesController
|
|||||||
AND (release_date IS NULL OR release_date <= CURDATE())
|
AND (release_date IS NULL OR release_date <= CURDATE())
|
||||||
ORDER BY artist_name ASC
|
ORDER BY artist_name ASC
|
||||||
");
|
");
|
||||||
|
$labelStmt = false;
|
||||||
|
if ($labelColumn !== '') {
|
||||||
|
$labelStmt = $db->query("
|
||||||
|
SELECT DISTINCT TRIM({$labelColumn}) AS label_name
|
||||||
|
FROM ac_releases
|
||||||
|
WHERE is_published = 1
|
||||||
|
AND (release_date IS NULL OR release_date <= CURDATE())
|
||||||
|
ORDER BY label_name ASC
|
||||||
|
");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$releases = $listStmt->fetchAll(PDO::FETCH_ASSOC);
|
$releases = $listStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
@@ -161,6 +260,158 @@ class ReleasesController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$artistOptions = array_values(array_unique($artistOptions));
|
$artistOptions = array_values(array_unique($artistOptions));
|
||||||
|
|
||||||
|
if ($labelStmt) {
|
||||||
|
$rawLabelRows = $labelStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
foreach ($rawLabelRows as $row) {
|
||||||
|
$name = trim((string)($row['label_name'] ?? ''));
|
||||||
|
if ($name !== '') {
|
||||||
|
$labelOptions[] = $name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$labelOptions = array_values(array_unique($labelOptions));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasTracks && in_array('key_signature', $trackColumns, true)) {
|
||||||
|
$keyStmt = $db->query("
|
||||||
|
SELECT DISTINCT TRIM(key_signature) AS k
|
||||||
|
FROM ac_release_tracks
|
||||||
|
WHERE key_signature IS NOT NULL AND key_signature != ''
|
||||||
|
ORDER BY k ASC
|
||||||
|
");
|
||||||
|
$keyRows = $keyStmt ? $keyStmt->fetchAll(PDO::FETCH_ASSOC) : [];
|
||||||
|
foreach ($keyRows as $row) {
|
||||||
|
$k = trim((string)($row['k'] ?? ''));
|
||||||
|
if ($k !== '') {
|
||||||
|
$keyOptions[] = $k;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$keyOptions = array_values(array_unique($keyOptions));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasStoreRelease || $hasStoreTracks || $hasTracks) {
|
||||||
|
$bundleStmt = null;
|
||||||
|
$trackPriceStmt = null;
|
||||||
|
$sampleStmt = null;
|
||||||
|
if ($hasStoreRelease) {
|
||||||
|
$bundleStmt = $db->prepare("
|
||||||
|
SELECT is_enabled, bundle_price, currency
|
||||||
|
FROM ac_store_release_products
|
||||||
|
WHERE release_id = :rid
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
}
|
||||||
|
if ($hasStoreTracks && $hasTracks) {
|
||||||
|
$trackPriceStmt = $db->prepare("
|
||||||
|
SELECT MIN(sp.track_price) AS min_price, MIN(sp.currency) AS currency
|
||||||
|
FROM ac_release_tracks t
|
||||||
|
JOIN ac_store_track_products sp ON sp.release_track_id = t.id
|
||||||
|
WHERE t.release_id = :rid AND sp.is_enabled = 1
|
||||||
|
");
|
||||||
|
}
|
||||||
|
if ($hasTracks && in_array('sample_url', $trackColumns, true)) {
|
||||||
|
$sampleStmt = $db->prepare("
|
||||||
|
SELECT sample_url
|
||||||
|
FROM ac_release_tracks
|
||||||
|
WHERE release_id = :rid AND sample_url IS NOT NULL AND sample_url != ''
|
||||||
|
ORDER BY track_no ASC, id ASC
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
}
|
||||||
|
foreach ($releases as &$releaseRow) {
|
||||||
|
$releaseRow['store_enabled'] = 0;
|
||||||
|
$releaseRow['display_price'] = null;
|
||||||
|
$releaseRow['display_currency'] = 'GBP';
|
||||||
|
$releaseRow['sample_url'] = (string)($releaseRow['sample_url'] ?? '');
|
||||||
|
$rid = (int)($releaseRow['id'] ?? 0);
|
||||||
|
if ($rid <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($bundleStmt) {
|
||||||
|
$bundleStmt->execute([':rid' => $rid]);
|
||||||
|
$bundle = $bundleStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if ($bundle && (int)($bundle['is_enabled'] ?? 0) === 1 && (float)($bundle['bundle_price'] ?? 0) > 0) {
|
||||||
|
$releaseRow['store_enabled'] = 1;
|
||||||
|
$releaseRow['display_price'] = (float)$bundle['bundle_price'];
|
||||||
|
$releaseRow['display_currency'] = (string)($bundle['currency'] ?? 'GBP');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($releaseRow['display_price'] === null && $trackPriceStmt) {
|
||||||
|
$trackPriceStmt->execute([':rid' => $rid]);
|
||||||
|
$minRow = $trackPriceStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if ($minRow && (float)($minRow['min_price'] ?? 0) > 0) {
|
||||||
|
$releaseRow['display_price'] = (float)$minRow['min_price'];
|
||||||
|
$releaseRow['display_currency'] = (string)($minRow['currency'] ?? 'GBP');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($releaseRow['sample_url'] === '' && $sampleStmt) {
|
||||||
|
$sampleStmt->execute([':rid' => $rid]);
|
||||||
|
$sample = $sampleStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if ($sample) {
|
||||||
|
$releaseRow['sample_url'] = (string)($sample['sample_url'] ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($releaseRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasTracks) {
|
||||||
|
$topTrackSelectLabel = $labelColumn !== '' ? "COALESCE(NULLIF(r.{$labelColumn}, ''), '') AS label_name," : "'' AS label_name,";
|
||||||
|
$topTrackSql = "
|
||||||
|
SELECT
|
||||||
|
t.id,
|
||||||
|
t.track_no,
|
||||||
|
t.title,
|
||||||
|
t.mix_name,
|
||||||
|
t.duration,
|
||||||
|
t.bpm,
|
||||||
|
t.key_signature,
|
||||||
|
t.sample_url,
|
||||||
|
r.slug AS release_slug,
|
||||||
|
r.title AS release_title,
|
||||||
|
COALESCE(NULLIF(r.artist_name, ''), a.name, '') AS artist_name,
|
||||||
|
{$topTrackSelectLabel}
|
||||||
|
COALESCE(sp.track_price, 0) AS track_price,
|
||||||
|
COALESCE(sp.currency, 'GBP') AS store_currency,
|
||||||
|
COALESCE(sp.is_enabled, 0) AS store_enabled,
|
||||||
|
r.cover_url
|
||||||
|
FROM ac_release_tracks t
|
||||||
|
JOIN ac_releases r ON r.id = t.release_id
|
||||||
|
LEFT JOIN ac_artists a ON a.id = r.artist_id
|
||||||
|
LEFT JOIN ac_store_track_products sp ON sp.release_track_id = t.id
|
||||||
|
WHERE r.is_published = 1
|
||||||
|
AND (r.release_date IS NULL OR r.release_date <= :today_tracks)
|
||||||
|
ORDER BY r.release_date DESC, r.created_at DESC, t.track_no ASC, t.id ASC
|
||||||
|
LIMIT 18
|
||||||
|
";
|
||||||
|
$topStmt = $db->prepare($topTrackSql);
|
||||||
|
$topStmt->execute([':today_tracks' => $today]);
|
||||||
|
$topTracks = $topStmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->tableExists($db, 'ac_artists')) {
|
||||||
|
$artistSql = "
|
||||||
|
SELECT name, slug, avatar_url, country
|
||||||
|
FROM ac_artists
|
||||||
|
WHERE is_active = 1
|
||||||
|
ORDER BY updated_at DESC, created_at DESC
|
||||||
|
LIMIT 10
|
||||||
|
";
|
||||||
|
$artistStmt2 = $db->query($artistSql);
|
||||||
|
$featuredArtists = $artistStmt2 ? $artistStmt2->fetchAll(PDO::FETCH_ASSOC) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->tableExists($db, 'ac_labels')) {
|
||||||
|
$labelSql = "
|
||||||
|
SELECT name, slug, logo_url
|
||||||
|
FROM ac_labels
|
||||||
|
WHERE is_active = 1
|
||||||
|
ORDER BY is_primary DESC, name ASC
|
||||||
|
LIMIT 24
|
||||||
|
";
|
||||||
|
$labelStmt2 = $db->query($labelSql);
|
||||||
|
$labels = $labelStmt2 ? $labelStmt2->fetchAll(PDO::FETCH_ASSOC) : [];
|
||||||
|
}
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,8 +426,18 @@ class ReleasesController
|
|||||||
'total_pages' => $totalPages,
|
'total_pages' => $totalPages,
|
||||||
'artist_filter' => $artistFilter,
|
'artist_filter' => $artistFilter,
|
||||||
'artist_options' => $artistOptions,
|
'artist_options' => $artistOptions,
|
||||||
|
'label_filter' => $labelFilter,
|
||||||
|
'label_options' => $labelOptions,
|
||||||
|
'key_filter' => $keyFilter,
|
||||||
|
'key_options' => $keyOptions,
|
||||||
|
'bpm_filter' => $bpmFilter,
|
||||||
|
'date_filter' => $dateFilter,
|
||||||
|
'format_filter' => $formatFilter,
|
||||||
'search' => $search,
|
'search' => $search,
|
||||||
'sort' => $sort,
|
'sort' => $sort,
|
||||||
|
'top_tracks' => $topTracks,
|
||||||
|
'featured_artists' => $featuredArtists,
|
||||||
|
'labels' => $labels,
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,6 +447,7 @@ class ReleasesController
|
|||||||
$slug = trim((string)($_GET['slug'] ?? ''));
|
$slug = trim((string)($_GET['slug'] ?? ''));
|
||||||
$release = null;
|
$release = null;
|
||||||
$tracks = [];
|
$tracks = [];
|
||||||
|
$bundles = [];
|
||||||
Plugins::sync();
|
Plugins::sync();
|
||||||
$storePluginEnabled = Plugins::isEnabled('store');
|
$storePluginEnabled = Plugins::isEnabled('store');
|
||||||
if ($slug !== '') {
|
if ($slug !== '') {
|
||||||
@@ -246,6 +508,78 @@ class ReleasesController
|
|||||||
}
|
}
|
||||||
$trackStmt->execute([':rid' => (int)$release['id']]);
|
$trackStmt->execute([':rid' => (int)$release['id']]);
|
||||||
$tracks = $trackStmt->fetchAll(PDO::FETCH_ASSOC);
|
$tracks = $trackStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($storePluginEnabled) {
|
||||||
|
try {
|
||||||
|
$bundleStmt = $db->prepare("
|
||||||
|
SELECT b.id, b.name, b.slug, b.bundle_price, b.currency, b.purchase_label
|
||||||
|
FROM ac_store_bundles b
|
||||||
|
JOIN ac_store_bundle_items bi ON bi.bundle_id = b.id
|
||||||
|
WHERE bi.release_id = :release_id
|
||||||
|
AND b.is_enabled = 1
|
||||||
|
ORDER BY b.created_at DESC
|
||||||
|
LIMIT 6
|
||||||
|
");
|
||||||
|
$bundleStmt->execute([':release_id' => (int)$release['id']]);
|
||||||
|
$bundles = $bundleStmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
||||||
|
|
||||||
|
if ($bundles) {
|
||||||
|
$coverStmt = $db->prepare("
|
||||||
|
SELECT r.cover_url
|
||||||
|
FROM ac_store_bundle_items bi
|
||||||
|
JOIN ac_releases r ON r.id = bi.release_id
|
||||||
|
WHERE bi.bundle_id = :bundle_id
|
||||||
|
AND r.is_published = 1
|
||||||
|
AND (r.release_date IS NULL OR r.release_date <= :today)
|
||||||
|
ORDER BY bi.sort_order ASC, bi.id ASC
|
||||||
|
LIMIT 6
|
||||||
|
");
|
||||||
|
$sumStmt = $db->prepare("
|
||||||
|
SELECT COALESCE(SUM(CASE WHEN srp.is_enabled = 1 THEN srp.bundle_price ELSE 0 END), 0) AS regular_total
|
||||||
|
FROM ac_store_bundle_items bi
|
||||||
|
LEFT JOIN ac_store_release_products srp ON srp.release_id = bi.release_id
|
||||||
|
WHERE bi.bundle_id = :bundle_id
|
||||||
|
");
|
||||||
|
$countStmt = $db->prepare("
|
||||||
|
SELECT COUNT(*) AS release_count
|
||||||
|
FROM ac_store_bundle_items
|
||||||
|
WHERE bundle_id = :bundle_id
|
||||||
|
");
|
||||||
|
|
||||||
|
foreach ($bundles as &$bundleRow) {
|
||||||
|
$bundleId = (int)($bundleRow['id'] ?? 0);
|
||||||
|
if ($bundleId <= 0) {
|
||||||
|
$bundleRow['covers'] = [];
|
||||||
|
$bundleRow['release_count'] = 0;
|
||||||
|
$bundleRow['regular_total'] = 0.0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$coverStmt->execute([
|
||||||
|
':bundle_id' => $bundleId,
|
||||||
|
':today' => date('Y-m-d'),
|
||||||
|
]);
|
||||||
|
$coverRows = $coverStmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
||||||
|
$covers = [];
|
||||||
|
foreach ($coverRows as $c) {
|
||||||
|
$url = trim((string)($c['cover_url'] ?? ''));
|
||||||
|
if ($url !== '') {
|
||||||
|
$covers[] = $url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$bundleRow['covers'] = $covers;
|
||||||
|
|
||||||
|
$sumStmt->execute([':bundle_id' => $bundleId]);
|
||||||
|
$bundleRow['regular_total'] = (float)($sumStmt->fetchColumn() ?: 0);
|
||||||
|
|
||||||
|
$countStmt->execute([':bundle_id' => $bundleId]);
|
||||||
|
$bundleRow['release_count'] = (int)($countStmt->fetchColumn() ?: 0);
|
||||||
|
}
|
||||||
|
unset($bundleRow);
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$bundles = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
$tracks = [];
|
$tracks = [];
|
||||||
@@ -256,6 +590,7 @@ class ReleasesController
|
|||||||
'title' => $release ? (string)$release['title'] : 'Release',
|
'title' => $release ? (string)$release['title'] : 'Release',
|
||||||
'release' => $release,
|
'release' => $release,
|
||||||
'tracks' => $tracks,
|
'tracks' => $tracks,
|
||||||
|
'bundles' => $bundles,
|
||||||
'store_plugin_enabled' => $storePluginEnabled,
|
'store_plugin_enabled' => $storePluginEnabled,
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
@@ -528,7 +863,7 @@ class ReleasesController
|
|||||||
$duration = trim((string)($_POST['duration'] ?? ''));
|
$duration = trim((string)($_POST['duration'] ?? ''));
|
||||||
$bpm = trim((string)($_POST['bpm'] ?? ''));
|
$bpm = trim((string)($_POST['bpm'] ?? ''));
|
||||||
$keySig = trim((string)($_POST['key_signature'] ?? ''));
|
$keySig = trim((string)($_POST['key_signature'] ?? ''));
|
||||||
$sampleUrl = trim((string)($_POST['sample_url'] ?? ''));
|
$sampleUrl = isset($_POST['sample_url']) ? trim((string)$_POST['sample_url']) : null;
|
||||||
$storeEnabled = isset($_POST['store_enabled']) ? 1 : 0;
|
$storeEnabled = isset($_POST['store_enabled']) ? 1 : 0;
|
||||||
$trackPrice = trim((string)($_POST['track_price'] ?? ''));
|
$trackPrice = trim((string)($_POST['track_price'] ?? ''));
|
||||||
$storeCurrency = strtoupper(trim((string)($_POST['store_currency'] ?? 'GBP')));
|
$storeCurrency = strtoupper(trim((string)($_POST['store_currency'] ?? 'GBP')));
|
||||||
@@ -670,18 +1005,36 @@ class ReleasesController
|
|||||||
$trackId = (int)($_POST['track_id'] ?? ($_POST['id'] ?? 0));
|
$trackId = (int)($_POST['track_id'] ?? ($_POST['id'] ?? 0));
|
||||||
$releaseId = (int)($_POST['release_id'] ?? 0);
|
$releaseId = (int)($_POST['release_id'] ?? 0);
|
||||||
$uploadKind = (string)($_POST['upload_kind'] ?? 'sample');
|
$uploadKind = (string)($_POST['upload_kind'] ?? 'sample');
|
||||||
|
if ($trackId <= 0) {
|
||||||
|
return $this->trackUploadRedirect($releaseId, $trackId, 'Save the track first, then upload audio.');
|
||||||
|
}
|
||||||
if ($uploadKind === 'full' && !$storePluginEnabled) {
|
if ($uploadKind === 'full' && !$storePluginEnabled) {
|
||||||
return $this->trackUploadRedirect($releaseId, $trackId, 'Enable Store plugin to upload full track files.');
|
return $this->trackUploadRedirect($releaseId, $trackId, 'Enable Store plugin to upload full track files.');
|
||||||
}
|
}
|
||||||
if (!$file || !isset($file['tmp_name'])) {
|
if (!$file || !is_array($file)) {
|
||||||
return $this->trackUploadRedirect($releaseId, $trackId);
|
return $this->trackUploadRedirect(
|
||||||
|
$releaseId,
|
||||||
|
$trackId,
|
||||||
|
$this->trackUploadDebugMessage('No file payload received.', null)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!array_key_exists('tmp_name', $file)) {
|
||||||
|
return $this->trackUploadRedirect(
|
||||||
|
$releaseId,
|
||||||
|
$trackId,
|
||||||
|
$this->trackUploadDebugMessage('Upload payload missing tmp_name.', $file)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if ($file['error'] !== UPLOAD_ERR_OK) {
|
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||||
return $this->trackUploadRedirect($releaseId, $trackId, $this->uploadErrorMessage((int)$file['error']));
|
return $this->trackUploadRedirect($releaseId, $trackId, $this->uploadErrorMessage((int)$file['error']));
|
||||||
}
|
}
|
||||||
$tmp = (string)$file['tmp_name'];
|
$tmp = (string)$file['tmp_name'];
|
||||||
if ($tmp === '' || !is_uploaded_file($tmp)) {
|
if ($tmp === '' || !is_uploaded_file($tmp)) {
|
||||||
return $this->trackUploadRedirect($releaseId, $trackId);
|
return $this->trackUploadRedirect(
|
||||||
|
$releaseId,
|
||||||
|
$trackId,
|
||||||
|
$this->trackUploadDebugMessage('Uploaded temp file not detected by PHP.', $file)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$uploadDir = __DIR__ . '/../../uploads/media';
|
$uploadDir = __DIR__ . '/../../uploads/media';
|
||||||
@@ -717,10 +1070,16 @@ class ReleasesController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$ext = strtolower(pathinfo((string)$file['name'], PATHINFO_EXTENSION));
|
$ext = strtolower(pathinfo((string)$file['name'], PATHINFO_EXTENSION));
|
||||||
if ($ext !== 'mp3') {
|
$mime = strtolower((string)($file['type'] ?? ''));
|
||||||
|
$mp3ByExt = $ext === 'mp3';
|
||||||
|
$mp3ByMime = in_array($mime, ['audio/mpeg', 'audio/mp3', 'audio/x-mp3', 'audio/mpg', 'audio/x-mpeg'], true);
|
||||||
|
if (!$mp3ByExt && !$mp3ByMime) {
|
||||||
$msg = $uploadKind === 'full' ? 'Full track must be an MP3.' : 'Sample must be an MP3.';
|
$msg = $uploadKind === 'full' ? 'Full track must be an MP3.' : 'Sample must be an MP3.';
|
||||||
return $this->trackUploadRedirect($releaseId, $trackId, $msg);
|
return $this->trackUploadRedirect($releaseId, $trackId, $msg);
|
||||||
}
|
}
|
||||||
|
if ($ext === '') {
|
||||||
|
$ext = 'mp3';
|
||||||
|
}
|
||||||
|
|
||||||
$baseName = preg_replace('~[^a-z0-9]+~', '-', strtolower((string)$file['name'])) ?? 'sample';
|
$baseName = preg_replace('~[^a-z0-9]+~', '-', strtolower((string)$file['name'])) ?? 'sample';
|
||||||
$baseName = trim($baseName, '-');
|
$baseName = trim($baseName, '-');
|
||||||
@@ -734,7 +1093,7 @@ class ReleasesController
|
|||||||
if ($uploadKind === 'full') {
|
if ($uploadKind === 'full') {
|
||||||
$fileUrl = $trackFolder . '/' . $fileName;
|
$fileUrl = $trackFolder . '/' . $fileName;
|
||||||
}
|
}
|
||||||
$fileType = (string)($file['type'] ?? '');
|
$fileType = (string)($file['type'] ?? 'audio/mpeg');
|
||||||
$fileSize = (int)($file['size'] ?? 0);
|
$fileSize = (int)($file['size'] ?? 0);
|
||||||
|
|
||||||
$db = Database::get();
|
$db = Database::get();
|
||||||
@@ -777,6 +1136,15 @@ class ReleasesController
|
|||||||
]);
|
]);
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$generatedSampleUrl = $this->generateTrackSampleFromFullUpload($dest, $trackId);
|
||||||
|
if ($generatedSampleUrl !== '') {
|
||||||
|
try {
|
||||||
|
$stmt = $db->prepare("UPDATE ac_release_tracks SET sample_url = :url WHERE id = :id");
|
||||||
|
$stmt->execute([':url' => $generatedSampleUrl, ':id' => $trackId]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$stmt = $db->prepare("UPDATE ac_release_tracks SET sample_url = :url WHERE id = :id");
|
$stmt = $db->prepare("UPDATE ac_release_tracks SET sample_url = :url WHERE id = :id");
|
||||||
$stmt->execute([':url' => $fileUrl, ':id' => $trackId]);
|
$stmt->execute([':url' => $fileUrl, ':id' => $trackId]);
|
||||||
@@ -786,6 +1154,121 @@ class ReleasesController
|
|||||||
return $this->trackUploadRedirect($releaseId, $trackId, '', $fileUrl, $uploadKind);
|
return $this->trackUploadRedirect($releaseId, $trackId, '', $fileUrl, $uploadKind);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function adminTrackGenerateSample(): Response
|
||||||
|
{
|
||||||
|
if (!Auth::check()) {
|
||||||
|
return new Response('', 302, ['Location' => '/admin/login']);
|
||||||
|
}
|
||||||
|
if (!Auth::hasRole(['admin', 'manager'])) {
|
||||||
|
return new Response('', 302, ['Location' => '/admin']);
|
||||||
|
}
|
||||||
|
$trackId = (int)($_POST['track_id'] ?? 0);
|
||||||
|
$releaseId = (int)($_POST['release_id'] ?? 0);
|
||||||
|
$start = (int)($_POST['sample_start'] ?? 0);
|
||||||
|
$duration = (int)($_POST['sample_duration'] ?? 90);
|
||||||
|
$duration = max(10, min(90, $duration));
|
||||||
|
$start = max(0, $start);
|
||||||
|
|
||||||
|
if ($trackId <= 0) {
|
||||||
|
return $this->trackUploadRedirect($releaseId, $trackId, 'Save the track first, then generate sample.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = Database::get();
|
||||||
|
if (!($db instanceof PDO)) {
|
||||||
|
return $this->trackUploadRedirect($releaseId, $trackId, 'Database unavailable.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$fullPath = $this->resolveTrackFullFilePath($db, $trackId);
|
||||||
|
if ($fullPath === '' || !is_file($fullPath)) {
|
||||||
|
return $this->trackUploadRedirect($releaseId, $trackId, 'No full track file found. Upload full track first.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$sampleUrl = $this->generateTrackSampleFromFullUpload($fullPath, $trackId, $start, $duration);
|
||||||
|
if ($sampleUrl === '') {
|
||||||
|
return $this->trackUploadRedirect($releaseId, $trackId, 'Sample generation failed. Check ffmpeg on server.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $db->prepare("UPDATE ac_release_tracks SET sample_url = :url WHERE id = :id");
|
||||||
|
$stmt->execute([':url' => $sampleUrl, ':id' => $trackId]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
return $this->trackUploadRedirect($releaseId, $trackId, 'Sample generated but DB update failed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response('', 302, [
|
||||||
|
'Location' => '/admin/releases/tracks/edit?release_id=' . $releaseId . '&id=' . $trackId
|
||||||
|
. '&sample_url=' . rawurlencode($sampleUrl)
|
||||||
|
. '&upload_error=' . rawurlencode('Sample generated successfully.'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function adminTrackSource(): Response
|
||||||
|
{
|
||||||
|
if (!Auth::check() || !Auth::hasRole(['admin', 'manager'])) {
|
||||||
|
return new Response('', 403);
|
||||||
|
}
|
||||||
|
$trackId = (int)($_GET['track_id'] ?? 0);
|
||||||
|
if ($trackId <= 0) {
|
||||||
|
return new Response('', 404);
|
||||||
|
}
|
||||||
|
$db = Database::get();
|
||||||
|
if (!($db instanceof PDO)) {
|
||||||
|
return new Response('', 500);
|
||||||
|
}
|
||||||
|
$fullPath = $this->resolveTrackFullFilePath($db, $trackId);
|
||||||
|
if ($fullPath === '' || !is_file($fullPath)) {
|
||||||
|
return new Response('', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$size = filesize($fullPath);
|
||||||
|
if ($size === false) {
|
||||||
|
return new Response('', 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: audio/mpeg');
|
||||||
|
header('Accept-Ranges: bytes');
|
||||||
|
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||||
|
|
||||||
|
$range = (string)($_SERVER['HTTP_RANGE'] ?? '');
|
||||||
|
if ($range !== '' && preg_match('/bytes=(\d*)-(\d*)/', $range, $m)) {
|
||||||
|
$start = $m[1] === '' ? 0 : (int)$m[1];
|
||||||
|
$end = $m[2] === '' ? ($size - 1) : (int)$m[2];
|
||||||
|
$start = max(0, min($start, $size - 1));
|
||||||
|
$end = max($start, min($end, $size - 1));
|
||||||
|
$length = $end - $start + 1;
|
||||||
|
|
||||||
|
header('HTTP/1.1 206 Partial Content');
|
||||||
|
header('Content-Length: ' . $length);
|
||||||
|
header("Content-Range: bytes {$start}-{$end}/{$size}");
|
||||||
|
|
||||||
|
$fp = fopen($fullPath, 'rb');
|
||||||
|
if ($fp === false) {
|
||||||
|
return new Response('', 500);
|
||||||
|
}
|
||||||
|
fseek($fp, $start);
|
||||||
|
$buffer = 8192;
|
||||||
|
$remaining = $length;
|
||||||
|
while ($remaining > 0 && !feof($fp)) {
|
||||||
|
$read = ($remaining > $buffer) ? $buffer : $remaining;
|
||||||
|
$chunk = fread($fp, $read);
|
||||||
|
if ($chunk === false) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
echo $chunk;
|
||||||
|
$remaining -= strlen($chunk);
|
||||||
|
if (connection_aborted()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose($fp);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Length: ' . $size);
|
||||||
|
readfile($fullPath);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
public function adminSave(): Response
|
public function adminSave(): Response
|
||||||
{
|
{
|
||||||
if (!Auth::check()) {
|
if (!Auth::check()) {
|
||||||
@@ -825,6 +1308,18 @@ class ReleasesController
|
|||||||
if (!$db instanceof PDO) {
|
if (!$db instanceof PDO) {
|
||||||
return $this->saveError($id, 'Database unavailable.');
|
return $this->saveError($id, 'Database unavailable.');
|
||||||
}
|
}
|
||||||
|
$existingSampleUrl = null;
|
||||||
|
if ($sampleUrl === null && $id > 0) {
|
||||||
|
try {
|
||||||
|
$sampleStmt = $db->prepare("SELECT sample_url FROM ac_releases WHERE id = :id LIMIT 1");
|
||||||
|
$sampleStmt->execute([':id' => $id]);
|
||||||
|
$sampleRow = $sampleStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if ($sampleRow) {
|
||||||
|
$existingSampleUrl = (string)($sampleRow['sample_url'] ?? '');
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$dupStmt = $id > 0
|
$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 AND id != :id LIMIT 1")
|
||||||
@@ -852,7 +1347,9 @@ class ReleasesController
|
|||||||
':catalog_no' => $catalogNo !== '' ? $catalogNo : null,
|
':catalog_no' => $catalogNo !== '' ? $catalogNo : null,
|
||||||
':release_date' => $releaseDate !== '' ? $releaseDate : null,
|
':release_date' => $releaseDate !== '' ? $releaseDate : null,
|
||||||
':cover_url' => $coverUrl !== '' ? $coverUrl : null,
|
':cover_url' => $coverUrl !== '' ? $coverUrl : null,
|
||||||
':sample_url' => $sampleUrl !== '' ? $sampleUrl : null,
|
':sample_url' => $sampleUrl !== null
|
||||||
|
? ($sampleUrl !== '' ? $sampleUrl : null)
|
||||||
|
: (($existingSampleUrl ?? '') !== '' ? $existingSampleUrl : null),
|
||||||
':is_published' => $isPublished,
|
':is_published' => $isPublished,
|
||||||
':id' => $id,
|
':id' => $id,
|
||||||
]);
|
]);
|
||||||
@@ -871,7 +1368,7 @@ class ReleasesController
|
|||||||
':catalog_no' => $catalogNo !== '' ? $catalogNo : null,
|
':catalog_no' => $catalogNo !== '' ? $catalogNo : null,
|
||||||
':release_date' => $releaseDate !== '' ? $releaseDate : null,
|
':release_date' => $releaseDate !== '' ? $releaseDate : null,
|
||||||
':cover_url' => $coverUrl !== '' ? $coverUrl : null,
|
':cover_url' => $coverUrl !== '' ? $coverUrl : null,
|
||||||
':sample_url' => $sampleUrl !== '' ? $sampleUrl : null,
|
':sample_url' => $sampleUrl !== null && $sampleUrl !== '' ? $sampleUrl : null,
|
||||||
':is_published' => $isPublished,
|
':is_published' => $isPublished,
|
||||||
]);
|
]);
|
||||||
$releaseId = (int)$db->lastInsertId();
|
$releaseId = (int)$db->lastInsertId();
|
||||||
@@ -1133,6 +1630,37 @@ class ReleasesController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
private function tableColumns(PDO $db, string $table): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$stmt = $db->query("SHOW COLUMNS FROM {$table}");
|
||||||
|
$rows = $stmt ? $stmt->fetchAll(PDO::FETCH_ASSOC) : [];
|
||||||
|
$columns = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$field = strtolower(trim((string)($row['Field'] ?? '')));
|
||||||
|
if ($field !== '') {
|
||||||
|
$columns[] = $field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $columns;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function tableExists(PDO $db, string $table): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$stmt = $db->query("SHOW TABLES LIKE " . $db->quote($table));
|
||||||
|
return (bool)($stmt && $stmt->fetch(PDO::FETCH_NUM));
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function releasesTableReady(): bool
|
private function releasesTableReady(): bool
|
||||||
{
|
{
|
||||||
$db = Database::get();
|
$db = Database::get();
|
||||||
@@ -1232,6 +1760,24 @@ class ReleasesController
|
|||||||
return new Response('', 302, ['Location' => $target]);
|
return new Response('', 302, ['Location' => $target]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function trackUploadDebugMessage(string $reason, ?array $file): string
|
||||||
|
{
|
||||||
|
$code = $file !== null && isset($file['error']) ? (int)$file['error'] : -1;
|
||||||
|
$name = $file !== null ? (string)($file['name'] ?? '') : '';
|
||||||
|
$size = $file !== null ? (int)($file['size'] ?? 0) : 0;
|
||||||
|
$maxUpload = (string)ini_get('upload_max_filesize');
|
||||||
|
$maxPost = (string)ini_get('post_max_size');
|
||||||
|
$contentLength = (string)($_SERVER['CONTENT_LENGTH'] ?? '0');
|
||||||
|
return $reason
|
||||||
|
. ' (file="' . $name
|
||||||
|
. '", size=' . $size
|
||||||
|
. ', code=' . $code
|
||||||
|
. ', content_length=' . $contentLength
|
||||||
|
. ', upload_max_filesize=' . $maxUpload
|
||||||
|
. ', post_max_size=' . $maxPost
|
||||||
|
. ')';
|
||||||
|
}
|
||||||
|
|
||||||
private function slugify(string $value): string
|
private function slugify(string $value): string
|
||||||
{
|
{
|
||||||
$value = strtolower(trim($value));
|
$value = strtolower(trim($value));
|
||||||
@@ -1272,4 +1818,149 @@ class ReleasesController
|
|||||||
];
|
];
|
||||||
return $map[$code] ?? 'Upload failed.';
|
return $map[$code] ?? 'Upload failed.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function generateTrackSampleFromFullUpload(string $fullPath, int $trackId, int $startSeconds = 45, int $durationSeconds = 90): string
|
||||||
|
{
|
||||||
|
if ($trackId <= 0 || $fullPath === '' || !is_file($fullPath)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (!function_exists('shell_exec')) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$ffmpeg = $this->resolveFfmpegBinary();
|
||||||
|
if ($ffmpeg === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$sampleDir = __DIR__ . '/../../uploads/media/samples';
|
||||||
|
if (!is_dir($sampleDir) && !@mkdir($sampleDir, 0755, true)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (!is_writable($sampleDir)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$sampleBase = 'track-' . $trackId . '-sample';
|
||||||
|
$db = Database::get();
|
||||||
|
if ($db instanceof PDO) {
|
||||||
|
try {
|
||||||
|
$releaseCols = $this->tableColumns($db, 'ac_releases');
|
||||||
|
$trackCols = $this->tableColumns($db, 'ac_release_tracks');
|
||||||
|
$artistExpr = in_array('artist_name', $releaseCols, true)
|
||||||
|
? "COALESCE(NULLIF(r.artist_name, ''), '')"
|
||||||
|
: "''";
|
||||||
|
$catalogExpr = in_array('catalog_no', $releaseCols, true)
|
||||||
|
? "COALESCE(NULLIF(r.catalog_no, ''), '')"
|
||||||
|
: "''";
|
||||||
|
$titleExpr = in_array('title', $trackCols, true)
|
||||||
|
? "COALESCE(NULLIF(t.title, ''), '')"
|
||||||
|
: "''";
|
||||||
|
|
||||||
|
$metaStmt = $db->prepare("
|
||||||
|
SELECT
|
||||||
|
{$artistExpr} AS artist_name,
|
||||||
|
{$catalogExpr} AS catalog_no,
|
||||||
|
{$titleExpr} AS track_title
|
||||||
|
FROM ac_release_tracks t
|
||||||
|
JOIN ac_releases r ON r.id = t.release_id
|
||||||
|
WHERE t.id = :id
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
$metaStmt->execute([':id' => $trackId]);
|
||||||
|
$meta = $metaStmt->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||||
|
$parts = [];
|
||||||
|
$artist = trim((string)($meta['artist_name'] ?? ''));
|
||||||
|
$catalog = trim((string)($meta['catalog_no'] ?? ''));
|
||||||
|
$title = trim((string)($meta['track_title'] ?? ''));
|
||||||
|
if ($artist !== '') {
|
||||||
|
$parts[] = $this->slugify($artist);
|
||||||
|
}
|
||||||
|
if ($catalog !== '') {
|
||||||
|
$parts[] = $this->slugify($catalog);
|
||||||
|
}
|
||||||
|
if ($title !== '') {
|
||||||
|
$parts[] = $this->slugify($title);
|
||||||
|
}
|
||||||
|
if ($parts) {
|
||||||
|
$sampleBase = implode('-', $parts) . '-sample';
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$sampleName = $sampleBase . '-' . date('YmdHis') . '.mp3';
|
||||||
|
$samplePath = $sampleDir . '/' . $sampleName;
|
||||||
|
|
||||||
|
$startSeconds = max(0, $startSeconds);
|
||||||
|
$durationSeconds = max(10, min(90, $durationSeconds));
|
||||||
|
|
||||||
|
$cmd = escapeshellarg($ffmpeg)
|
||||||
|
. ' -hide_banner -loglevel error -y'
|
||||||
|
. ' -ss ' . (int)$startSeconds . ' -t ' . (int)$durationSeconds
|
||||||
|
. ' -i ' . escapeshellarg($fullPath)
|
||||||
|
. ' -codec:a libmp3lame -b:a 192k '
|
||||||
|
. escapeshellarg($samplePath)
|
||||||
|
. ' 2>&1';
|
||||||
|
$out = shell_exec($cmd);
|
||||||
|
|
||||||
|
if (!is_file($samplePath) || filesize($samplePath) <= 0) {
|
||||||
|
if (is_file($samplePath)) {
|
||||||
|
@unlink($samplePath);
|
||||||
|
}
|
||||||
|
if ($out) {
|
||||||
|
error_log('AC sample generation failed: ' . trim((string)$out));
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '/uploads/media/samples/' . $sampleName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveTrackFullFilePath(PDO $db, int $trackId): string
|
||||||
|
{
|
||||||
|
if ($trackId <= 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$stmt = $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
|
||||||
|
");
|
||||||
|
$stmt->execute([':track_id' => $trackId]);
|
||||||
|
$fileUrl = trim((string)($stmt->fetchColumn() ?: ''));
|
||||||
|
if ($fileUrl === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($fileUrl, '/uploads/')) {
|
||||||
|
return __DIR__ . '/../../' . ltrim($fileUrl, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
$privateRoot = rtrim((string)Settings::get('store_private_root', '/home/audiocore.site/private_downloads'), '/');
|
||||||
|
if ($privateRoot === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return $privateRoot . '/' . ltrim($fileUrl, '/');
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveFfmpegBinary(): string
|
||||||
|
{
|
||||||
|
$candidates = ['ffmpeg', '/usr/bin/ffmpeg', '/usr/local/bin/ffmpeg'];
|
||||||
|
foreach ($candidates as $bin) {
|
||||||
|
$cmd = 'command -v ' . escapeshellarg($bin) . ' 2>/dev/null';
|
||||||
|
$found = trim((string)shell_exec($cmd));
|
||||||
|
if ($found !== '') {
|
||||||
|
return $found;
|
||||||
|
}
|
||||||
|
if ($bin !== '' && $bin[0] === '/' && is_executable($bin)) {
|
||||||
|
return $bin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ $release = $release ?? null;
|
|||||||
$error = $error ?? '';
|
$error = $error ?? '';
|
||||||
$uploadError = (string)($_GET['upload_error'] ?? '');
|
$uploadError = (string)($_GET['upload_error'] ?? '');
|
||||||
$storePluginEnabled = (bool)($store_plugin_enabled ?? false);
|
$storePluginEnabled = (bool)($store_plugin_enabled ?? false);
|
||||||
|
$trackId = (int)($track['id'] ?? 0);
|
||||||
|
$fullSourceUrl = $trackId > 0 ? '/admin/releases/tracks/source?track_id=' . $trackId : '';
|
||||||
ob_start();
|
ob_start();
|
||||||
?>
|
?>
|
||||||
<section class="admin-card">
|
<section class="admin-card">
|
||||||
@@ -53,7 +55,7 @@ ob_start();
|
|||||||
<div style="font-size:13px; color:var(--text);">or click to upload</div>
|
<div style="font-size:13px; color:var(--text);">or click to upload</div>
|
||||||
<div id="trackSampleFileName" style="font-size:11px; color:var(--muted);">No file selected</div>
|
<div id="trackSampleFileName" style="font-size:11px; color:var(--muted);">No file selected</div>
|
||||||
</label>
|
</label>
|
||||||
<input class="input" type="file" id="trackSampleFile" name="track_sample" accept="audio/mpeg" style="display:none;">
|
<input class="input" type="file" id="trackSampleFile" name="track_sample" accept=".mp3,audio/mpeg,audio/mp3" style="display:none;">
|
||||||
<div style="margin-top:10px; display:flex; justify-content:flex-end;">
|
<div style="margin-top:10px; display:flex; justify-content:flex-end;">
|
||||||
<button type="submit" class="btn small" formaction="/admin/releases/tracks/upload" formmethod="post" formenctype="multipart/form-data" name="upload_kind" value="sample">Upload</button>
|
<button type="submit" class="btn small" formaction="/admin/releases/tracks/upload" formmethod="post" formenctype="multipart/form-data" name="upload_kind" value="sample">Upload</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,12 +72,35 @@ ob_start();
|
|||||||
<div style="font-size:13px; color:var(--text);">or click to upload</div>
|
<div style="font-size:13px; color:var(--text);">or click to upload</div>
|
||||||
<div id="trackFullFileName" style="font-size:11px; color:var(--muted);">No file selected</div>
|
<div id="trackFullFileName" style="font-size:11px; color:var(--muted);">No file selected</div>
|
||||||
</label>
|
</label>
|
||||||
<input class="input" type="file" id="trackFullFile" name="track_sample" accept="audio/mpeg" style="display:none;">
|
<input class="input" type="file" id="trackFullFile" name="track_sample" accept=".mp3,audio/mpeg,audio/mp3" style="display:none;">
|
||||||
<div style="margin-top:10px; display:flex; justify-content:flex-end;">
|
<div style="margin-top:10px; display:flex; justify-content:flex-end;">
|
||||||
<button type="submit" class="btn small" formaction="/admin/releases/tracks/upload" formmethod="post" formenctype="multipart/form-data" name="upload_kind" value="full">Upload Full</button>
|
<button type="submit" class="btn small" formaction="/admin/releases/tracks/upload" formmethod="post" formenctype="multipart/form-data" name="upload_kind" value="full">Upload Full</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php if ($trackId > 0 && (string)($track['full_file_url'] ?? '') !== ''): ?>
|
||||||
|
<div class="admin-card" style="padding:14px; background: rgba(10,10,12,0.6);">
|
||||||
|
<div class="label">Generate sample from full track</div>
|
||||||
|
<div id="trackWaveform" style="margin-top:8px; height:96px; border-radius:12px; border:1px solid rgba(255,255,255,.12); background:rgba(0,0,0,.25);"></div>
|
||||||
|
<div style="display:grid; gap:10px; margin-top:10px;">
|
||||||
|
<input type="hidden" id="sampleStart" name="sample_start" value="0">
|
||||||
|
<input type="hidden" id="sampleDuration" name="sample_duration" value="90">
|
||||||
|
<div style="display:flex; flex-wrap:wrap; align-items:center; gap:10px;">
|
||||||
|
<button type="button" class="btn outline small" id="previewSampleRegionBtn">Preview</button>
|
||||||
|
<div class="input" style="display:flex; align-items:center; gap:10px; min-height:42px; width:clamp(240px,42vw,440px);">
|
||||||
|
<span style="font-size:11px; letter-spacing:.18em; text-transform:uppercase; color:var(--muted);">Range</span>
|
||||||
|
<span id="sampleRangeText" style="font-family:'IBM Plex Mono',monospace; color:var(--text);">0s -> 90s</span>
|
||||||
|
<span id="sampleDurationText" style="margin-left:auto; font-family:'IBM Plex Mono',monospace; color:var(--muted);">90s</span>
|
||||||
|
</div>
|
||||||
|
<div style="margin-left:auto; display:flex; gap:10px;">
|
||||||
|
<button type="button" class="btn outline small" id="resetSampleRegionBtn">Reset range</button>
|
||||||
|
<button type="submit" class="btn small" formaction="/admin/releases/tracks/sample/generate" formmethod="post">Generate sample</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<label class="label">Full File URL (Store Download)</label>
|
<label class="label">Full File URL (Store Download)</label>
|
||||||
<input class="input" name="full_file_url" value="<?= htmlspecialchars((string)($track['full_file_url'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" placeholder="/uploads/media/track-full.mp3">
|
<input class="input" name="full_file_url" value="<?= htmlspecialchars((string)($track['full_file_url'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" placeholder="/uploads/media/track-full.mp3">
|
||||||
|
|
||||||
@@ -105,6 +130,8 @@ ob_start();
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
<script src="https://unpkg.com/wavesurfer.js@7/dist/wavesurfer.min.js"></script>
|
||||||
|
<script src="https://unpkg.com/wavesurfer.js@7/dist/plugins/regions.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
const drop = document.getElementById('trackSampleDropzone');
|
const drop = document.getElementById('trackSampleDropzone');
|
||||||
@@ -154,6 +181,113 @@ ob_start();
|
|||||||
fullName.textContent = fullFile.files.length ? fullFile.files[0].name : 'No file selected';
|
fullName.textContent = fullFile.files.length ? fullFile.files[0].name : 'No file selected';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const waveWrap = document.getElementById('trackWaveform');
|
||||||
|
const previewRegionBtn = document.getElementById('previewSampleRegionBtn');
|
||||||
|
const startInput = document.getElementById('sampleStart');
|
||||||
|
const durationInput = document.getElementById('sampleDuration');
|
||||||
|
const rangeText = document.getElementById('sampleRangeText');
|
||||||
|
const durationText = document.getElementById('sampleDurationText');
|
||||||
|
const resetRegionBtn = document.getElementById('resetSampleRegionBtn');
|
||||||
|
if (waveWrap && startInput && durationInput && window.WaveSurfer) {
|
||||||
|
let fixedDuration = 90;
|
||||||
|
let regionRef = null;
|
||||||
|
let isPreviewPlaying = false;
|
||||||
|
const ws = WaveSurfer.create({
|
||||||
|
container: waveWrap,
|
||||||
|
waveColor: 'rgba(175,190,220,0.35)',
|
||||||
|
progressColor: '#22f2a5',
|
||||||
|
cursorColor: '#22f2a5',
|
||||||
|
barWidth: 2,
|
||||||
|
barGap: 2,
|
||||||
|
height: 92,
|
||||||
|
});
|
||||||
|
const regions = ws.registerPlugin(window.WaveSurfer.Regions.create());
|
||||||
|
ws.load('<?= htmlspecialchars($fullSourceUrl, ENT_QUOTES, 'UTF-8') ?>');
|
||||||
|
|
||||||
|
function syncRange(startSec) {
|
||||||
|
const start = Math.max(0, Math.floor(startSec));
|
||||||
|
const end = Math.max(start, Math.floor(start + fixedDuration));
|
||||||
|
startInput.value = String(start);
|
||||||
|
durationInput.value = String(fixedDuration);
|
||||||
|
if (rangeText) rangeText.textContent = start + 's → ' + end + 's';
|
||||||
|
if (durationText) durationText.textContent = fixedDuration + 's';
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.on('ready', () => {
|
||||||
|
const total = Math.floor(ws.getDuration() || 0);
|
||||||
|
fixedDuration = Math.max(10, Math.min(90, total > 0 ? total : 90));
|
||||||
|
syncRange(0);
|
||||||
|
|
||||||
|
regionRef = regions.addRegion({
|
||||||
|
start: 0,
|
||||||
|
end: fixedDuration,
|
||||||
|
drag: true,
|
||||||
|
resize: false,
|
||||||
|
color: 'rgba(34,242,165,0.20)',
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateRegion = () => {
|
||||||
|
if (!regionRef) return;
|
||||||
|
let start = regionRef.start || 0;
|
||||||
|
const maxStart = Math.max(0, (ws.getDuration() || fixedDuration) - fixedDuration);
|
||||||
|
if (start < 0) start = 0;
|
||||||
|
if (start > maxStart) start = maxStart;
|
||||||
|
regionRef.setOptions({ start: start, end: start + fixedDuration });
|
||||||
|
syncRange(start);
|
||||||
|
};
|
||||||
|
|
||||||
|
regionRef.on('update-end', updateRegion);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('interaction', () => {});
|
||||||
|
|
||||||
|
ws.on('audioprocess', () => {
|
||||||
|
if (!isPreviewPlaying || !regionRef) return;
|
||||||
|
const t = ws.getCurrentTime() || 0;
|
||||||
|
if (t >= regionRef.end) {
|
||||||
|
ws.pause();
|
||||||
|
ws.setTime(regionRef.start);
|
||||||
|
isPreviewPlaying = false;
|
||||||
|
if (previewRegionBtn) previewRegionBtn.textContent = 'Preview';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('pause', () => {
|
||||||
|
if (isPreviewPlaying) {
|
||||||
|
isPreviewPlaying = false;
|
||||||
|
if (previewRegionBtn) previewRegionBtn.textContent = 'Preview';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resetRegionBtn) {
|
||||||
|
resetRegionBtn.addEventListener('click', () => {
|
||||||
|
if (!regionRef) return;
|
||||||
|
regionRef.setOptions({ start: 0, end: fixedDuration });
|
||||||
|
syncRange(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previewRegionBtn) {
|
||||||
|
previewRegionBtn.addEventListener('click', () => {
|
||||||
|
if (!regionRef) return;
|
||||||
|
if (isPreviewPlaying) {
|
||||||
|
ws.pause();
|
||||||
|
isPreviewPlaying = false;
|
||||||
|
previewRegionBtn.textContent = 'Preview';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ws.setTime(regionRef.start);
|
||||||
|
ws.play();
|
||||||
|
isPreviewPlaying = true;
|
||||||
|
previewRegionBtn.textContent = 'Stop preview';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (resetRegionBtn) {
|
||||||
|
resetRegionBtn.addEventListener('click', () => {
|
||||||
|
if (startInput) startInput.value = '0';
|
||||||
|
});
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<?php
|
<?php
|
||||||
|
|||||||
Reference in New Issue
Block a user