Files
wp-fedistream/includes/WooCommerce/AlbumProduct.php
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

500 lines
10 KiB
PHP

<?php
/**
* Album Product Type for WooCommerce.
*
* @package WP_FediStream
*/
namespace WP_FediStream\WooCommerce;
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* FediStream Album product type.
*
* Digital product representing a FediStream album.
*/
class AlbumProduct extends \WC_Product {
/**
* Product type.
*
* @var string
*/
protected $product_type = 'fedistream_album';
/**
* Constructor.
*
* @param int|\WC_Product|object $product Product ID or object.
*/
public function __construct( $product = 0 ) {
parent::__construct( $product );
}
/**
* Get product type.
*
* @return string
*/
public function get_type(): string {
return 'fedistream_album';
}
/**
* Albums are virtual products.
*
* @param string $context View or edit context.
* @return bool
*/
public function get_virtual( $context = 'view' ): bool {
return true;
}
/**
* Albums are downloadable products.
*
* @param string $context View or edit context.
* @return bool
*/
public function get_downloadable( $context = 'view' ): bool {
return true;
}
/**
* Get the linked album ID.
*
* @return int
*/
public function get_linked_album_id(): int {
return (int) $this->get_meta( '_fedistream_linked_album', true );
}
/**
* Get the linked album post.
*
* @return \WP_Post|null
*/
public function get_linked_album(): ?\WP_Post {
$album_id = $this->get_linked_album_id();
if ( ! $album_id ) {
return null;
}
$album = get_post( $album_id );
if ( ! $album || 'fedistream_album' !== $album->post_type ) {
return null;
}
return $album;
}
/**
* Get the pricing type.
*
* @return string fixed, pwyw, or nyp
*/
public function get_pricing_type(): string {
return $this->get_meta( '_fedistream_pricing_type', true ) ?: 'fixed';
}
/**
* Get minimum price for PWYW.
*
* @return float
*/
public function get_min_price(): float {
return (float) $this->get_meta( '_fedistream_min_price', true );
}
/**
* Get suggested price for PWYW.
*
* @return float
*/
public function get_suggested_price(): float {
return (float) $this->get_meta( '_fedistream_suggested_price', true );
}
/**
* Check if streaming is included.
*
* @return bool
*/
public function includes_streaming(): bool {
return 'yes' === $this->get_meta( '_fedistream_include_streaming', true );
}
/**
* Get available download formats.
*
* @return array
*/
public function get_available_formats(): array {
$formats = $this->get_meta( '_fedistream_available_formats', true );
return is_array( $formats ) ? $formats : array( 'mp3' );
}
/**
* Get tracks in this album.
*
* @return array Array of WP_Post objects.
*/
public function get_tracks(): array {
$album_id = $this->get_linked_album_id();
if ( ! $album_id ) {
return array();
}
$tracks = get_posts(
array(
'post_type' => 'fedistream_track',
'post_status' => 'publish',
'posts_per_page' => -1,
'meta_key' => '_fedistream_track_number', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
'orderby' => 'meta_value_num',
'order' => 'ASC',
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
array(
'key' => '_fedistream_album_id',
'value' => $album_id,
),
),
)
);
return $tracks;
}
/**
* Get track count.
*
* @return int
*/
public function get_track_count(): int {
$album_id = $this->get_linked_album_id();
if ( ! $album_id ) {
return 0;
}
$count = get_post_meta( $album_id, '_fedistream_total_tracks', true );
if ( $count ) {
return (int) $count;
}
return count( $this->get_tracks() );
}
/**
* Get total duration in seconds.
*
* @return int
*/
public function get_total_duration(): int {
$album_id = $this->get_linked_album_id();
if ( ! $album_id ) {
return 0;
}
$duration = get_post_meta( $album_id, '_fedistream_total_duration', true );
if ( $duration ) {
return (int) $duration;
}
// Calculate from tracks.
$tracks = $this->get_tracks();
$duration = 0;
foreach ( $tracks as $track ) {
$duration += (int) get_post_meta( $track->ID, '_fedistream_duration', true );
}
return $duration;
}
/**
* Get formatted duration.
*
* @return string
*/
public function get_formatted_duration(): string {
$seconds = $this->get_total_duration();
if ( ! $seconds ) {
return '';
}
$hours = floor( $seconds / 3600 );
$mins = floor( ( $seconds % 3600 ) / 60 );
$secs = $seconds % 60;
if ( $hours > 0 ) {
return sprintf( '%d:%02d:%02d', $hours, $mins, $secs );
}
return sprintf( '%d:%02d', $mins, $secs );
}
/**
* Get artist name(s).
*
* @return string
*/
public function get_artist_name(): string {
$album_id = $this->get_linked_album_id();
if ( ! $album_id ) {
return '';
}
$artist_id = get_post_meta( $album_id, '_fedistream_album_artist', true );
if ( ! $artist_id ) {
return '';
}
$artist = get_post( $artist_id );
return $artist ? $artist->post_title : '';
}
/**
* Get album artwork URL.
*
* @param string $size Image size.
* @return string
*/
public function get_album_artwork( string $size = 'medium' ): string {
$album_id = $this->get_linked_album_id();
if ( ! $album_id ) {
return '';
}
$thumbnail_id = get_post_thumbnail_id( $album_id );
if ( ! $thumbnail_id ) {
return '';
}
$image = wp_get_attachment_image_url( $thumbnail_id, $size );
return $image ?: '';
}
/**
* Get release date.
*
* @return string
*/
public function get_release_date(): string {
$album_id = $this->get_linked_album_id();
if ( ! $album_id ) {
return '';
}
return get_post_meta( $album_id, '_fedistream_release_date', true ) ?: '';
}
/**
* Get album type (album, ep, single, compilation).
*
* @return string
*/
public function get_album_type(): string {
$album_id = $this->get_linked_album_id();
if ( ! $album_id ) {
return '';
}
return get_post_meta( $album_id, '_fedistream_album_type', true ) ?: 'album';
}
/**
* Get downloads for this product.
*
* Generates downloadable files based on available formats.
*
* @param string $context View or edit context.
* @return array
*/
public function get_downloads( $context = 'view' ): array {
$downloads = parent::get_downloads( $context );
// If no manual downloads set, generate from linked album.
if ( empty( $downloads ) && $this->get_linked_album_id() ) {
$downloads = $this->generate_album_downloads();
}
return $downloads;
}
/**
* Generate download files from linked album.
*
* @return array
*/
private function generate_album_downloads(): array {
$downloads = array();
$tracks = $this->get_tracks();
$formats = $this->get_available_formats();
$album = $this->get_linked_album();
if ( empty( $tracks ) || ! $album ) {
return $downloads;
}
// For each format, create a download entry.
foreach ( $formats as $format ) {
$format_label = strtoupper( $format );
// Create album ZIP download entry.
$download_id = 'album-' . $album->ID . '-' . $format;
$downloads[ $download_id ] = array(
'id' => $download_id,
'name' => sprintf(
/* translators: 1: Album name, 2: Format name */
__( '%1$s (%2$s)', 'wp-fedistream' ),
$album->post_title,
$format_label
),
'file' => add_query_arg(
array(
'fedistream_download' => 'album',
'album_id' => $album->ID,
'format' => $format,
),
home_url( '/' )
),
);
}
return $downloads;
}
/**
* Check if purchasable.
*
* @return bool
*/
public function is_purchasable(): bool {
// Must have a linked album.
if ( ! $this->get_linked_album_id() ) {
return false;
}
// Check price for fixed pricing.
if ( 'fixed' === $this->get_pricing_type() ) {
return $this->get_price() !== '' && $this->get_price() >= 0;
}
// PWYW and NYP are always purchasable.
return true;
}
/**
* Get price HTML.
*
* @param string $price Price HTML.
* @return string
*/
public function get_price_html( $price = '' ): string {
$pricing_type = $this->get_pricing_type();
if ( 'nyp' === $pricing_type ) {
return '<span class="fedistream-nyp-price">' . esc_html__( 'Name Your Price', 'wp-fedistream' ) . '</span>';
}
if ( 'pwyw' === $pricing_type ) {
$min_price = $this->get_min_price();
$suggested = $this->get_suggested_price();
$html = '<span class="fedistream-pwyw-price">';
if ( $min_price > 0 ) {
$html .= sprintf(
/* translators: %s: Minimum price */
esc_html__( 'From %s', 'wp-fedistream' ),
wc_price( $min_price )
);
} else {
$html .= esc_html__( 'Pay What You Want', 'wp-fedistream' );
}
if ( $suggested > 0 ) {
$html .= ' <span class="fedistream-suggested">';
$html .= sprintf(
/* translators: %s: Suggested price */
esc_html__( '(Suggested: %s)', 'wp-fedistream' ),
wc_price( $suggested )
);
$html .= '</span>';
}
$html .= '</span>';
return $html;
}
return parent::get_price_html( $price );
}
/**
* Add to cart validation for PWYW products.
*
* @param bool $passed Validation passed.
* @param int $product_id Product ID.
* @param int $quantity Quantity.
* @return bool
*/
public static function validate_add_to_cart( bool $passed, int $product_id, int $quantity ): bool {
$product = wc_get_product( $product_id );
if ( ! $product || 'fedistream_album' !== $product->get_type() ) {
return $passed;
}
$pricing_type = $product->get_pricing_type();
if ( 'pwyw' === $pricing_type || 'nyp' === $pricing_type ) {
// Check if custom price is set.
$custom_price = isset( $_POST['fedistream_custom_price'] ) ? // phpcs:ignore WordPress.Security.NonceVerification.Missing
wc_format_decimal( sanitize_text_field( wp_unslash( $_POST['fedistream_custom_price'] ) ) ) : // phpcs:ignore WordPress.Security.NonceVerification.Missing
0;
$min_price = $product->get_min_price();
if ( 'pwyw' === $pricing_type && $custom_price < $min_price ) {
wc_add_notice(
sprintf(
/* translators: %s: Minimum price */
__( 'Please enter at least %s', 'wp-fedistream' ),
wc_price( $min_price )
),
'error'
);
return false;
}
// Store custom price in session for cart.
WC()->session->set( 'fedistream_custom_price_' . $product_id, $custom_price );
}
return $passed;
}
}