2026-03-04 20:46:11 +00:00
< ? 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 {
2026-03-05 14:18:20 +00:00
$today = date ( 'Y-m-d' );
2026-03-04 20:46:11 +00:00
$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 = [];
2026-03-05 14:18:20 +00:00
$where = [ " r.is_published = 1 " , " (r.release_date IS NULL OR r.release_date <= :today) " ];
$params [ ':today' ] = $today ;
2026-03-04 20:46:11 +00:00
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
2026-03-05 14:18:20 +00:00
AND ( r . release_date IS NULL OR r . release_date <= CURDATE ())
2026-03-04 20:46:11 +00:00
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
2026-03-05 14:18:20 +00:00
AND ( release_date IS NULL OR release_date <= CURDATE ())
2026-03-04 20:46:11 +00:00
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 {
2026-03-05 14:18:20 +00:00
$stmt = $db -> prepare ( "
SELECT *
FROM ac_releases
WHERE slug = : slug
AND is_published = 1
AND ( release_date IS NULL OR release_date <= : today )
LIMIT 1
" );
$stmt -> execute ([
':slug' => $slug ,
':today' => date ( 'Y-m-d' ),
]);
2026-03-04 20:46:11 +00:00
$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.' ;
}
}