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:
520
includes/WooCommerce/TrackProduct.php
Normal file
520
includes/WooCommerce/TrackProduct.php
Normal file
@@ -0,0 +1,520 @@
|
||||
<?php
|
||||
/**
|
||||
* Track Product Type for WooCommerce.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\WooCommerce;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* FediStream Track product type.
|
||||
*
|
||||
* Digital product representing a single FediStream track.
|
||||
*/
|
||||
class TrackProduct extends \WC_Product {
|
||||
|
||||
/**
|
||||
* Product type.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $product_type = 'fedistream_track';
|
||||
|
||||
/**
|
||||
* 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_track';
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks are virtual products.
|
||||
*
|
||||
* @param string $context View or edit context.
|
||||
* @return bool
|
||||
*/
|
||||
public function get_virtual( $context = 'view' ): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks are downloadable products.
|
||||
*
|
||||
* @param string $context View or edit context.
|
||||
* @return bool
|
||||
*/
|
||||
public function get_downloadable( $context = 'view' ): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the linked track ID.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_linked_track_id(): int {
|
||||
return (int) $this->get_meta( '_fedistream_linked_track', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the linked track post.
|
||||
*
|
||||
* @return \WP_Post|null
|
||||
*/
|
||||
public function get_linked_track(): ?\WP_Post {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$track = get_post( $track_id );
|
||||
|
||||
if ( ! $track || 'fedistream_track' !== $track->post_type ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $track;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 track duration in seconds.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_duration(): int {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) get_post_meta( $track_id, '_fedistream_duration', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatted duration.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_formatted_duration(): string {
|
||||
$seconds = $this->get_duration();
|
||||
|
||||
if ( ! $seconds ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$mins = floor( $seconds / 60 );
|
||||
$secs = $seconds % 60;
|
||||
|
||||
return sprintf( '%d:%02d', $mins, $secs );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get artist name(s).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_artist_name(): string {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$artist_ids = get_post_meta( $track_id, '_fedistream_artist_ids', true );
|
||||
|
||||
if ( ! is_array( $artist_ids ) || empty( $artist_ids ) ) {
|
||||
// Fall back to album artist.
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
$artist_id = $album_id ? get_post_meta( $album_id, '_fedistream_album_artist', true ) : 0;
|
||||
|
||||
if ( $artist_id ) {
|
||||
$artist = get_post( $artist_id );
|
||||
return $artist ? $artist->post_title : '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
$names = array();
|
||||
foreach ( $artist_ids as $artist_id ) {
|
||||
$artist = get_post( $artist_id );
|
||||
if ( $artist ) {
|
||||
$names[] = $artist->post_title;
|
||||
}
|
||||
}
|
||||
|
||||
return implode( ', ', $names );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get album name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_album_name(): string {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
|
||||
if ( ! $album_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$album = get_post( $album_id );
|
||||
|
||||
return $album ? $album->post_title : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track artwork URL.
|
||||
*
|
||||
* Falls back to album artwork if track has none.
|
||||
*
|
||||
* @param string $size Image size.
|
||||
* @return string
|
||||
*/
|
||||
public function get_track_artwork( string $size = 'medium' ): string {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Try track thumbnail first.
|
||||
$thumbnail_id = get_post_thumbnail_id( $track_id );
|
||||
|
||||
// Fall back to album artwork.
|
||||
if ( ! $thumbnail_id ) {
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
$thumbnail_id = $album_id ? get_post_thumbnail_id( $album_id ) : 0;
|
||||
}
|
||||
|
||||
if ( ! $thumbnail_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$image = wp_get_attachment_image_url( $thumbnail_id, $size );
|
||||
|
||||
return $image ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get audio file URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_audio_url(): string {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$audio_id = get_post_meta( $track_id, '_fedistream_audio_file', true );
|
||||
|
||||
if ( ! $audio_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return wp_get_attachment_url( $audio_id ) ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if track is explicit.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_explicit(): bool {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool) get_post_meta( $track_id, '_fedistream_explicit', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track BPM.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_bpm(): int {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) get_post_meta( $track_id, '_fedistream_bpm', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track musical key.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_musical_key(): string {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return get_post_meta( $track_id, '_fedistream_key', true ) ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ISRC code.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_isrc(): string {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return get_post_meta( $track_id, '_fedistream_isrc', true ) ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 track.
|
||||
if ( empty( $downloads ) && $this->get_linked_track_id() ) {
|
||||
$downloads = $this->generate_track_downloads();
|
||||
}
|
||||
|
||||
return $downloads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate download files from linked track.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function generate_track_downloads(): array {
|
||||
$downloads = array();
|
||||
$track = $this->get_linked_track();
|
||||
$formats = $this->get_available_formats();
|
||||
|
||||
if ( ! $track ) {
|
||||
return $downloads;
|
||||
}
|
||||
|
||||
// For each format, create a download entry.
|
||||
foreach ( $formats as $format ) {
|
||||
$format_label = strtoupper( $format );
|
||||
$download_id = 'track-' . $track->ID . '-' . $format;
|
||||
|
||||
$downloads[ $download_id ] = array(
|
||||
'id' => $download_id,
|
||||
'name' => sprintf(
|
||||
/* translators: 1: Track name, 2: Format name */
|
||||
__( '%1$s (%2$s)', 'wp-fedistream' ),
|
||||
$track->post_title,
|
||||
$format_label
|
||||
),
|
||||
'file' => add_query_arg(
|
||||
array(
|
||||
'fedistream_download' => 'track',
|
||||
'track_id' => $track->ID,
|
||||
'format' => $format,
|
||||
),
|
||||
home_url( '/' )
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return $downloads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if purchasable.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_purchasable(): bool {
|
||||
// Must have a linked track.
|
||||
if ( ! $this->get_linked_track_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_track' !== $product->get_type() ) {
|
||||
return $passed;
|
||||
}
|
||||
|
||||
$pricing_type = $product->get_pricing_type();
|
||||
|
||||
if ( 'pwyw' === $pricing_type || 'nyp' === $pricing_type ) {
|
||||
$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;
|
||||
}
|
||||
|
||||
WC()->session->set( 'fedistream_custom_price_' . $product_id, $custom_price );
|
||||
}
|
||||
|
||||
return $passed;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user