Files
magdev 4a5d7b9f4d 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>
2026-01-28 23:23:05 +01:00

341 lines
15 KiB
PHP

<?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 );
}
}