You've already forked wp-fedistream
feat: Initial release v0.1.0
WP FediStream - Stream music over ActivityPub Features: - Custom post types: Artist, Album, Track, Playlist - Custom taxonomies: Genre, Mood, License - User roles: Artist, Label - Admin dashboard with statistics - Frontend templates and shortcodes - Audio player with queue management - ActivityPub integration with actor support - WooCommerce product types for albums/tracks - User library with favorites and history - Notification system (in-app and email) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
202
includes/PostTypes/AbstractPostType.php
Normal file
202
includes/PostTypes/AbstractPostType.php
Normal file
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
/**
|
||||
* Abstract base class for custom post types.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\PostTypes;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract post type class.
|
||||
*
|
||||
* Provides common functionality for all custom post types.
|
||||
*/
|
||||
abstract class AbstractPostType {
|
||||
|
||||
/**
|
||||
* Post type key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $post_type;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'init', array( $this, 'register' ) );
|
||||
add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ) );
|
||||
add_action( 'save_post_' . $this->post_type, array( $this, 'save_meta' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the post type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function register(): void;
|
||||
|
||||
/**
|
||||
* Add meta boxes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function add_meta_boxes(): void;
|
||||
|
||||
/**
|
||||
* Save post meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
abstract public function save_meta( int $post_id, \WP_Post $post ): void;
|
||||
|
||||
/**
|
||||
* Get the post type key.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_post_type(): string {
|
||||
return $this->post_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify nonce and user capabilities before saving.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $nonce_action Nonce action name.
|
||||
* @param string $nonce_name Nonce field name.
|
||||
* @return bool Whether save should proceed.
|
||||
*/
|
||||
protected function can_save( int $post_id, string $nonce_action, string $nonce_name ): bool {
|
||||
// Verify nonce.
|
||||
if ( ! isset( $_POST[ $nonce_name ] ) || ! wp_verify_nonce( sanitize_key( $_POST[ $nonce_name ] ), $nonce_action ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check autosave.
|
||||
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check permissions.
|
||||
if ( ! current_user_can( 'edit_post', $post_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and save a text meta field.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param string $post_key POST array key.
|
||||
* @return void
|
||||
*/
|
||||
protected function save_text_meta( int $post_id, string $meta_key, string $post_key ): void {
|
||||
if ( isset( $_POST[ $post_key ] ) ) {
|
||||
update_post_meta( $post_id, $meta_key, sanitize_text_field( wp_unslash( $_POST[ $post_key ] ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and save a textarea meta field.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param string $post_key POST array key.
|
||||
* @return void
|
||||
*/
|
||||
protected function save_textarea_meta( int $post_id, string $meta_key, string $post_key ): void {
|
||||
if ( isset( $_POST[ $post_key ] ) ) {
|
||||
update_post_meta( $post_id, $meta_key, sanitize_textarea_field( wp_unslash( $_POST[ $post_key ] ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and save an integer meta field.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param string $post_key POST array key.
|
||||
* @return void
|
||||
*/
|
||||
protected function save_int_meta( int $post_id, string $meta_key, string $post_key ): void {
|
||||
if ( isset( $_POST[ $post_key ] ) ) {
|
||||
update_post_meta( $post_id, $meta_key, absint( $_POST[ $post_key ] ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and save a URL meta field.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param string $post_key POST array key.
|
||||
* @return void
|
||||
*/
|
||||
protected function save_url_meta( int $post_id, string $meta_key, string $post_key ): void {
|
||||
if ( isset( $_POST[ $post_key ] ) ) {
|
||||
update_post_meta( $post_id, $meta_key, esc_url_raw( wp_unslash( $_POST[ $post_key ] ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and save a boolean meta field.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param string $post_key POST array key.
|
||||
* @return void
|
||||
*/
|
||||
protected function save_bool_meta( int $post_id, string $meta_key, string $post_key ): void {
|
||||
$value = isset( $_POST[ $post_key ] ) ? 1 : 0;
|
||||
update_post_meta( $post_id, $meta_key, $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and save an array meta field.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param string $post_key POST array key.
|
||||
* @return void
|
||||
*/
|
||||
protected function save_array_meta( int $post_id, string $meta_key, string $post_key ): void {
|
||||
if ( isset( $_POST[ $post_key ] ) && is_array( $_POST[ $post_key ] ) ) {
|
||||
$values = array_map( 'sanitize_text_field', wp_unslash( $_POST[ $post_key ] ) );
|
||||
update_post_meta( $post_id, $meta_key, $values );
|
||||
} else {
|
||||
delete_post_meta( $post_id, $meta_key );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and save a date meta field.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param string $post_key POST array key.
|
||||
* @return void
|
||||
*/
|
||||
protected function save_date_meta( int $post_id, string $meta_key, string $post_key ): void {
|
||||
if ( isset( $_POST[ $post_key ] ) && ! empty( $_POST[ $post_key ] ) ) {
|
||||
$date = sanitize_text_field( wp_unslash( $_POST[ $post_key ] ) );
|
||||
// Validate date format (YYYY-MM-DD).
|
||||
if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) {
|
||||
update_post_meta( $post_id, $meta_key, $date );
|
||||
}
|
||||
} else {
|
||||
delete_post_meta( $post_id, $meta_key );
|
||||
}
|
||||
}
|
||||
}
|
||||
340
includes/PostTypes/Album.php
Normal file
340
includes/PostTypes/Album.php
Normal file
@@ -0,0 +1,340 @@
|
||||
<?php
|
||||
/**
|
||||
* Album custom post type.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\PostTypes;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Album post type class.
|
||||
*
|
||||
* Handles registration and management of albums/releases.
|
||||
*/
|
||||
class Album extends AbstractPostType {
|
||||
|
||||
/**
|
||||
* Post type key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $post_type = 'fedistream_album';
|
||||
|
||||
/**
|
||||
* Meta key prefix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private const META_PREFIX = '_fedistream_album_';
|
||||
|
||||
/**
|
||||
* Register the post type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void {
|
||||
$labels = array(
|
||||
'name' => _x( 'Albums', 'Post type general name', 'wp-fedistream' ),
|
||||
'singular_name' => _x( 'Album', 'Post type singular name', 'wp-fedistream' ),
|
||||
'menu_name' => _x( 'Albums', 'Admin Menu text', 'wp-fedistream' ),
|
||||
'name_admin_bar' => _x( 'Album', 'Add New on Toolbar', 'wp-fedistream' ),
|
||||
'add_new' => __( 'Add New', 'wp-fedistream' ),
|
||||
'add_new_item' => __( 'Add New Album', 'wp-fedistream' ),
|
||||
'new_item' => __( 'New Album', 'wp-fedistream' ),
|
||||
'edit_item' => __( 'Edit Album', 'wp-fedistream' ),
|
||||
'view_item' => __( 'View Album', 'wp-fedistream' ),
|
||||
'all_items' => __( 'All Albums', 'wp-fedistream' ),
|
||||
'search_items' => __( 'Search Albums', 'wp-fedistream' ),
|
||||
'parent_item_colon' => __( 'Parent Albums:', 'wp-fedistream' ),
|
||||
'not_found' => __( 'No albums found.', 'wp-fedistream' ),
|
||||
'not_found_in_trash' => __( 'No albums found in Trash.', 'wp-fedistream' ),
|
||||
'featured_image' => _x( 'Album Artwork', 'Overrides the "Featured Image" phrase', 'wp-fedistream' ),
|
||||
'set_featured_image' => _x( 'Set album artwork', 'Overrides the "Set featured image" phrase', 'wp-fedistream' ),
|
||||
'remove_featured_image' => _x( 'Remove album artwork', 'Overrides the "Remove featured image" phrase', 'wp-fedistream' ),
|
||||
'use_featured_image' => _x( 'Use as album artwork', 'Overrides the "Use as featured image" phrase', 'wp-fedistream' ),
|
||||
'archives' => _x( 'Album archives', 'The post type archive label', 'wp-fedistream' ),
|
||||
'insert_into_item' => _x( 'Insert into album', 'Overrides the "Insert into post" phrase', 'wp-fedistream' ),
|
||||
'uploaded_to_this_item' => _x( 'Uploaded to this album', 'Overrides the "Uploaded to this post" phrase', 'wp-fedistream' ),
|
||||
'filter_items_list' => _x( 'Filter albums list', 'Screen reader text', 'wp-fedistream' ),
|
||||
'items_list_navigation' => _x( 'Albums list navigation', 'Screen reader text', 'wp-fedistream' ),
|
||||
'items_list' => _x( 'Albums list', 'Screen reader text', 'wp-fedistream' ),
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'labels' => $labels,
|
||||
'public' => true,
|
||||
'publicly_queryable' => true,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => false, // Will be added to custom menu.
|
||||
'query_var' => true,
|
||||
'rewrite' => array( 'slug' => 'albums' ),
|
||||
'capability_type' => array( 'fedistream_album', 'fedistream_albums' ),
|
||||
'map_meta_cap' => true,
|
||||
'has_archive' => true,
|
||||
'hierarchical' => false,
|
||||
'menu_position' => null,
|
||||
'menu_icon' => 'dashicons-album',
|
||||
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt', 'revisions' ),
|
||||
'show_in_rest' => true,
|
||||
'rest_base' => 'albums',
|
||||
);
|
||||
|
||||
register_post_type( $this->post_type, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add meta boxes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_meta_boxes(): void {
|
||||
add_meta_box(
|
||||
'fedistream_album_info',
|
||||
__( 'Album Information', 'wp-fedistream' ),
|
||||
array( $this, 'render_info_meta_box' ),
|
||||
$this->post_type,
|
||||
'normal',
|
||||
'high'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_album_artist',
|
||||
__( 'Artist', 'wp-fedistream' ),
|
||||
array( $this, 'render_artist_meta_box' ),
|
||||
$this->post_type,
|
||||
'side',
|
||||
'high'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_album_codes',
|
||||
__( 'Album Codes', 'wp-fedistream' ),
|
||||
array( $this, 'render_codes_meta_box' ),
|
||||
$this->post_type,
|
||||
'side',
|
||||
'default'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render album info meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_info_meta_box( \WP_Post $post ): void {
|
||||
wp_nonce_field( 'fedistream_album_save', 'fedistream_album_nonce' );
|
||||
|
||||
$album_type = get_post_meta( $post->ID, self::META_PREFIX . 'type', true );
|
||||
$release_date = get_post_meta( $post->ID, self::META_PREFIX . 'release_date', true );
|
||||
$total_tracks = get_post_meta( $post->ID, self::META_PREFIX . 'total_tracks', true );
|
||||
$total_duration = get_post_meta( $post->ID, self::META_PREFIX . 'total_duration', true );
|
||||
?>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_album_type"><?php esc_html_e( 'Release Type', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select name="fedistream_album_type" id="fedistream_album_type">
|
||||
<option value="album" <?php selected( $album_type, 'album' ); ?>><?php esc_html_e( 'Album', 'wp-fedistream' ); ?></option>
|
||||
<option value="ep" <?php selected( $album_type, 'ep' ); ?>><?php esc_html_e( 'EP', 'wp-fedistream' ); ?></option>
|
||||
<option value="single" <?php selected( $album_type, 'single' ); ?>><?php esc_html_e( 'Single', 'wp-fedistream' ); ?></option>
|
||||
<option value="compilation" <?php selected( $album_type, 'compilation' ); ?>><?php esc_html_e( 'Compilation', 'wp-fedistream' ); ?></option>
|
||||
<option value="live" <?php selected( $album_type, 'live' ); ?>><?php esc_html_e( 'Live Album', 'wp-fedistream' ); ?></option>
|
||||
<option value="remix" <?php selected( $album_type, 'remix' ); ?>><?php esc_html_e( 'Remix Album', 'wp-fedistream' ); ?></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_album_release_date"><?php esc_html_e( 'Release Date', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="date" name="fedistream_album_release_date" id="fedistream_album_release_date" value="<?php echo esc_attr( $release_date ); ?>" class="regular-text">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_album_total_tracks"><?php esc_html_e( 'Total Tracks', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="fedistream_album_total_tracks" id="fedistream_album_total_tracks" value="<?php echo esc_attr( $total_tracks ); ?>" min="1" max="999" class="small-text">
|
||||
<p class="description"><?php esc_html_e( 'Auto-calculated when tracks are added.', 'wp-fedistream' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_album_total_duration"><?php esc_html_e( 'Total Duration', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="fedistream_album_total_duration" id="fedistream_album_total_duration" value="<?php echo esc_attr( $total_duration ); ?>" min="0" class="small-text" readonly>
|
||||
<span class="description"><?php esc_html_e( 'seconds (auto-calculated)', 'wp-fedistream' ); ?></span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render artist selection meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_artist_meta_box( \WP_Post $post ): void {
|
||||
$selected_artist = get_post_meta( $post->ID, self::META_PREFIX . 'artist', true );
|
||||
|
||||
$artists = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_artist',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
?>
|
||||
<p>
|
||||
<label for="fedistream_album_artist"><?php esc_html_e( 'Primary Artist', 'wp-fedistream' ); ?></label>
|
||||
</p>
|
||||
<select name="fedistream_album_artist" id="fedistream_album_artist" class="widefat">
|
||||
<option value=""><?php esc_html_e( '— Select Artist —', 'wp-fedistream' ); ?></option>
|
||||
<?php foreach ( $artists as $artist ) : ?>
|
||||
<option value="<?php echo esc_attr( $artist->ID ); ?>" <?php selected( $selected_artist, $artist->ID ); ?>>
|
||||
<?php echo esc_html( $artist->post_title ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p class="description">
|
||||
<?php
|
||||
if ( empty( $artists ) ) {
|
||||
printf(
|
||||
/* translators: %s: URL to add new artist */
|
||||
esc_html__( 'No artists found. %s', 'wp-fedistream' ),
|
||||
'<a href="' . esc_url( admin_url( 'post-new.php?post_type=fedistream_artist' ) ) . '">' . esc_html__( 'Add an artist first.', 'wp-fedistream' ) . '</a>'
|
||||
);
|
||||
}
|
||||
?>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render album codes meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_codes_meta_box( \WP_Post $post ): void {
|
||||
$upc = get_post_meta( $post->ID, self::META_PREFIX . 'upc', true );
|
||||
$catalog_number = get_post_meta( $post->ID, self::META_PREFIX . 'catalog_number', true );
|
||||
?>
|
||||
<p>
|
||||
<label for="fedistream_album_upc"><?php esc_html_e( 'UPC/EAN', 'wp-fedistream' ); ?></label>
|
||||
<input type="text" name="fedistream_album_upc" id="fedistream_album_upc" value="<?php echo esc_attr( $upc ); ?>" class="widefat" pattern="[0-9]{12,13}" title="<?php esc_attr_e( '12-13 digit UPC/EAN code', 'wp-fedistream' ); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="fedistream_album_catalog_number"><?php esc_html_e( 'Catalog Number', 'wp-fedistream' ); ?></label>
|
||||
<input type="text" name="fedistream_album_catalog_number" id="fedistream_album_catalog_number" value="<?php echo esc_attr( $catalog_number ); ?>" class="widefat">
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save post meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function save_meta( int $post_id, \WP_Post $post ): void {
|
||||
if ( ! $this->can_save( $post_id, 'fedistream_album_save', 'fedistream_album_nonce' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save album type.
|
||||
if ( isset( $_POST['fedistream_album_type'] ) ) {
|
||||
$allowed_types = array( 'album', 'ep', 'single', 'compilation', 'live', 'remix' );
|
||||
$type = sanitize_text_field( wp_unslash( $_POST['fedistream_album_type'] ) );
|
||||
if ( in_array( $type, $allowed_types, true ) ) {
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'type', $type );
|
||||
}
|
||||
}
|
||||
|
||||
// Save other fields.
|
||||
$this->save_date_meta( $post_id, self::META_PREFIX . 'release_date', 'fedistream_album_release_date' );
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'artist', 'fedistream_album_artist' );
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'total_tracks', 'fedistream_album_total_tracks' );
|
||||
$this->save_text_meta( $post_id, self::META_PREFIX . 'upc', 'fedistream_album_upc' );
|
||||
$this->save_text_meta( $post_id, self::META_PREFIX . 'catalog_number', 'fedistream_album_catalog_number' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get album by ID with meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return array|null Album data or null.
|
||||
*/
|
||||
public static function get_album( int $post_id ): ?array {
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post || 'fedistream_album' !== $post->post_type ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$artist_id = get_post_meta( $post_id, self::META_PREFIX . 'artist', true );
|
||||
|
||||
return array(
|
||||
'id' => $post->ID,
|
||||
'title' => $post->post_title,
|
||||
'slug' => $post->post_name,
|
||||
'description' => $post->post_content,
|
||||
'excerpt' => $post->post_excerpt,
|
||||
'type' => get_post_meta( $post_id, self::META_PREFIX . 'type', true ) ?: 'album',
|
||||
'release_date' => get_post_meta( $post_id, self::META_PREFIX . 'release_date', true ),
|
||||
'artist_id' => $artist_id,
|
||||
'artist_name' => $artist_id ? get_the_title( $artist_id ) : '',
|
||||
'total_tracks' => (int) get_post_meta( $post_id, self::META_PREFIX . 'total_tracks', true ),
|
||||
'total_duration' => (int) get_post_meta( $post_id, self::META_PREFIX . 'total_duration', true ),
|
||||
'upc' => get_post_meta( $post_id, self::META_PREFIX . 'upc', true ),
|
||||
'catalog_number' => get_post_meta( $post_id, self::META_PREFIX . 'catalog_number', true ),
|
||||
'artwork' => get_the_post_thumbnail_url( $post_id, 'large' ),
|
||||
'url' => get_permalink( $post_id ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update album track count and duration.
|
||||
*
|
||||
* @param int $album_id Album post ID.
|
||||
* @return void
|
||||
*/
|
||||
public static function update_album_stats( int $album_id ): void {
|
||||
$tracks = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_track',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
'meta_key' => '_fedistream_track_album',
|
||||
'meta_value' => $album_id,
|
||||
)
|
||||
);
|
||||
|
||||
$total_tracks = count( $tracks );
|
||||
$total_duration = 0;
|
||||
|
||||
foreach ( $tracks as $track ) {
|
||||
$duration = (int) get_post_meta( $track->ID, '_fedistream_track_duration', true );
|
||||
$total_duration += $duration;
|
||||
}
|
||||
|
||||
update_post_meta( $album_id, self::META_PREFIX . 'total_tracks', $total_tracks );
|
||||
update_post_meta( $album_id, self::META_PREFIX . 'total_duration', $total_duration );
|
||||
}
|
||||
}
|
||||
331
includes/PostTypes/Artist.php
Normal file
331
includes/PostTypes/Artist.php
Normal file
@@ -0,0 +1,331 @@
|
||||
<?php
|
||||
/**
|
||||
* Artist custom post type.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\PostTypes;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Artist post type class.
|
||||
*
|
||||
* Handles registration and management of artist/band profiles.
|
||||
*/
|
||||
class Artist extends AbstractPostType {
|
||||
|
||||
/**
|
||||
* Post type key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $post_type = 'fedistream_artist';
|
||||
|
||||
/**
|
||||
* Meta key prefix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private const META_PREFIX = '_fedistream_artist_';
|
||||
|
||||
/**
|
||||
* Register the post type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void {
|
||||
$labels = array(
|
||||
'name' => _x( 'Artists', 'Post type general name', 'wp-fedistream' ),
|
||||
'singular_name' => _x( 'Artist', 'Post type singular name', 'wp-fedistream' ),
|
||||
'menu_name' => _x( 'Artists', 'Admin Menu text', 'wp-fedistream' ),
|
||||
'name_admin_bar' => _x( 'Artist', 'Add New on Toolbar', 'wp-fedistream' ),
|
||||
'add_new' => __( 'Add New', 'wp-fedistream' ),
|
||||
'add_new_item' => __( 'Add New Artist', 'wp-fedistream' ),
|
||||
'new_item' => __( 'New Artist', 'wp-fedistream' ),
|
||||
'edit_item' => __( 'Edit Artist', 'wp-fedistream' ),
|
||||
'view_item' => __( 'View Artist', 'wp-fedistream' ),
|
||||
'all_items' => __( 'All Artists', 'wp-fedistream' ),
|
||||
'search_items' => __( 'Search Artists', 'wp-fedistream' ),
|
||||
'parent_item_colon' => __( 'Parent Artists:', 'wp-fedistream' ),
|
||||
'not_found' => __( 'No artists found.', 'wp-fedistream' ),
|
||||
'not_found_in_trash' => __( 'No artists found in Trash.', 'wp-fedistream' ),
|
||||
'featured_image' => _x( 'Artist Photo', 'Overrides the "Featured Image" phrase', 'wp-fedistream' ),
|
||||
'set_featured_image' => _x( 'Set artist photo', 'Overrides the "Set featured image" phrase', 'wp-fedistream' ),
|
||||
'remove_featured_image' => _x( 'Remove artist photo', 'Overrides the "Remove featured image" phrase', 'wp-fedistream' ),
|
||||
'use_featured_image' => _x( 'Use as artist photo', 'Overrides the "Use as featured image" phrase', 'wp-fedistream' ),
|
||||
'archives' => _x( 'Artist archives', 'The post type archive label', 'wp-fedistream' ),
|
||||
'insert_into_item' => _x( 'Insert into artist', 'Overrides the "Insert into post" phrase', 'wp-fedistream' ),
|
||||
'uploaded_to_this_item' => _x( 'Uploaded to this artist', 'Overrides the "Uploaded to this post" phrase', 'wp-fedistream' ),
|
||||
'filter_items_list' => _x( 'Filter artists list', 'Screen reader text', 'wp-fedistream' ),
|
||||
'items_list_navigation' => _x( 'Artists list navigation', 'Screen reader text', 'wp-fedistream' ),
|
||||
'items_list' => _x( 'Artists list', 'Screen reader text', 'wp-fedistream' ),
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'labels' => $labels,
|
||||
'public' => true,
|
||||
'publicly_queryable' => true,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => false, // Will be added to custom menu.
|
||||
'query_var' => true,
|
||||
'rewrite' => array( 'slug' => 'artists' ),
|
||||
'capability_type' => array( 'fedistream_artist', 'fedistream_artists' ),
|
||||
'map_meta_cap' => true,
|
||||
'has_archive' => true,
|
||||
'hierarchical' => false,
|
||||
'menu_position' => null,
|
||||
'menu_icon' => 'dashicons-groups',
|
||||
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt', 'revisions' ),
|
||||
'show_in_rest' => true,
|
||||
'rest_base' => 'artists',
|
||||
);
|
||||
|
||||
register_post_type( $this->post_type, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add meta boxes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_meta_boxes(): void {
|
||||
add_meta_box(
|
||||
'fedistream_artist_info',
|
||||
__( 'Artist Information', 'wp-fedistream' ),
|
||||
array( $this, 'render_info_meta_box' ),
|
||||
$this->post_type,
|
||||
'normal',
|
||||
'high'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_artist_social',
|
||||
__( 'Social Links', 'wp-fedistream' ),
|
||||
array( $this, 'render_social_meta_box' ),
|
||||
$this->post_type,
|
||||
'normal',
|
||||
'default'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_artist_members',
|
||||
__( 'Band Members', 'wp-fedistream' ),
|
||||
array( $this, 'render_members_meta_box' ),
|
||||
$this->post_type,
|
||||
'side',
|
||||
'default'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render artist info meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_info_meta_box( \WP_Post $post ): void {
|
||||
wp_nonce_field( 'fedistream_artist_save', 'fedistream_artist_nonce' );
|
||||
|
||||
$artist_type = get_post_meta( $post->ID, self::META_PREFIX . 'type', true );
|
||||
$formed_date = get_post_meta( $post->ID, self::META_PREFIX . 'formed_date', true );
|
||||
$location = get_post_meta( $post->ID, self::META_PREFIX . 'location', true );
|
||||
$website = get_post_meta( $post->ID, self::META_PREFIX . 'website', true );
|
||||
?>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_artist_type"><?php esc_html_e( 'Artist Type', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select name="fedistream_artist_type" id="fedistream_artist_type">
|
||||
<option value="solo" <?php selected( $artist_type, 'solo' ); ?>><?php esc_html_e( 'Solo Artist', 'wp-fedistream' ); ?></option>
|
||||
<option value="band" <?php selected( $artist_type, 'band' ); ?>><?php esc_html_e( 'Band', 'wp-fedistream' ); ?></option>
|
||||
<option value="duo" <?php selected( $artist_type, 'duo' ); ?>><?php esc_html_e( 'Duo', 'wp-fedistream' ); ?></option>
|
||||
<option value="collective" <?php selected( $artist_type, 'collective' ); ?>><?php esc_html_e( 'Collective', 'wp-fedistream' ); ?></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_artist_formed_date"><?php esc_html_e( 'Formed Date', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="date" name="fedistream_artist_formed_date" id="fedistream_artist_formed_date" value="<?php echo esc_attr( $formed_date ); ?>" class="regular-text">
|
||||
<p class="description"><?php esc_html_e( 'When the artist/band was formed or started their career.', 'wp-fedistream' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_artist_location"><?php esc_html_e( 'Location', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" name="fedistream_artist_location" id="fedistream_artist_location" value="<?php echo esc_attr( $location ); ?>" class="regular-text">
|
||||
<p class="description"><?php esc_html_e( 'City, Country or region where the artist is based.', 'wp-fedistream' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_artist_website"><?php esc_html_e( 'Website', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="url" name="fedistream_artist_website" id="fedistream_artist_website" value="<?php echo esc_url( $website ); ?>" class="regular-text">
|
||||
<p class="description"><?php esc_html_e( 'Official website URL.', 'wp-fedistream' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render social links meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_social_meta_box( \WP_Post $post ): void {
|
||||
$social_links = get_post_meta( $post->ID, self::META_PREFIX . 'social_links', true );
|
||||
if ( ! is_array( $social_links ) ) {
|
||||
$social_links = array();
|
||||
}
|
||||
|
||||
$platforms = array(
|
||||
'mastodon' => __( 'Mastodon', 'wp-fedistream' ),
|
||||
'bandcamp' => __( 'Bandcamp', 'wp-fedistream' ),
|
||||
'soundcloud' => __( 'SoundCloud', 'wp-fedistream' ),
|
||||
'youtube' => __( 'YouTube', 'wp-fedistream' ),
|
||||
'instagram' => __( 'Instagram', 'wp-fedistream' ),
|
||||
'twitter' => __( 'Twitter/X', 'wp-fedistream' ),
|
||||
'facebook' => __( 'Facebook', 'wp-fedistream' ),
|
||||
'tiktok' => __( 'TikTok', 'wp-fedistream' ),
|
||||
'other' => __( 'Other', 'wp-fedistream' ),
|
||||
);
|
||||
?>
|
||||
<table class="form-table">
|
||||
<?php foreach ( $platforms as $key => $label ) : ?>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_social_<?php echo esc_attr( $key ); ?>"><?php echo esc_html( $label ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="url" name="fedistream_artist_social[<?php echo esc_attr( $key ); ?>]" id="fedistream_social_<?php echo esc_attr( $key ); ?>" value="<?php echo esc_url( $social_links[ $key ] ?? '' ); ?>" class="regular-text">
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render band members meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_members_meta_box( \WP_Post $post ): void {
|
||||
$members = get_post_meta( $post->ID, self::META_PREFIX . 'members', true );
|
||||
if ( ! is_array( $members ) ) {
|
||||
$members = array();
|
||||
}
|
||||
|
||||
$artist_type = get_post_meta( $post->ID, self::META_PREFIX . 'type', true );
|
||||
?>
|
||||
<div id="fedistream-members-wrapper" style="<?php echo 'solo' === $artist_type ? 'display:none;' : ''; ?>">
|
||||
<p class="description"><?php esc_html_e( 'Add band/group members (comma-separated names).', 'wp-fedistream' ); ?></p>
|
||||
<textarea name="fedistream_artist_members" id="fedistream_artist_members" rows="5" class="large-text"><?php echo esc_textarea( implode( "\n", $members ) ); ?></textarea>
|
||||
<p class="description"><?php esc_html_e( 'One member per line.', 'wp-fedistream' ); ?></p>
|
||||
</div>
|
||||
<script>
|
||||
jQuery(document).ready(function($) {
|
||||
$('#fedistream_artist_type').on('change', function() {
|
||||
if ($(this).val() === 'solo') {
|
||||
$('#fedistream-members-wrapper').hide();
|
||||
} else {
|
||||
$('#fedistream-members-wrapper').show();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save post meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function save_meta( int $post_id, \WP_Post $post ): void {
|
||||
if ( ! $this->can_save( $post_id, 'fedistream_artist_save', 'fedistream_artist_nonce' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save artist type.
|
||||
if ( isset( $_POST['fedistream_artist_type'] ) ) {
|
||||
$allowed_types = array( 'solo', 'band', 'duo', 'collective' );
|
||||
$type = sanitize_text_field( wp_unslash( $_POST['fedistream_artist_type'] ) );
|
||||
if ( in_array( $type, $allowed_types, true ) ) {
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'type', $type );
|
||||
}
|
||||
}
|
||||
|
||||
// Save other fields.
|
||||
$this->save_date_meta( $post_id, self::META_PREFIX . 'formed_date', 'fedistream_artist_formed_date' );
|
||||
$this->save_text_meta( $post_id, self::META_PREFIX . 'location', 'fedistream_artist_location' );
|
||||
$this->save_url_meta( $post_id, self::META_PREFIX . 'website', 'fedistream_artist_website' );
|
||||
|
||||
// Save social links.
|
||||
if ( isset( $_POST['fedistream_artist_social'] ) && is_array( $_POST['fedistream_artist_social'] ) ) {
|
||||
$social_links = array();
|
||||
foreach ( $_POST['fedistream_artist_social'] as $key => $url ) {
|
||||
$clean_key = sanitize_key( $key );
|
||||
$clean_url = esc_url_raw( wp_unslash( $url ) );
|
||||
if ( ! empty( $clean_url ) ) {
|
||||
$social_links[ $clean_key ] = $clean_url;
|
||||
}
|
||||
}
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'social_links', $social_links );
|
||||
}
|
||||
|
||||
// Save members.
|
||||
if ( isset( $_POST['fedistream_artist_members'] ) ) {
|
||||
$members_text = sanitize_textarea_field( wp_unslash( $_POST['fedistream_artist_members'] ) );
|
||||
$members = array_filter( array_map( 'trim', explode( "\n", $members_text ) ) );
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'members', $members );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get artist by ID with meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return array|null Artist data or null.
|
||||
*/
|
||||
public static function get_artist( int $post_id ): ?array {
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post || 'fedistream_artist' !== $post->post_type ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'id' => $post->ID,
|
||||
'name' => $post->post_title,
|
||||
'slug' => $post->post_name,
|
||||
'bio' => $post->post_content,
|
||||
'excerpt' => $post->post_excerpt,
|
||||
'type' => get_post_meta( $post_id, self::META_PREFIX . 'type', true ) ?: 'solo',
|
||||
'formed_date' => get_post_meta( $post_id, self::META_PREFIX . 'formed_date', true ),
|
||||
'location' => get_post_meta( $post_id, self::META_PREFIX . 'location', true ),
|
||||
'website' => get_post_meta( $post_id, self::META_PREFIX . 'website', true ),
|
||||
'social_links' => get_post_meta( $post_id, self::META_PREFIX . 'social_links', true ) ?: array(),
|
||||
'members' => get_post_meta( $post_id, self::META_PREFIX . 'members', true ) ?: array(),
|
||||
'photo' => get_the_post_thumbnail_url( $post_id, 'large' ),
|
||||
'url' => get_permalink( $post_id ),
|
||||
);
|
||||
}
|
||||
}
|
||||
458
includes/PostTypes/Playlist.php
Normal file
458
includes/PostTypes/Playlist.php
Normal file
@@ -0,0 +1,458 @@
|
||||
<?php
|
||||
/**
|
||||
* Playlist custom post type.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\PostTypes;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Playlist post type class.
|
||||
*
|
||||
* Handles registration and management of playlists.
|
||||
*/
|
||||
class Playlist extends AbstractPostType {
|
||||
|
||||
/**
|
||||
* Post type key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $post_type = 'fedistream_playlist';
|
||||
|
||||
/**
|
||||
* Meta key prefix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private const META_PREFIX = '_fedistream_playlist_';
|
||||
|
||||
/**
|
||||
* Register the post type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void {
|
||||
$labels = array(
|
||||
'name' => _x( 'Playlists', 'Post type general name', 'wp-fedistream' ),
|
||||
'singular_name' => _x( 'Playlist', 'Post type singular name', 'wp-fedistream' ),
|
||||
'menu_name' => _x( 'Playlists', 'Admin Menu text', 'wp-fedistream' ),
|
||||
'name_admin_bar' => _x( 'Playlist', 'Add New on Toolbar', 'wp-fedistream' ),
|
||||
'add_new' => __( 'Add New', 'wp-fedistream' ),
|
||||
'add_new_item' => __( 'Add New Playlist', 'wp-fedistream' ),
|
||||
'new_item' => __( 'New Playlist', 'wp-fedistream' ),
|
||||
'edit_item' => __( 'Edit Playlist', 'wp-fedistream' ),
|
||||
'view_item' => __( 'View Playlist', 'wp-fedistream' ),
|
||||
'all_items' => __( 'All Playlists', 'wp-fedistream' ),
|
||||
'search_items' => __( 'Search Playlists', 'wp-fedistream' ),
|
||||
'parent_item_colon' => __( 'Parent Playlists:', 'wp-fedistream' ),
|
||||
'not_found' => __( 'No playlists found.', 'wp-fedistream' ),
|
||||
'not_found_in_trash' => __( 'No playlists found in Trash.', 'wp-fedistream' ),
|
||||
'featured_image' => _x( 'Playlist Cover', 'Overrides the "Featured Image" phrase', 'wp-fedistream' ),
|
||||
'set_featured_image' => _x( 'Set playlist cover', 'Overrides the "Set featured image" phrase', 'wp-fedistream' ),
|
||||
'remove_featured_image' => _x( 'Remove playlist cover', 'Overrides the "Remove featured image" phrase', 'wp-fedistream' ),
|
||||
'use_featured_image' => _x( 'Use as playlist cover', 'Overrides the "Use as featured image" phrase', 'wp-fedistream' ),
|
||||
'archives' => _x( 'Playlist archives', 'The post type archive label', 'wp-fedistream' ),
|
||||
'insert_into_item' => _x( 'Insert into playlist', 'Overrides the "Insert into post" phrase', 'wp-fedistream' ),
|
||||
'uploaded_to_this_item' => _x( 'Uploaded to this playlist', 'Overrides the "Uploaded to this post" phrase', 'wp-fedistream' ),
|
||||
'filter_items_list' => _x( 'Filter playlists list', 'Screen reader text', 'wp-fedistream' ),
|
||||
'items_list_navigation' => _x( 'Playlists list navigation', 'Screen reader text', 'wp-fedistream' ),
|
||||
'items_list' => _x( 'Playlists list', 'Screen reader text', 'wp-fedistream' ),
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'labels' => $labels,
|
||||
'public' => true,
|
||||
'publicly_queryable' => true,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => false, // Will be added to custom menu.
|
||||
'query_var' => true,
|
||||
'rewrite' => array( 'slug' => 'playlists' ),
|
||||
'capability_type' => array( 'fedistream_playlist', 'fedistream_playlists' ),
|
||||
'map_meta_cap' => true,
|
||||
'has_archive' => true,
|
||||
'hierarchical' => false,
|
||||
'menu_position' => null,
|
||||
'menu_icon' => 'dashicons-playlist-audio',
|
||||
'supports' => array( 'title', 'editor', 'thumbnail', 'author', 'revisions' ),
|
||||
'show_in_rest' => true,
|
||||
'rest_base' => 'playlists',
|
||||
);
|
||||
|
||||
register_post_type( $this->post_type, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add meta boxes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_meta_boxes(): void {
|
||||
add_meta_box(
|
||||
'fedistream_playlist_tracks',
|
||||
__( 'Playlist Tracks', 'wp-fedistream' ),
|
||||
array( $this, 'render_tracks_meta_box' ),
|
||||
$this->post_type,
|
||||
'normal',
|
||||
'high'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_playlist_settings',
|
||||
__( 'Playlist Settings', 'wp-fedistream' ),
|
||||
array( $this, 'render_settings_meta_box' ),
|
||||
$this->post_type,
|
||||
'side',
|
||||
'default'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_playlist_stats',
|
||||
__( 'Playlist Stats', 'wp-fedistream' ),
|
||||
array( $this, 'render_stats_meta_box' ),
|
||||
$this->post_type,
|
||||
'side',
|
||||
'default'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render playlist tracks meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_tracks_meta_box( \WP_Post $post ): void {
|
||||
wp_nonce_field( 'fedistream_playlist_save', 'fedistream_playlist_nonce' );
|
||||
|
||||
global $wpdb;
|
||||
|
||||
// Get tracks in this playlist from the pivot table.
|
||||
$table = $wpdb->prefix . 'fedistream_playlist_tracks';
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$playlist_tracks = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT track_id, position FROM $table WHERE playlist_id = %d ORDER BY position ASC",
|
||||
$post->ID
|
||||
)
|
||||
);
|
||||
|
||||
$track_ids = wp_list_pluck( $playlist_tracks, 'track_id' );
|
||||
|
||||
// Get all available tracks.
|
||||
$available_tracks = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_track',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
?>
|
||||
<div class="fedistream-playlist-tracks">
|
||||
<h4><?php esc_html_e( 'Current Tracks', 'wp-fedistream' ); ?></h4>
|
||||
<ul id="fedistream-playlist-track-list" class="fedistream-sortable">
|
||||
<?php
|
||||
foreach ( $track_ids as $track_id ) :
|
||||
$track = get_post( $track_id );
|
||||
if ( ! $track ) {
|
||||
continue;
|
||||
}
|
||||
$artists = get_post_meta( $track_id, '_fedistream_track_artists', true ) ?: array();
|
||||
$duration = get_post_meta( $track_id, '_fedistream_track_duration', true );
|
||||
?>
|
||||
<li data-track-id="<?php echo esc_attr( $track_id ); ?>" style="padding: 8px; margin: 4px 0; background: #f9f9f9; border: 1px solid #ddd; cursor: move;">
|
||||
<input type="hidden" name="fedistream_playlist_tracks[]" value="<?php echo esc_attr( $track_id ); ?>">
|
||||
<span class="dashicons dashicons-menu" style="vertical-align: middle;"></span>
|
||||
<strong><?php echo esc_html( $track->post_title ); ?></strong>
|
||||
<?php if ( ! empty( $artists ) ) : ?>
|
||||
<span class="description">
|
||||
— <?php echo esc_html( implode( ', ', array_map( 'get_the_title', $artists ) ) ); ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<?php if ( $duration ) : ?>
|
||||
<span class="description">(<?php echo esc_html( gmdate( 'i:s', (int) $duration ) ); ?>)</span>
|
||||
<?php endif; ?>
|
||||
<button type="button" class="button-link fedistream-remove-track" style="color: #a00; float: right;">
|
||||
<?php esc_html_e( 'Remove', 'wp-fedistream' ); ?>
|
||||
</button>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
|
||||
<hr>
|
||||
|
||||
<h4><?php esc_html_e( 'Add Tracks', 'wp-fedistream' ); ?></h4>
|
||||
<p>
|
||||
<select id="fedistream-add-track-select" class="widefat">
|
||||
<option value=""><?php esc_html_e( '— Select a track to add —', 'wp-fedistream' ); ?></option>
|
||||
<?php foreach ( $available_tracks as $track ) : ?>
|
||||
<?php
|
||||
$artists = get_post_meta( $track->ID, '_fedistream_track_artists', true ) ?: array();
|
||||
$duration = get_post_meta( $track->ID, '_fedistream_track_duration', true );
|
||||
?>
|
||||
<option value="<?php echo esc_attr( $track->ID ); ?>" data-title="<?php echo esc_attr( $track->post_title ); ?>" data-artists="<?php echo esc_attr( implode( ', ', array_map( 'get_the_title', $artists ) ) ); ?>" data-duration="<?php echo esc_attr( $duration ? gmdate( 'i:s', (int) $duration ) : '' ); ?>">
|
||||
<?php echo esc_html( $track->post_title ); ?>
|
||||
<?php if ( ! empty( $artists ) ) : ?>
|
||||
— <?php echo esc_html( implode( ', ', array_map( 'get_the_title', $artists ) ) ); ?>
|
||||
<?php endif; ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
<button type="button" class="button" id="fedistream-add-track-btn"><?php esc_html_e( 'Add to Playlist', 'wp-fedistream' ); ?></button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.fedistream-sortable li { list-style: none; }
|
||||
.fedistream-sortable .ui-sortable-helper { background: #fff !important; box-shadow: 0 2px 4px rgba(0,0,0,0.2); }
|
||||
.fedistream-sortable .ui-sortable-placeholder { visibility: visible !important; background: #e0e0e0; border: 1px dashed #999; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
jQuery(document).ready(function($) {
|
||||
// Make list sortable.
|
||||
$('#fedistream-playlist-track-list').sortable({
|
||||
placeholder: 'ui-sortable-placeholder',
|
||||
axis: 'y'
|
||||
});
|
||||
|
||||
// Add track.
|
||||
$('#fedistream-add-track-btn').on('click', function() {
|
||||
var $select = $('#fedistream-add-track-select');
|
||||
var trackId = $select.val();
|
||||
if (!trackId) return;
|
||||
|
||||
var $option = $select.find('option:selected');
|
||||
var title = $option.data('title');
|
||||
var artists = $option.data('artists');
|
||||
var duration = $option.data('duration');
|
||||
|
||||
var html = '<li data-track-id="' + trackId + '" style="padding: 8px; margin: 4px 0; background: #f9f9f9; border: 1px solid #ddd; cursor: move;">' +
|
||||
'<input type="hidden" name="fedistream_playlist_tracks[]" value="' + trackId + '">' +
|
||||
'<span class="dashicons dashicons-menu" style="vertical-align: middle;"></span> ' +
|
||||
'<strong>' + title + '</strong>';
|
||||
|
||||
if (artists) {
|
||||
html += ' <span class="description">— ' + artists + '</span>';
|
||||
}
|
||||
if (duration) {
|
||||
html += ' <span class="description">(' + duration + ')</span>';
|
||||
}
|
||||
|
||||
html += '<button type="button" class="button-link fedistream-remove-track" style="color: #a00; float: right;"><?php echo esc_js( __( 'Remove', 'wp-fedistream' ) ); ?></button>' +
|
||||
'</li>';
|
||||
|
||||
$('#fedistream-playlist-track-list').append(html);
|
||||
$select.val('');
|
||||
});
|
||||
|
||||
// Remove track.
|
||||
$(document).on('click', '.fedistream-remove-track', function() {
|
||||
$(this).closest('li').remove();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render playlist settings meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_settings_meta_box( \WP_Post $post ): void {
|
||||
$visibility = get_post_meta( $post->ID, self::META_PREFIX . 'visibility', true ) ?: 'public';
|
||||
$collaborative = get_post_meta( $post->ID, self::META_PREFIX . 'collaborative', true );
|
||||
$federated = get_post_meta( $post->ID, self::META_PREFIX . 'federated', true );
|
||||
?>
|
||||
<p>
|
||||
<label for="fedistream_playlist_visibility"><?php esc_html_e( 'Visibility', 'wp-fedistream' ); ?></label>
|
||||
<select name="fedistream_playlist_visibility" id="fedistream_playlist_visibility" class="widefat">
|
||||
<option value="public" <?php selected( $visibility, 'public' ); ?>><?php esc_html_e( 'Public', 'wp-fedistream' ); ?></option>
|
||||
<option value="unlisted" <?php selected( $visibility, 'unlisted' ); ?>><?php esc_html_e( 'Unlisted (link only)', 'wp-fedistream' ); ?></option>
|
||||
<option value="private" <?php selected( $visibility, 'private' ); ?>><?php esc_html_e( 'Private', 'wp-fedistream' ); ?></option>
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" name="fedistream_playlist_collaborative" value="1" <?php checked( $collaborative, 1 ); ?>>
|
||||
<?php esc_html_e( 'Collaborative', 'wp-fedistream' ); ?>
|
||||
</label>
|
||||
<br>
|
||||
<span class="description"><?php esc_html_e( 'Allow others to add tracks.', 'wp-fedistream' ); ?></span>
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" name="fedistream_playlist_federated" value="1" <?php checked( $federated, 1 ); ?>>
|
||||
<?php esc_html_e( 'Federated', 'wp-fedistream' ); ?>
|
||||
</label>
|
||||
<br>
|
||||
<span class="description"><?php esc_html_e( 'Allow tracks from other FediStream instances.', 'wp-fedistream' ); ?></span>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render playlist stats meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_stats_meta_box( \WP_Post $post ): void {
|
||||
$track_count = get_post_meta( $post->ID, self::META_PREFIX . 'track_count', true ) ?: 0;
|
||||
$total_duration = get_post_meta( $post->ID, self::META_PREFIX . 'total_duration', true ) ?: 0;
|
||||
?>
|
||||
<p>
|
||||
<strong><?php esc_html_e( 'Tracks:', 'wp-fedistream' ); ?></strong>
|
||||
<?php echo esc_html( $track_count ); ?>
|
||||
</p>
|
||||
<p>
|
||||
<strong><?php esc_html_e( 'Total Duration:', 'wp-fedistream' ); ?></strong>
|
||||
<?php
|
||||
if ( $total_duration > 3600 ) {
|
||||
echo esc_html( gmdate( 'H:i:s', (int) $total_duration ) );
|
||||
} else {
|
||||
echo esc_html( gmdate( 'i:s', (int) $total_duration ) );
|
||||
}
|
||||
?>
|
||||
</p>
|
||||
<p class="description"><?php esc_html_e( 'Stats are automatically updated when saved.', 'wp-fedistream' ); ?></p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save post meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function save_meta( int $post_id, \WP_Post $post ): void {
|
||||
if ( ! $this->can_save( $post_id, 'fedistream_playlist_save', 'fedistream_playlist_nonce' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'fedistream_playlist_tracks';
|
||||
|
||||
// Save visibility.
|
||||
if ( isset( $_POST['fedistream_playlist_visibility'] ) ) {
|
||||
$allowed_visibility = array( 'public', 'unlisted', 'private' );
|
||||
$visibility = sanitize_text_field( wp_unslash( $_POST['fedistream_playlist_visibility'] ) );
|
||||
if ( in_array( $visibility, $allowed_visibility, true ) ) {
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'visibility', $visibility );
|
||||
}
|
||||
}
|
||||
|
||||
// Save settings.
|
||||
$this->save_bool_meta( $post_id, self::META_PREFIX . 'collaborative', 'fedistream_playlist_collaborative' );
|
||||
$this->save_bool_meta( $post_id, self::META_PREFIX . 'federated', 'fedistream_playlist_federated' );
|
||||
|
||||
// Save tracks.
|
||||
// First, delete existing tracks for this playlist.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$wpdb->delete( $table, array( 'playlist_id' => $post_id ), array( '%d' ) );
|
||||
|
||||
// Insert new tracks.
|
||||
if ( isset( $_POST['fedistream_playlist_tracks'] ) && is_array( $_POST['fedistream_playlist_tracks'] ) ) {
|
||||
$position = 0;
|
||||
$total_duration = 0;
|
||||
|
||||
foreach ( $_POST['fedistream_playlist_tracks'] as $track_id ) {
|
||||
$track_id = absint( $track_id );
|
||||
if ( $track_id > 0 ) {
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$wpdb->insert(
|
||||
$table,
|
||||
array(
|
||||
'playlist_id' => $post_id,
|
||||
'track_id' => $track_id,
|
||||
'position' => $position,
|
||||
),
|
||||
array( '%d', '%d', '%d' )
|
||||
);
|
||||
++$position;
|
||||
|
||||
// Sum duration.
|
||||
$duration = (int) get_post_meta( $track_id, '_fedistream_track_duration', true );
|
||||
$total_duration += $duration;
|
||||
}
|
||||
}
|
||||
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'track_count', $position );
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'total_duration', $total_duration );
|
||||
} else {
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'track_count', 0 );
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'total_duration', 0 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get playlist by ID with meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return array|null Playlist data or null.
|
||||
*/
|
||||
public static function get_playlist( int $post_id ): ?array {
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post || 'fedistream_playlist' !== $post->post_type ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'id' => $post->ID,
|
||||
'title' => $post->post_title,
|
||||
'slug' => $post->post_name,
|
||||
'description' => $post->post_content,
|
||||
'author_id' => $post->post_author,
|
||||
'author_name' => get_the_author_meta( 'display_name', $post->post_author ),
|
||||
'visibility' => get_post_meta( $post_id, self::META_PREFIX . 'visibility', true ) ?: 'public',
|
||||
'collaborative' => (bool) get_post_meta( $post_id, self::META_PREFIX . 'collaborative', true ),
|
||||
'federated' => (bool) get_post_meta( $post_id, self::META_PREFIX . 'federated', true ),
|
||||
'track_count' => (int) get_post_meta( $post_id, self::META_PREFIX . 'track_count', true ),
|
||||
'total_duration' => (int) get_post_meta( $post_id, self::META_PREFIX . 'total_duration', true ),
|
||||
'cover' => get_the_post_thumbnail_url( $post_id, 'large' ),
|
||||
'url' => get_permalink( $post_id ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tracks in a playlist.
|
||||
*
|
||||
* @param int $playlist_id Playlist post ID.
|
||||
* @return array Array of track data.
|
||||
*/
|
||||
public static function get_playlist_tracks( int $playlist_id ): array {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_playlist_tracks';
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$results = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT track_id FROM $table WHERE playlist_id = %d ORDER BY position ASC",
|
||||
$playlist_id
|
||||
)
|
||||
);
|
||||
|
||||
$tracks = array();
|
||||
foreach ( $results as $row ) {
|
||||
$track = Track::get_track( (int) $row->track_id );
|
||||
if ( $track ) {
|
||||
$tracks[] = $track;
|
||||
}
|
||||
}
|
||||
|
||||
return $tracks;
|
||||
}
|
||||
}
|
||||
615
includes/PostTypes/Track.php
Normal file
615
includes/PostTypes/Track.php
Normal file
@@ -0,0 +1,615 @@
|
||||
<?php
|
||||
/**
|
||||
* Track custom post type.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\PostTypes;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track post type class.
|
||||
*
|
||||
* Handles registration and management of individual tracks.
|
||||
*/
|
||||
class Track extends AbstractPostType {
|
||||
|
||||
/**
|
||||
* Post type key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $post_type = 'fedistream_track';
|
||||
|
||||
/**
|
||||
* Meta key prefix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private const META_PREFIX = '_fedistream_track_';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
add_action( 'save_post_fedistream_track', array( $this, 'update_album_on_save' ), 20, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the post type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void {
|
||||
$labels = array(
|
||||
'name' => _x( 'Tracks', 'Post type general name', 'wp-fedistream' ),
|
||||
'singular_name' => _x( 'Track', 'Post type singular name', 'wp-fedistream' ),
|
||||
'menu_name' => _x( 'Tracks', 'Admin Menu text', 'wp-fedistream' ),
|
||||
'name_admin_bar' => _x( 'Track', 'Add New on Toolbar', 'wp-fedistream' ),
|
||||
'add_new' => __( 'Add New', 'wp-fedistream' ),
|
||||
'add_new_item' => __( 'Add New Track', 'wp-fedistream' ),
|
||||
'new_item' => __( 'New Track', 'wp-fedistream' ),
|
||||
'edit_item' => __( 'Edit Track', 'wp-fedistream' ),
|
||||
'view_item' => __( 'View Track', 'wp-fedistream' ),
|
||||
'all_items' => __( 'All Tracks', 'wp-fedistream' ),
|
||||
'search_items' => __( 'Search Tracks', 'wp-fedistream' ),
|
||||
'parent_item_colon' => __( 'Parent Tracks:', 'wp-fedistream' ),
|
||||
'not_found' => __( 'No tracks found.', 'wp-fedistream' ),
|
||||
'not_found_in_trash' => __( 'No tracks found in Trash.', 'wp-fedistream' ),
|
||||
'featured_image' => _x( 'Track Artwork', 'Overrides the "Featured Image" phrase', 'wp-fedistream' ),
|
||||
'set_featured_image' => _x( 'Set track artwork', 'Overrides the "Set featured image" phrase', 'wp-fedistream' ),
|
||||
'remove_featured_image' => _x( 'Remove track artwork', 'Overrides the "Remove featured image" phrase', 'wp-fedistream' ),
|
||||
'use_featured_image' => _x( 'Use as track artwork', 'Overrides the "Use as featured image" phrase', 'wp-fedistream' ),
|
||||
'archives' => _x( 'Track archives', 'The post type archive label', 'wp-fedistream' ),
|
||||
'insert_into_item' => _x( 'Insert into track', 'Overrides the "Insert into post" phrase', 'wp-fedistream' ),
|
||||
'uploaded_to_this_item' => _x( 'Uploaded to this track', 'Overrides the "Uploaded to this post" phrase', 'wp-fedistream' ),
|
||||
'filter_items_list' => _x( 'Filter tracks list', 'Screen reader text', 'wp-fedistream' ),
|
||||
'items_list_navigation' => _x( 'Tracks list navigation', 'Screen reader text', 'wp-fedistream' ),
|
||||
'items_list' => _x( 'Tracks list', 'Screen reader text', 'wp-fedistream' ),
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'labels' => $labels,
|
||||
'public' => true,
|
||||
'publicly_queryable' => true,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => false, // Will be added to custom menu.
|
||||
'query_var' => true,
|
||||
'rewrite' => array( 'slug' => 'tracks' ),
|
||||
'capability_type' => array( 'fedistream_track', 'fedistream_tracks' ),
|
||||
'map_meta_cap' => true,
|
||||
'has_archive' => true,
|
||||
'hierarchical' => false,
|
||||
'menu_position' => null,
|
||||
'menu_icon' => 'dashicons-format-audio',
|
||||
'supports' => array( 'title', 'editor', 'thumbnail', 'revisions' ),
|
||||
'show_in_rest' => true,
|
||||
'rest_base' => 'tracks',
|
||||
);
|
||||
|
||||
register_post_type( $this->post_type, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add meta boxes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_meta_boxes(): void {
|
||||
add_meta_box(
|
||||
'fedistream_track_audio',
|
||||
__( 'Audio File', 'wp-fedistream' ),
|
||||
array( $this, 'render_audio_meta_box' ),
|
||||
$this->post_type,
|
||||
'normal',
|
||||
'high'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_track_info',
|
||||
__( 'Track Information', 'wp-fedistream' ),
|
||||
array( $this, 'render_info_meta_box' ),
|
||||
$this->post_type,
|
||||
'normal',
|
||||
'high'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_track_album',
|
||||
__( 'Album', 'wp-fedistream' ),
|
||||
array( $this, 'render_album_meta_box' ),
|
||||
$this->post_type,
|
||||
'side',
|
||||
'high'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_track_artists',
|
||||
__( 'Artists', 'wp-fedistream' ),
|
||||
array( $this, 'render_artists_meta_box' ),
|
||||
$this->post_type,
|
||||
'side',
|
||||
'default'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_track_codes',
|
||||
__( 'Track Codes', 'wp-fedistream' ),
|
||||
array( $this, 'render_codes_meta_box' ),
|
||||
$this->post_type,
|
||||
'side',
|
||||
'default'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render audio file meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_audio_meta_box( \WP_Post $post ): void {
|
||||
wp_nonce_field( 'fedistream_track_save', 'fedistream_track_nonce' );
|
||||
|
||||
$audio_file = get_post_meta( $post->ID, self::META_PREFIX . 'audio_file', true );
|
||||
$audio_format = get_post_meta( $post->ID, self::META_PREFIX . 'audio_format', true );
|
||||
$duration = get_post_meta( $post->ID, self::META_PREFIX . 'duration', true );
|
||||
|
||||
wp_enqueue_media();
|
||||
?>
|
||||
<div class="fedistream-audio-upload">
|
||||
<p>
|
||||
<label for="fedistream_track_audio_file"><?php esc_html_e( 'Audio File', 'wp-fedistream' ); ?></label>
|
||||
</p>
|
||||
<input type="hidden" name="fedistream_track_audio_file" id="fedistream_track_audio_file" value="<?php echo esc_attr( $audio_file ); ?>">
|
||||
<div id="fedistream-audio-preview">
|
||||
<?php if ( $audio_file ) : ?>
|
||||
<?php
|
||||
$audio_url = wp_get_attachment_url( $audio_file );
|
||||
if ( $audio_url ) :
|
||||
?>
|
||||
<audio controls style="width: 100%;">
|
||||
<source src="<?php echo esc_url( $audio_url ); ?>" type="audio/<?php echo esc_attr( $audio_format ?: 'mpeg' ); ?>">
|
||||
</audio>
|
||||
<p><strong><?php echo esc_html( basename( get_attached_file( $audio_file ) ) ); ?></strong></p>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<p>
|
||||
<button type="button" class="button" id="fedistream-upload-audio"><?php esc_html_e( 'Select Audio File', 'wp-fedistream' ); ?></button>
|
||||
<button type="button" class="button" id="fedistream-remove-audio" style="<?php echo $audio_file ? '' : 'display:none;'; ?>"><?php esc_html_e( 'Remove', 'wp-fedistream' ); ?></button>
|
||||
</p>
|
||||
<p class="description"><?php esc_html_e( 'Supported formats: MP3, WAV, FLAC, OGG', 'wp-fedistream' ); ?></p>
|
||||
</div>
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_track_audio_format"><?php esc_html_e( 'Audio Format', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select name="fedistream_track_audio_format" id="fedistream_track_audio_format">
|
||||
<option value=""><?php esc_html_e( '— Auto-detect —', 'wp-fedistream' ); ?></option>
|
||||
<option value="mp3" <?php selected( $audio_format, 'mp3' ); ?>>MP3</option>
|
||||
<option value="wav" <?php selected( $audio_format, 'wav' ); ?>>WAV</option>
|
||||
<option value="flac" <?php selected( $audio_format, 'flac' ); ?>>FLAC</option>
|
||||
<option value="ogg" <?php selected( $audio_format, 'ogg' ); ?>>OGG</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_track_duration"><?php esc_html_e( 'Duration (seconds)', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="fedistream_track_duration" id="fedistream_track_duration" value="<?php echo esc_attr( $duration ); ?>" min="0" class="small-text">
|
||||
<span id="fedistream-duration-display">
|
||||
<?php
|
||||
if ( $duration ) {
|
||||
printf(
|
||||
/* translators: %s: formatted duration */
|
||||
esc_html__( '(%s)', 'wp-fedistream' ),
|
||||
esc_html( gmdate( 'i:s', (int) $duration ) )
|
||||
);
|
||||
}
|
||||
?>
|
||||
</span>
|
||||
<p class="description"><?php esc_html_e( 'Auto-detected from audio file if available.', 'wp-fedistream' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<script>
|
||||
jQuery(document).ready(function($) {
|
||||
var mediaUploader;
|
||||
|
||||
$('#fedistream-upload-audio').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (mediaUploader) {
|
||||
mediaUploader.open();
|
||||
return;
|
||||
}
|
||||
|
||||
mediaUploader = wp.media({
|
||||
title: '<?php echo esc_js( __( 'Select Audio File', 'wp-fedistream' ) ); ?>',
|
||||
button: { text: '<?php echo esc_js( __( 'Use this file', 'wp-fedistream' ) ); ?>' },
|
||||
library: { type: 'audio' },
|
||||
multiple: false
|
||||
});
|
||||
|
||||
mediaUploader.on('select', function() {
|
||||
var attachment = mediaUploader.state().get('selection').first().toJSON();
|
||||
$('#fedistream_track_audio_file').val(attachment.id);
|
||||
$('#fedistream-audio-preview').html(
|
||||
'<audio controls style="width: 100%;"><source src="' + attachment.url + '" type="' + attachment.mime + '"></audio>' +
|
||||
'<p><strong>' + attachment.filename + '</strong></p>'
|
||||
);
|
||||
$('#fedistream-remove-audio').show();
|
||||
|
||||
// Auto-detect format.
|
||||
var ext = attachment.filename.split('.').pop().toLowerCase();
|
||||
if (['mp3', 'wav', 'flac', 'ogg'].indexOf(ext) !== -1) {
|
||||
$('#fedistream_track_audio_format').val(ext);
|
||||
}
|
||||
});
|
||||
|
||||
mediaUploader.open();
|
||||
});
|
||||
|
||||
$('#fedistream-remove-audio').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
$('#fedistream_track_audio_file').val('');
|
||||
$('#fedistream-audio-preview').html('');
|
||||
$(this).hide();
|
||||
});
|
||||
|
||||
// Duration display.
|
||||
$('#fedistream_track_duration').on('change', function() {
|
||||
var seconds = parseInt($(this).val(), 10);
|
||||
if (seconds > 0) {
|
||||
var mins = Math.floor(seconds / 60);
|
||||
var secs = seconds % 60;
|
||||
$('#fedistream-duration-display').text('(' + mins + ':' + (secs < 10 ? '0' : '') + secs + ')');
|
||||
} else {
|
||||
$('#fedistream-duration-display').text('');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render track info meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_info_meta_box( \WP_Post $post ): void {
|
||||
$track_number = get_post_meta( $post->ID, self::META_PREFIX . 'number', true );
|
||||
$disc_number = get_post_meta( $post->ID, self::META_PREFIX . 'disc_number', true );
|
||||
$bpm = get_post_meta( $post->ID, self::META_PREFIX . 'bpm', true );
|
||||
$key = get_post_meta( $post->ID, self::META_PREFIX . 'key', true );
|
||||
$explicit = get_post_meta( $post->ID, self::META_PREFIX . 'explicit', true );
|
||||
$preview_start = get_post_meta( $post->ID, self::META_PREFIX . 'preview_start', true );
|
||||
$preview_duration = get_post_meta( $post->ID, self::META_PREFIX . 'preview_duration', true );
|
||||
|
||||
$musical_keys = array(
|
||||
'' => __( '— Select Key —', 'wp-fedistream' ),
|
||||
'C' => 'C Major',
|
||||
'Cm' => 'C Minor',
|
||||
'C#' => 'C# Major',
|
||||
'C#m' => 'C# Minor',
|
||||
'D' => 'D Major',
|
||||
'Dm' => 'D Minor',
|
||||
'D#' => 'D# Major',
|
||||
'D#m' => 'D# Minor',
|
||||
'E' => 'E Major',
|
||||
'Em' => 'E Minor',
|
||||
'F' => 'F Major',
|
||||
'Fm' => 'F Minor',
|
||||
'F#' => 'F# Major',
|
||||
'F#m' => 'F# Minor',
|
||||
'G' => 'G Major',
|
||||
'Gm' => 'G Minor',
|
||||
'G#' => 'G# Major',
|
||||
'G#m' => 'G# Minor',
|
||||
'A' => 'A Major',
|
||||
'Am' => 'A Minor',
|
||||
'A#' => 'A# Major',
|
||||
'A#m' => 'A# Minor',
|
||||
'B' => 'B Major',
|
||||
'Bm' => 'B Minor',
|
||||
);
|
||||
?>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_track_number"><?php esc_html_e( 'Track Number', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="fedistream_track_number" id="fedistream_track_number" value="<?php echo esc_attr( $track_number ); ?>" min="1" max="999" class="small-text">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_track_disc_number"><?php esc_html_e( 'Disc Number', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="fedistream_track_disc_number" id="fedistream_track_disc_number" value="<?php echo esc_attr( $disc_number ?: 1 ); ?>" min="1" max="99" class="small-text">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_track_bpm"><?php esc_html_e( 'BPM', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="fedistream_track_bpm" id="fedistream_track_bpm" value="<?php echo esc_attr( $bpm ); ?>" min="20" max="300" class="small-text">
|
||||
<p class="description"><?php esc_html_e( 'Beats per minute (tempo).', 'wp-fedistream' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_track_key"><?php esc_html_e( 'Musical Key', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select name="fedistream_track_key" id="fedistream_track_key">
|
||||
<?php foreach ( $musical_keys as $value => $label ) : ?>
|
||||
<option value="<?php echo esc_attr( $value ); ?>" <?php selected( $key, $value ); ?>><?php echo esc_html( $label ); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<?php esc_html_e( 'Explicit Content', 'wp-fedistream' ); ?>
|
||||
</th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="fedistream_track_explicit" value="1" <?php checked( $explicit, 1 ); ?>>
|
||||
<?php esc_html_e( 'This track contains explicit content', 'wp-fedistream' ); ?>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_track_preview_start"><?php esc_html_e( 'Preview Settings', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<label>
|
||||
<?php esc_html_e( 'Start at:', 'wp-fedistream' ); ?>
|
||||
<input type="number" name="fedistream_track_preview_start" id="fedistream_track_preview_start" value="<?php echo esc_attr( $preview_start ?: 0 ); ?>" min="0" class="small-text">
|
||||
<?php esc_html_e( 'seconds', 'wp-fedistream' ); ?>
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
<?php esc_html_e( 'Duration:', 'wp-fedistream' ); ?>
|
||||
<input type="number" name="fedistream_track_preview_duration" id="fedistream_track_preview_duration" value="<?php echo esc_attr( $preview_duration ?: 30 ); ?>" min="10" max="60" class="small-text">
|
||||
<?php esc_html_e( 'seconds', 'wp-fedistream' ); ?>
|
||||
</label>
|
||||
<p class="description"><?php esc_html_e( 'Preview clip for non-authenticated users or before purchase.', 'wp-fedistream' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render album selection meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_album_meta_box( \WP_Post $post ): void {
|
||||
$selected_album = get_post_meta( $post->ID, self::META_PREFIX . 'album', true );
|
||||
|
||||
$albums = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_album',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => array( 'publish', 'draft' ),
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
?>
|
||||
<p>
|
||||
<select name="fedistream_track_album" id="fedistream_track_album" class="widefat">
|
||||
<option value=""><?php esc_html_e( '— No Album (Single) —', 'wp-fedistream' ); ?></option>
|
||||
<?php foreach ( $albums as $album ) : ?>
|
||||
<?php
|
||||
$artist_id = get_post_meta( $album->ID, '_fedistream_album_artist', true );
|
||||
$artist_name = $artist_id ? get_the_title( $artist_id ) : '';
|
||||
?>
|
||||
<option value="<?php echo esc_attr( $album->ID ); ?>" <?php selected( $selected_album, $album->ID ); ?>>
|
||||
<?php echo esc_html( $album->post_title ); ?>
|
||||
<?php if ( $artist_name ) : ?>
|
||||
(<?php echo esc_html( $artist_name ); ?>)
|
||||
<?php endif; ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render artists meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_artists_meta_box( \WP_Post $post ): void {
|
||||
$selected_artists = get_post_meta( $post->ID, self::META_PREFIX . 'artists', true );
|
||||
if ( ! is_array( $selected_artists ) ) {
|
||||
$selected_artists = array();
|
||||
}
|
||||
|
||||
$artists = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_artist',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
?>
|
||||
<p class="description"><?php esc_html_e( 'Select all artists featured on this track.', 'wp-fedistream' ); ?></p>
|
||||
<div style="max-height: 200px; overflow-y: auto; border: 1px solid #ddd; padding: 5px;">
|
||||
<?php foreach ( $artists as $artist ) : ?>
|
||||
<label style="display: block; padding: 2px 0;">
|
||||
<input type="checkbox" name="fedistream_track_artists[]" value="<?php echo esc_attr( $artist->ID ); ?>" <?php checked( in_array( $artist->ID, $selected_artists, true ) ); ?>>
|
||||
<?php echo esc_html( $artist->post_title ); ?>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php if ( empty( $artists ) ) : ?>
|
||||
<p>
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: URL to add new artist */
|
||||
esc_html__( 'No artists found. %s', 'wp-fedistream' ),
|
||||
'<a href="' . esc_url( admin_url( 'post-new.php?post_type=fedistream_artist' ) ) . '">' . esc_html__( 'Add an artist first.', 'wp-fedistream' ) . '</a>'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render track codes meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_codes_meta_box( \WP_Post $post ): void {
|
||||
$isrc = get_post_meta( $post->ID, self::META_PREFIX . 'isrc', true );
|
||||
?>
|
||||
<p>
|
||||
<label for="fedistream_track_isrc"><?php esc_html_e( 'ISRC', 'wp-fedistream' ); ?></label>
|
||||
<input type="text" name="fedistream_track_isrc" id="fedistream_track_isrc" value="<?php echo esc_attr( $isrc ); ?>" class="widefat" pattern="[A-Z]{2}[A-Z0-9]{3}[0-9]{7}" title="<?php esc_attr_e( 'ISRC format: CC-XXX-YY-NNNNN', 'wp-fedistream' ); ?>">
|
||||
<span class="description"><?php esc_html_e( 'International Standard Recording Code', 'wp-fedistream' ); ?></span>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save post meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function save_meta( int $post_id, \WP_Post $post ): void {
|
||||
if ( ! $this->can_save( $post_id, 'fedistream_track_save', 'fedistream_track_nonce' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save audio fields.
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'audio_file', 'fedistream_track_audio_file' );
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'duration', 'fedistream_track_duration' );
|
||||
|
||||
// Save audio format.
|
||||
if ( isset( $_POST['fedistream_track_audio_format'] ) ) {
|
||||
$allowed_formats = array( '', 'mp3', 'wav', 'flac', 'ogg' );
|
||||
$format = sanitize_text_field( wp_unslash( $_POST['fedistream_track_audio_format'] ) );
|
||||
if ( in_array( $format, $allowed_formats, true ) ) {
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'audio_format', $format );
|
||||
}
|
||||
}
|
||||
|
||||
// Save track info.
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'number', 'fedistream_track_number' );
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'disc_number', 'fedistream_track_disc_number' );
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'bpm', 'fedistream_track_bpm' );
|
||||
$this->save_text_meta( $post_id, self::META_PREFIX . 'key', 'fedistream_track_key' );
|
||||
$this->save_bool_meta( $post_id, self::META_PREFIX . 'explicit', 'fedistream_track_explicit' );
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'preview_start', 'fedistream_track_preview_start' );
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'preview_duration', 'fedistream_track_preview_duration' );
|
||||
|
||||
// Save album.
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'album', 'fedistream_track_album' );
|
||||
|
||||
// Save artists.
|
||||
if ( isset( $_POST['fedistream_track_artists'] ) && is_array( $_POST['fedistream_track_artists'] ) ) {
|
||||
$artists = array_map( 'absint', $_POST['fedistream_track_artists'] );
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'artists', $artists );
|
||||
} else {
|
||||
delete_post_meta( $post_id, self::META_PREFIX . 'artists' );
|
||||
}
|
||||
|
||||
// Save ISRC.
|
||||
$this->save_text_meta( $post_id, self::META_PREFIX . 'isrc', 'fedistream_track_isrc' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update album stats when track is saved.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function update_album_on_save( int $post_id, \WP_Post $post ): void {
|
||||
$album_id = get_post_meta( $post_id, self::META_PREFIX . 'album', true );
|
||||
if ( $album_id ) {
|
||||
Album::update_album_stats( (int) $album_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track by ID with meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return array|null Track data or null.
|
||||
*/
|
||||
public static function get_track( int $post_id ): ?array {
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post || 'fedistream_track' !== $post->post_type ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$album_id = get_post_meta( $post_id, self::META_PREFIX . 'album', true );
|
||||
$audio_file = get_post_meta( $post_id, self::META_PREFIX . 'audio_file', true );
|
||||
$artists = get_post_meta( $post_id, self::META_PREFIX . 'artists', true ) ?: array();
|
||||
|
||||
// Get artist names.
|
||||
$artist_names = array();
|
||||
foreach ( $artists as $artist_id ) {
|
||||
$artist_names[] = get_the_title( $artist_id );
|
||||
}
|
||||
|
||||
return array(
|
||||
'id' => $post->ID,
|
||||
'title' => $post->post_title,
|
||||
'slug' => $post->post_name,
|
||||
'lyrics' => $post->post_content,
|
||||
'album_id' => $album_id ? (int) $album_id : null,
|
||||
'album_title' => $album_id ? get_the_title( $album_id ) : null,
|
||||
'artists' => $artists,
|
||||
'artist_names' => $artist_names,
|
||||
'track_number' => (int) get_post_meta( $post_id, self::META_PREFIX . 'number', true ),
|
||||
'disc_number' => (int) get_post_meta( $post_id, self::META_PREFIX . 'disc_number', true ) ?: 1,
|
||||
'duration' => (int) get_post_meta( $post_id, self::META_PREFIX . 'duration', true ),
|
||||
'audio_file' => $audio_file ? (int) $audio_file : null,
|
||||
'audio_url' => $audio_file ? wp_get_attachment_url( $audio_file ) : null,
|
||||
'audio_format' => get_post_meta( $post_id, self::META_PREFIX . 'audio_format', true ),
|
||||
'bpm' => (int) get_post_meta( $post_id, self::META_PREFIX . 'bpm', true ),
|
||||
'key' => get_post_meta( $post_id, self::META_PREFIX . 'key', true ),
|
||||
'explicit' => (bool) get_post_meta( $post_id, self::META_PREFIX . 'explicit', true ),
|
||||
'isrc' => get_post_meta( $post_id, self::META_PREFIX . 'isrc', true ),
|
||||
'preview_start' => (int) get_post_meta( $post_id, self::META_PREFIX . 'preview_start', true ),
|
||||
'preview_duration' => (int) get_post_meta( $post_id, self::META_PREFIX . 'preview_duration', true ) ?: 30,
|
||||
'artwork' => get_the_post_thumbnail_url( $post_id, 'large' ),
|
||||
'url' => get_permalink( $post_id ),
|
||||
);
|
||||
}
|
||||
}
|
||||
1
includes/PostTypes/index.php
Normal file
1
includes/PostTypes/index.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php // Silence is golden.
|
||||
Reference in New Issue
Block a user