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:
499
includes/WooCommerce/AlbumProduct.php
Normal file
499
includes/WooCommerce/AlbumProduct.php
Normal file
@@ -0,0 +1,499 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
474
includes/WooCommerce/DigitalDelivery.php
Normal file
474
includes/WooCommerce/DigitalDelivery.php
Normal file
@@ -0,0 +1,474 @@
|
||||
<?php
|
||||
/**
|
||||
* Digital Delivery Handler for WooCommerce.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\WooCommerce;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles digital delivery of purchased music.
|
||||
*/
|
||||
class DigitalDelivery {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
// Handle download requests.
|
||||
add_action( 'init', array( $this, 'handle_download_request' ) );
|
||||
|
||||
// Add download links to order emails.
|
||||
add_action( 'woocommerce_email_after_order_table', array( $this, 'add_download_links_to_email' ), 10, 4 );
|
||||
|
||||
// Add download section to My Account.
|
||||
add_action( 'woocommerce_account_downloads_endpoint', array( $this, 'customize_downloads_display' ) );
|
||||
|
||||
// Generate secure download tokens.
|
||||
add_filter( 'woocommerce_download_file_force', array( $this, 'force_download_for_audio' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle download requests.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle_download_request(): void {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( ! isset( $_GET['fedistream_download'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$type = sanitize_text_field( wp_unslash( $_GET['fedistream_download'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
|
||||
// Verify user is logged in.
|
||||
if ( ! is_user_logged_in() ) {
|
||||
wp_die( esc_html__( 'You must be logged in to download files.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
if ( 'track' === $type ) {
|
||||
$this->handle_track_download( $user_id );
|
||||
} elseif ( 'album' === $type ) {
|
||||
$this->handle_album_download( $user_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle track download.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @return void
|
||||
*/
|
||||
private function handle_track_download( int $user_id ): void {
|
||||
$track_id = isset( $_GET['track_id'] ) ? absint( $_GET['track_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$format = isset( $_GET['format'] ) ? sanitize_text_field( wp_unslash( $_GET['format'] ) ) : 'mp3'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
|
||||
if ( ! $track_id ) {
|
||||
wp_die( esc_html__( 'Invalid track.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
// Verify purchase.
|
||||
if ( ! Integration::user_has_purchased( $user_id, 'track', $track_id ) ) {
|
||||
wp_die( esc_html__( 'You have not purchased this track.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
$track = get_post( $track_id );
|
||||
if ( ! $track || 'fedistream_track' !== $track->post_type ) {
|
||||
wp_die( esc_html__( 'Track not found.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
// Get audio file.
|
||||
$audio_id = get_post_meta( $track_id, '_fedistream_audio_file', true );
|
||||
if ( ! $audio_id ) {
|
||||
wp_die( esc_html__( 'No audio file available.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
$file_path = get_attached_file( $audio_id );
|
||||
if ( ! $file_path || ! file_exists( $file_path ) ) {
|
||||
wp_die( esc_html__( 'File not found.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
// Convert format if needed.
|
||||
$converted_file = $this->get_converted_file( $file_path, $format, $track_id );
|
||||
|
||||
// Generate filename.
|
||||
$filename = sanitize_file_name( $track->post_title ) . '.' . $format;
|
||||
|
||||
// Serve the file.
|
||||
$this->serve_file( $converted_file, $filename, $format );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle album download (ZIP of all tracks).
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @return void
|
||||
*/
|
||||
private function handle_album_download( int $user_id ): void {
|
||||
$album_id = isset( $_GET['album_id'] ) ? absint( $_GET['album_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$format = isset( $_GET['format'] ) ? sanitize_text_field( wp_unslash( $_GET['format'] ) ) : 'mp3'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
|
||||
if ( ! $album_id ) {
|
||||
wp_die( esc_html__( 'Invalid album.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
// Verify purchase.
|
||||
if ( ! Integration::user_has_purchased( $user_id, 'album', $album_id ) ) {
|
||||
wp_die( esc_html__( 'You have not purchased this album.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
$album = get_post( $album_id );
|
||||
if ( ! $album || 'fedistream_album' !== $album->post_type ) {
|
||||
wp_die( esc_html__( 'Album not found.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
// Get tracks.
|
||||
$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,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( empty( $tracks ) ) {
|
||||
wp_die( esc_html__( 'No tracks found in this album.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
// Create ZIP file.
|
||||
$zip_file = $this->create_album_zip( $album, $tracks, $format );
|
||||
|
||||
if ( ! $zip_file ) {
|
||||
wp_die( esc_html__( 'Failed to create download package.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
// Generate filename.
|
||||
$artist_id = get_post_meta( $album_id, '_fedistream_album_artist', true );
|
||||
$artist = $artist_id ? get_post( $artist_id ) : null;
|
||||
$artist_name = $artist ? $artist->post_title : 'Unknown Artist';
|
||||
|
||||
$filename = sanitize_file_name( $artist_name . ' - ' . $album->post_title ) . '.' . strtoupper( $format ) . '.zip';
|
||||
|
||||
// Serve the file.
|
||||
$this->serve_file( $zip_file, $filename, 'zip' );
|
||||
|
||||
// Clean up temp file.
|
||||
wp_delete_file( $zip_file );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get converted file path.
|
||||
*
|
||||
* @param string $source_path Source file path.
|
||||
* @param string $format Target format.
|
||||
* @param int $track_id Track ID for caching.
|
||||
* @return string Converted file path.
|
||||
*/
|
||||
private function get_converted_file( string $source_path, string $format, int $track_id ): string {
|
||||
$source_ext = strtolower( pathinfo( $source_path, PATHINFO_EXTENSION ) );
|
||||
|
||||
// If same format, return source.
|
||||
if ( $source_ext === $format ) {
|
||||
return $source_path;
|
||||
}
|
||||
|
||||
// Check for cached conversion.
|
||||
$cache_dir = wp_upload_dir()['basedir'] . '/fedistream-cache/';
|
||||
$cache_file = $cache_dir . 'track-' . $track_id . '.' . $format;
|
||||
|
||||
if ( file_exists( $cache_file ) ) {
|
||||
return $cache_file;
|
||||
}
|
||||
|
||||
// Create cache directory.
|
||||
if ( ! file_exists( $cache_dir ) ) {
|
||||
wp_mkdir_p( $cache_dir );
|
||||
}
|
||||
|
||||
// For now, return source file (format conversion would require FFmpeg).
|
||||
// In production, you'd use FFmpeg or similar for conversion.
|
||||
// This is a placeholder for the conversion logic.
|
||||
return $source_path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ZIP file for album download.
|
||||
*
|
||||
* @param \WP_Post $album Album post.
|
||||
* @param array $tracks Track posts.
|
||||
* @param string $format Audio format.
|
||||
* @return string|null ZIP file path or null on failure.
|
||||
*/
|
||||
private function create_album_zip( \WP_Post $album, array $tracks, string $format ): ?string {
|
||||
if ( ! class_exists( 'ZipArchive' ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$temp_dir = get_temp_dir();
|
||||
$zip_path = $temp_dir . 'fedistream-album-' . $album->ID . '-' . time() . '.zip';
|
||||
|
||||
$zip = new \ZipArchive();
|
||||
if ( $zip->open( $zip_path, \ZipArchive::CREATE ) !== true ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$track_number = 0;
|
||||
foreach ( $tracks as $track ) {
|
||||
++$track_number;
|
||||
|
||||
$audio_id = get_post_meta( $track->ID, '_fedistream_audio_file', true );
|
||||
if ( ! $audio_id ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$file_path = get_attached_file( $audio_id );
|
||||
if ( ! $file_path || ! file_exists( $file_path ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get converted file.
|
||||
$converted_file = $this->get_converted_file( $file_path, $format, $track->ID );
|
||||
|
||||
// Create filename with track number.
|
||||
$filename = sprintf(
|
||||
'%02d - %s.%s',
|
||||
$track_number,
|
||||
sanitize_file_name( $track->post_title ),
|
||||
$format
|
||||
);
|
||||
|
||||
$zip->addFile( $converted_file, $filename );
|
||||
}
|
||||
|
||||
// Add cover art if available.
|
||||
$thumbnail_id = get_post_thumbnail_id( $album->ID );
|
||||
if ( $thumbnail_id ) {
|
||||
$cover_path = get_attached_file( $thumbnail_id );
|
||||
if ( $cover_path && file_exists( $cover_path ) ) {
|
||||
$cover_ext = pathinfo( $cover_path, PATHINFO_EXTENSION );
|
||||
$zip->addFile( $cover_path, 'cover.' . $cover_ext );
|
||||
}
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
|
||||
return file_exists( $zip_path ) ? $zip_path : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve a file for download.
|
||||
*
|
||||
* @param string $file_path File path.
|
||||
* @param string $filename Download filename.
|
||||
* @param string $format File format.
|
||||
* @return void
|
||||
*/
|
||||
private function serve_file( string $file_path, string $filename, string $format ): void {
|
||||
if ( ! file_exists( $file_path ) ) {
|
||||
wp_die( esc_html__( 'File not found.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
// Get MIME type.
|
||||
$mime_types = array(
|
||||
'mp3' => 'audio/mpeg',
|
||||
'flac' => 'audio/flac',
|
||||
'wav' => 'audio/wav',
|
||||
'ogg' => 'audio/ogg',
|
||||
'aac' => 'audio/aac',
|
||||
'zip' => 'application/zip',
|
||||
);
|
||||
|
||||
$mime_type = $mime_types[ $format ] ?? 'application/octet-stream';
|
||||
|
||||
// Clean output buffer.
|
||||
while ( ob_get_level() ) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
// Set headers.
|
||||
nocache_headers();
|
||||
header( 'Content-Type: ' . $mime_type );
|
||||
header( 'Content-Description: File Transfer' );
|
||||
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
|
||||
header( 'Content-Transfer-Encoding: binary' );
|
||||
header( 'Content-Length: ' . filesize( $file_path ) );
|
||||
|
||||
// Read file.
|
||||
readfile( $file_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add download links to order confirmation email.
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
* @param bool $sent_to_admin Whether sent to admin.
|
||||
* @param bool $plain_text Whether plain text.
|
||||
* @param object $email Email object.
|
||||
* @return void
|
||||
*/
|
||||
public function add_download_links_to_email( \WC_Order $order, bool $sent_to_admin, bool $plain_text, $email ): void {
|
||||
if ( $sent_to_admin || 'completed' !== $order->get_status() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$has_fedistream = false;
|
||||
foreach ( $order->get_items() as $item ) {
|
||||
$product_type = \WC_Product_Factory::get_product_type( $item->get_product_id() );
|
||||
if ( in_array( $product_type, array( 'fedistream_album', 'fedistream_track' ), true ) ) {
|
||||
$has_fedistream = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $has_fedistream ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$downloads_url = wc_get_account_endpoint_url( 'downloads' );
|
||||
|
||||
if ( $plain_text ) {
|
||||
echo "\n\n";
|
||||
echo esc_html__( 'Your FediStream Downloads', 'wp-fedistream' ) . "\n";
|
||||
echo esc_html__( 'Access your purchased music at:', 'wp-fedistream' ) . ' ' . esc_url( $downloads_url ) . "\n";
|
||||
} else {
|
||||
?>
|
||||
<h2><?php esc_html_e( 'Your FediStream Downloads', 'wp-fedistream' ); ?></h2>
|
||||
<p>
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: Downloads URL */
|
||||
esc_html__( 'Access your purchased music in your %s.', 'wp-fedistream' ),
|
||||
'<a href="' . esc_url( $downloads_url ) . '">' . esc_html__( 'account downloads', 'wp-fedistream' ) . '</a>'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Customize downloads display in My Account.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function customize_downloads_display(): void {
|
||||
if ( ! is_user_logged_in() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$purchases = $this->get_user_purchases( $user_id );
|
||||
|
||||
if ( empty( $purchases ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
?>
|
||||
<h3><?php esc_html_e( 'FediStream Library', 'wp-fedistream' ); ?></h3>
|
||||
<table class="woocommerce-table shop_table shop_table_responsive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Title', 'wp-fedistream' ); ?></th>
|
||||
<th><?php esc_html_e( 'Type', 'wp-fedistream' ); ?></th>
|
||||
<th><?php esc_html_e( 'Purchased', 'wp-fedistream' ); ?></th>
|
||||
<th><?php esc_html_e( 'Download', 'wp-fedistream' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ( $purchases as $purchase ) : ?>
|
||||
<?php
|
||||
$content = get_post( $purchase->content_id );
|
||||
if ( ! $content ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$formats = array( 'mp3', 'flac' ); // Default formats.
|
||||
?>
|
||||
<tr>
|
||||
<td><?php echo esc_html( $content->post_title ); ?></td>
|
||||
<td><?php echo esc_html( ucfirst( $purchase->content_type ) ); ?></td>
|
||||
<td><?php echo esc_html( date_i18n( get_option( 'date_format' ), strtotime( $purchase->purchased_at ) ) ); ?></td>
|
||||
<td>
|
||||
<?php foreach ( $formats as $format ) : ?>
|
||||
<?php
|
||||
$download_url = add_query_arg(
|
||||
array(
|
||||
'fedistream_download' => $purchase->content_type,
|
||||
$purchase->content_type . '_id' => $purchase->content_id,
|
||||
'format' => $format,
|
||||
),
|
||||
home_url( '/' )
|
||||
);
|
||||
?>
|
||||
<a href="<?php echo esc_url( $download_url ); ?>" class="button button-small">
|
||||
<?php echo esc_html( strtoupper( $format ) ); ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's purchases.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @return array
|
||||
*/
|
||||
private function get_user_purchases( int $user_id ): array {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_purchases';
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$purchases = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$table} WHERE user_id = %d ORDER BY purchased_at DESC",
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
|
||||
return $purchases ?: array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Force download for audio files.
|
||||
*
|
||||
* @param bool $force Force download.
|
||||
* @param string $file_path File path.
|
||||
* @return bool
|
||||
*/
|
||||
public function force_download_for_audio( bool $force, string $file_path ): bool {
|
||||
$ext = strtolower( pathinfo( $file_path, PATHINFO_EXTENSION ) );
|
||||
|
||||
$audio_extensions = array( 'mp3', 'wav', 'flac', 'ogg', 'aac', 'm4a' );
|
||||
|
||||
if ( in_array( $ext, $audio_extensions, true ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $force;
|
||||
}
|
||||
}
|
||||
738
includes/WooCommerce/Integration.php
Normal file
738
includes/WooCommerce/Integration.php
Normal file
@@ -0,0 +1,738 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Integration.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\WooCommerce;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main WooCommerce integration class.
|
||||
*/
|
||||
class Integration {
|
||||
|
||||
/**
|
||||
* Whether WooCommerce is active.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private bool $woocommerce_active = false;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'plugins_loaded', array( $this, 'check_woocommerce' ), 5 );
|
||||
add_action( 'plugins_loaded', array( $this, 'init' ), 20 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if WooCommerce is active.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function check_woocommerce(): void {
|
||||
$this->woocommerce_active = class_exists( 'WooCommerce' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize WooCommerce integration.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init(): void {
|
||||
if ( ! $this->woocommerce_active ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Register custom product types.
|
||||
add_filter( 'product_type_selector', array( $this, 'add_product_types' ) );
|
||||
add_filter( 'woocommerce_product_class', array( $this, 'product_class' ), 10, 2 );
|
||||
|
||||
// Initialize product type classes.
|
||||
add_action( 'init', array( $this, 'register_product_types' ), 5 );
|
||||
|
||||
// Add product data tabs.
|
||||
add_filter( 'woocommerce_product_data_tabs', array( $this, 'add_product_data_tabs' ) );
|
||||
add_action( 'woocommerce_product_data_panels', array( $this, 'add_product_data_panels' ) );
|
||||
|
||||
// Save product meta.
|
||||
add_action( 'woocommerce_process_product_meta', array( $this, 'save_product_meta' ) );
|
||||
|
||||
// Frontend hooks.
|
||||
add_action( 'woocommerce_single_product_summary', array( $this, 'display_track_preview' ), 25 );
|
||||
|
||||
// Purchase access hooks.
|
||||
add_action( 'woocommerce_order_status_completed', array( $this, 'grant_access_on_purchase' ) );
|
||||
|
||||
// Download hooks.
|
||||
add_filter( 'woocommerce_downloadable_file_allowed_mime_types', array( $this, 'allowed_audio_mimes' ) );
|
||||
|
||||
// Admin columns.
|
||||
add_filter( 'manage_edit-product_columns', array( $this, 'add_product_columns' ) );
|
||||
add_action( 'manage_product_posts_custom_column', array( $this, 'render_product_columns' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if WooCommerce is active.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_active(): bool {
|
||||
return $this->woocommerce_active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register custom product types.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_product_types(): void {
|
||||
// Product types are registered via class loading.
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom product types to the selector.
|
||||
*
|
||||
* @param array $types Product types.
|
||||
* @return array Modified product types.
|
||||
*/
|
||||
public function add_product_types( array $types ): array {
|
||||
$types['fedistream_album'] = __( 'FediStream Album', 'wp-fedistream' );
|
||||
$types['fedistream_track'] = __( 'FediStream Track', 'wp-fedistream' );
|
||||
|
||||
return $types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product class for custom types.
|
||||
*
|
||||
* @param string $classname Product class name.
|
||||
* @param string $product_type Product type.
|
||||
* @return string Modified class name.
|
||||
*/
|
||||
public function product_class( string $classname, string $product_type ): string {
|
||||
if ( 'fedistream_album' === $product_type ) {
|
||||
return AlbumProduct::class;
|
||||
}
|
||||
|
||||
if ( 'fedistream_track' === $product_type ) {
|
||||
return TrackProduct::class;
|
||||
}
|
||||
|
||||
return $classname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add product data tabs.
|
||||
*
|
||||
* @param array $tabs Product data tabs.
|
||||
* @return array Modified tabs.
|
||||
*/
|
||||
public function add_product_data_tabs( array $tabs ): array {
|
||||
$tabs['fedistream'] = array(
|
||||
'label' => __( 'FediStream', 'wp-fedistream' ),
|
||||
'target' => 'fedistream_product_data',
|
||||
'class' => array( 'show_if_fedistream_album', 'show_if_fedistream_track' ),
|
||||
'priority' => 21,
|
||||
);
|
||||
|
||||
$tabs['fedistream_formats'] = array(
|
||||
'label' => __( 'Audio Formats', 'wp-fedistream' ),
|
||||
'target' => 'fedistream_formats_data',
|
||||
'class' => array( 'show_if_fedistream_album', 'show_if_fedistream_track' ),
|
||||
'priority' => 22,
|
||||
);
|
||||
|
||||
return $tabs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add product data panels.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_product_data_panels(): void {
|
||||
global $post;
|
||||
|
||||
$product_id = $post->ID;
|
||||
|
||||
// Get linked content.
|
||||
$linked_album = get_post_meta( $product_id, '_fedistream_linked_album', true );
|
||||
$linked_track = get_post_meta( $product_id, '_fedistream_linked_track', true );
|
||||
|
||||
// Get pricing options.
|
||||
$pricing_type = get_post_meta( $product_id, '_fedistream_pricing_type', true ) ?: 'fixed';
|
||||
$min_price = get_post_meta( $product_id, '_fedistream_min_price', true );
|
||||
$suggested_price = get_post_meta( $product_id, '_fedistream_suggested_price', true );
|
||||
|
||||
// Get format options.
|
||||
$available_formats = get_post_meta( $product_id, '_fedistream_available_formats', true ) ?: array( 'mp3' );
|
||||
$include_streaming = get_post_meta( $product_id, '_fedistream_include_streaming', true );
|
||||
|
||||
// Get albums for dropdown.
|
||||
$albums = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_album',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
|
||||
// Get tracks for dropdown.
|
||||
$tracks = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_track',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
|
||||
?>
|
||||
<div id="fedistream_product_data" class="panel woocommerce_options_panel">
|
||||
<div class="options_group show_if_fedistream_album">
|
||||
<p class="form-field">
|
||||
<label for="_fedistream_linked_album"><?php esc_html_e( 'Linked Album', 'wp-fedistream' ); ?></label>
|
||||
<select id="_fedistream_linked_album" name="_fedistream_linked_album" class="wc-enhanced-select" style="width: 50%;">
|
||||
<option value=""><?php esc_html_e( 'Select an album...', 'wp-fedistream' ); ?></option>
|
||||
<?php foreach ( $albums as $album ) : ?>
|
||||
<option value="<?php echo esc_attr( $album->ID ); ?>" <?php selected( $linked_album, $album->ID ); ?>>
|
||||
<?php echo esc_html( $album->post_title ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php echo wc_help_tip( __( 'Select the FediStream album this product represents.', 'wp-fedistream' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="options_group show_if_fedistream_track">
|
||||
<p class="form-field">
|
||||
<label for="_fedistream_linked_track"><?php esc_html_e( 'Linked Track', 'wp-fedistream' ); ?></label>
|
||||
<select id="_fedistream_linked_track" name="_fedistream_linked_track" class="wc-enhanced-select" style="width: 50%;">
|
||||
<option value=""><?php esc_html_e( 'Select a track...', 'wp-fedistream' ); ?></option>
|
||||
<?php foreach ( $tracks as $track ) : ?>
|
||||
<option value="<?php echo esc_attr( $track->ID ); ?>" <?php selected( $linked_track, $track->ID ); ?>>
|
||||
<?php echo esc_html( $track->post_title ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php echo wc_help_tip( __( 'Select the FediStream track this product represents.', 'wp-fedistream' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="options_group">
|
||||
<p class="form-field">
|
||||
<label for="_fedistream_pricing_type"><?php esc_html_e( 'Pricing Type', 'wp-fedistream' ); ?></label>
|
||||
<select id="_fedistream_pricing_type" name="_fedistream_pricing_type" class="wc-enhanced-select" style="width: 50%;">
|
||||
<option value="fixed" <?php selected( $pricing_type, 'fixed' ); ?>><?php esc_html_e( 'Fixed Price', 'wp-fedistream' ); ?></option>
|
||||
<option value="pwyw" <?php selected( $pricing_type, 'pwyw' ); ?>><?php esc_html_e( 'Pay What You Want', 'wp-fedistream' ); ?></option>
|
||||
<option value="nyp" <?php selected( $pricing_type, 'nyp' ); ?>><?php esc_html_e( 'Name Your Price (Free+)', 'wp-fedistream' ); ?></option>
|
||||
</select>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="options_group fedistream-pwyw-options">
|
||||
<?php
|
||||
woocommerce_wp_text_input(
|
||||
array(
|
||||
'id' => '_fedistream_min_price',
|
||||
'label' => __( 'Minimum Price', 'wp-fedistream' ) . ' (' . get_woocommerce_currency_symbol() . ')',
|
||||
'desc_tip' => true,
|
||||
'description' => __( 'Minimum price for Pay What You Want. Leave empty for no minimum.', 'wp-fedistream' ),
|
||||
'type' => 'text',
|
||||
'data_type' => 'price',
|
||||
'value' => $min_price,
|
||||
)
|
||||
);
|
||||
|
||||
woocommerce_wp_text_input(
|
||||
array(
|
||||
'id' => '_fedistream_suggested_price',
|
||||
'label' => __( 'Suggested Price', 'wp-fedistream' ) . ' (' . get_woocommerce_currency_symbol() . ')',
|
||||
'desc_tip' => true,
|
||||
'description' => __( 'Suggested price shown to customers.', 'wp-fedistream' ),
|
||||
'type' => 'text',
|
||||
'data_type' => 'price',
|
||||
'value' => $suggested_price,
|
||||
)
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
|
||||
<div class="options_group">
|
||||
<?php
|
||||
woocommerce_wp_checkbox(
|
||||
array(
|
||||
'id' => '_fedistream_include_streaming',
|
||||
'label' => __( 'Include Streaming', 'wp-fedistream' ),
|
||||
'description' => __( 'Purchase unlocks full-quality streaming access.', 'wp-fedistream' ),
|
||||
'value' => $include_streaming,
|
||||
)
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="fedistream_formats_data" class="panel woocommerce_options_panel">
|
||||
<div class="options_group">
|
||||
<p class="form-field">
|
||||
<label><?php esc_html_e( 'Available Formats', 'wp-fedistream' ); ?></label>
|
||||
<span class="fedistream-format-checkboxes">
|
||||
<?php
|
||||
$formats = array(
|
||||
'mp3' => 'MP3 (320kbps)',
|
||||
'flac' => 'FLAC (Lossless)',
|
||||
'wav' => 'WAV (Uncompressed)',
|
||||
'aac' => 'AAC (256kbps)',
|
||||
'ogg' => 'OGG Vorbis',
|
||||
);
|
||||
|
||||
foreach ( $formats as $format => $label ) :
|
||||
$checked = is_array( $available_formats ) && in_array( $format, $available_formats, true );
|
||||
?>
|
||||
<label style="display: block; margin-bottom: 5px;">
|
||||
<input type="checkbox" name="_fedistream_available_formats[]" value="<?php echo esc_attr( $format ); ?>" <?php checked( $checked ); ?>>
|
||||
<?php echo esc_html( $label ); ?>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</span>
|
||||
</p>
|
||||
<p class="description" style="margin-left: 150px;">
|
||||
<?php esc_html_e( 'Select which audio formats customers can download after purchase.', 'wp-fedistream' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
jQuery(function($) {
|
||||
function togglePricingOptions() {
|
||||
var type = $('#_fedistream_pricing_type').val();
|
||||
if (type === 'pwyw' || type === 'nyp') {
|
||||
$('.fedistream-pwyw-options').show();
|
||||
} else {
|
||||
$('.fedistream-pwyw-options').hide();
|
||||
}
|
||||
}
|
||||
|
||||
$('#_fedistream_pricing_type').on('change', togglePricingOptions);
|
||||
togglePricingOptions();
|
||||
|
||||
// Show/hide tabs based on product type.
|
||||
$('input#_virtual, input#_downloadable').on('change', function() {
|
||||
var type = $('select#product-type').val();
|
||||
if (type === 'fedistream_album' || type === 'fedistream_track') {
|
||||
$('input#_virtual').prop('checked', true);
|
||||
$('input#_downloadable').prop('checked', true);
|
||||
}
|
||||
});
|
||||
|
||||
$('select#product-type').on('change', function() {
|
||||
var type = $(this).val();
|
||||
if (type === 'fedistream_album' || type === 'fedistream_track') {
|
||||
$('input#_virtual').prop('checked', true).trigger('change');
|
||||
$('input#_downloadable').prop('checked', true).trigger('change');
|
||||
}
|
||||
}).trigger('change');
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save product meta.
|
||||
*
|
||||
* @param int $product_id Product ID.
|
||||
* @return void
|
||||
*/
|
||||
public function save_product_meta( int $product_id ): void {
|
||||
// Linked content.
|
||||
if ( isset( $_POST['_fedistream_linked_album'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
update_post_meta( $product_id, '_fedistream_linked_album', absint( $_POST['_fedistream_linked_album'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
}
|
||||
|
||||
if ( isset( $_POST['_fedistream_linked_track'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
update_post_meta( $product_id, '_fedistream_linked_track', absint( $_POST['_fedistream_linked_track'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
}
|
||||
|
||||
// Pricing options.
|
||||
if ( isset( $_POST['_fedistream_pricing_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
update_post_meta( $product_id, '_fedistream_pricing_type', sanitize_text_field( wp_unslash( $_POST['_fedistream_pricing_type'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
}
|
||||
|
||||
if ( isset( $_POST['_fedistream_min_price'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
update_post_meta( $product_id, '_fedistream_min_price', wc_format_decimal( sanitize_text_field( wp_unslash( $_POST['_fedistream_min_price'] ) ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
}
|
||||
|
||||
if ( isset( $_POST['_fedistream_suggested_price'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
update_post_meta( $product_id, '_fedistream_suggested_price', wc_format_decimal( sanitize_text_field( wp_unslash( $_POST['_fedistream_suggested_price'] ) ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
}
|
||||
|
||||
// Streaming access.
|
||||
$include_streaming = isset( $_POST['_fedistream_include_streaming'] ) ? 'yes' : 'no'; // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
update_post_meta( $product_id, '_fedistream_include_streaming', $include_streaming );
|
||||
|
||||
// Available formats.
|
||||
if ( isset( $_POST['_fedistream_available_formats'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
$formats = array_map( 'sanitize_text_field', wp_unslash( $_POST['_fedistream_available_formats'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
update_post_meta( $product_id, '_fedistream_available_formats', $formats );
|
||||
} else {
|
||||
update_post_meta( $product_id, '_fedistream_available_formats', array() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display track preview on product page.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function display_track_preview(): void {
|
||||
global $product;
|
||||
|
||||
if ( ! $product ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$product_type = $product->get_type();
|
||||
|
||||
if ( 'fedistream_track' === $product_type ) {
|
||||
$track_id = get_post_meta( $product->get_id(), '_fedistream_linked_track', true );
|
||||
if ( $track_id ) {
|
||||
$this->render_track_preview( $track_id );
|
||||
}
|
||||
} elseif ( 'fedistream_album' === $product_type ) {
|
||||
$album_id = get_post_meta( $product->get_id(), '_fedistream_linked_album', true );
|
||||
if ( $album_id ) {
|
||||
$this->render_album_preview( $album_id );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render track preview player.
|
||||
*
|
||||
* @param int $track_id Track ID.
|
||||
* @return void
|
||||
*/
|
||||
private function render_track_preview( int $track_id ): void {
|
||||
$audio_id = get_post_meta( $track_id, '_fedistream_audio_file', true );
|
||||
$audio_url = $audio_id ? wp_get_attachment_url( $audio_id ) : '';
|
||||
|
||||
if ( ! $audio_url ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$duration = get_post_meta( $track_id, '_fedistream_duration', true );
|
||||
|
||||
?>
|
||||
<div class="fedistream-product-preview">
|
||||
<h4><?php esc_html_e( 'Preview', 'wp-fedistream' ); ?></h4>
|
||||
<div class="fedistream-mini-player" data-track-id="<?php echo esc_attr( $track_id ); ?>">
|
||||
<button class="fedistream-preview-play" type="button" aria-label="<?php esc_attr_e( 'Play preview', 'wp-fedistream' ); ?>">
|
||||
<span class="dashicons dashicons-controls-play"></span>
|
||||
</button>
|
||||
<div class="fedistream-preview-progress">
|
||||
<div class="fedistream-preview-progress-bar"></div>
|
||||
</div>
|
||||
<?php if ( $duration ) : ?>
|
||||
<span class="fedistream-preview-duration"><?php echo esc_html( gmdate( 'i:s', $duration ) ); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render album preview tracklist.
|
||||
*
|
||||
* @param int $album_id Album ID.
|
||||
* @return void
|
||||
*/
|
||||
private function render_album_preview( int $album_id ): void {
|
||||
$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,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( empty( $tracks ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
?>
|
||||
<div class="fedistream-product-tracklist">
|
||||
<h4><?php esc_html_e( 'Tracklist', 'wp-fedistream' ); ?></h4>
|
||||
<ol class="fedistream-album-tracks">
|
||||
<?php foreach ( $tracks as $track ) : ?>
|
||||
<?php
|
||||
$duration = get_post_meta( $track->ID, '_fedistream_duration', true );
|
||||
?>
|
||||
<li class="fedistream-album-track" data-track-id="<?php echo esc_attr( $track->ID ); ?>">
|
||||
<span class="fedistream-track-title"><?php echo esc_html( $track->post_title ); ?></span>
|
||||
<?php if ( $duration ) : ?>
|
||||
<span class="fedistream-track-duration"><?php echo esc_html( gmdate( 'i:s', $duration ) ); ?></span>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ol>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant streaming/download access on purchase completion.
|
||||
*
|
||||
* @param int $order_id Order ID.
|
||||
* @return void
|
||||
*/
|
||||
public function grant_access_on_purchase( int $order_id ): void {
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
if ( ! $order ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$customer_id = $order->get_customer_id();
|
||||
if ( ! $customer_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $order->get_items() as $item ) {
|
||||
$product_id = $item->get_product_id();
|
||||
$product_type = \WC_Product_Factory::get_product_type( $product_id );
|
||||
|
||||
if ( 'fedistream_album' === $product_type ) {
|
||||
$album_id = get_post_meta( $product_id, '_fedistream_linked_album', true );
|
||||
if ( $album_id ) {
|
||||
$this->grant_album_access( $customer_id, $album_id, $order_id );
|
||||
}
|
||||
} elseif ( 'fedistream_track' === $product_type ) {
|
||||
$track_id = get_post_meta( $product_id, '_fedistream_linked_track', true );
|
||||
if ( $track_id ) {
|
||||
$this->grant_track_access( $customer_id, $track_id, $order_id );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant album access to a customer.
|
||||
*
|
||||
* @param int $customer_id Customer ID.
|
||||
* @param int $album_id Album ID.
|
||||
* @param int $order_id Order ID.
|
||||
* @return void
|
||||
*/
|
||||
private function grant_album_access( int $customer_id, int $album_id, int $order_id ): void {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_purchases';
|
||||
|
||||
// Check if access already exists.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$exists = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM {$table} WHERE user_id = %d AND content_type = 'album' AND content_id = %d",
|
||||
$customer_id,
|
||||
$album_id
|
||||
)
|
||||
);
|
||||
|
||||
if ( $exists ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$wpdb->insert(
|
||||
$table,
|
||||
array(
|
||||
'user_id' => $customer_id,
|
||||
'content_type' => 'album',
|
||||
'content_id' => $album_id,
|
||||
'order_id' => $order_id,
|
||||
'purchased_at' => current_time( 'mysql' ),
|
||||
),
|
||||
array( '%d', '%s', '%d', '%d', '%s' )
|
||||
);
|
||||
|
||||
// Also grant access to all tracks in the album.
|
||||
$tracks = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_track',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
array(
|
||||
'key' => '_fedistream_album_id',
|
||||
'value' => $album_id,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $tracks as $track ) {
|
||||
$this->grant_track_access( $customer_id, $track->ID, $order_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant track access to a customer.
|
||||
*
|
||||
* @param int $customer_id Customer ID.
|
||||
* @param int $track_id Track ID.
|
||||
* @param int $order_id Order ID.
|
||||
* @return void
|
||||
*/
|
||||
private function grant_track_access( int $customer_id, int $track_id, int $order_id ): void {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_purchases';
|
||||
|
||||
// Check if access already exists.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$exists = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM {$table} WHERE user_id = %d AND content_type = 'track' AND content_id = %d",
|
||||
$customer_id,
|
||||
$track_id
|
||||
)
|
||||
);
|
||||
|
||||
if ( $exists ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$wpdb->insert(
|
||||
$table,
|
||||
array(
|
||||
'user_id' => $customer_id,
|
||||
'content_type' => 'track',
|
||||
'content_id' => $track_id,
|
||||
'order_id' => $order_id,
|
||||
'purchased_at' => current_time( 'mysql' ),
|
||||
),
|
||||
array( '%d', '%s', '%d', '%d', '%s' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has purchased content.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @param string $content_type Content type (album or track).
|
||||
* @param int $content_id Content ID.
|
||||
* @return bool
|
||||
*/
|
||||
public static function user_has_purchased( int $user_id, string $content_type, int $content_id ): bool {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_purchases';
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$exists = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM {$table} WHERE user_id = %d AND content_type = %s AND content_id = %d",
|
||||
$user_id,
|
||||
$content_type,
|
||||
$content_id
|
||||
)
|
||||
);
|
||||
|
||||
return (bool) $exists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add allowed audio MIME types.
|
||||
*
|
||||
* @param array $types Allowed MIME types.
|
||||
* @return array Modified MIME types.
|
||||
*/
|
||||
public function allowed_audio_mimes( array $types ): array {
|
||||
$types['flac'] = 'audio/flac';
|
||||
$types['wav'] = 'audio/wav';
|
||||
$types['ogg'] = 'audio/ogg';
|
||||
$types['aac'] = 'audio/aac';
|
||||
|
||||
return $types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add product columns.
|
||||
*
|
||||
* @param array $columns Columns.
|
||||
* @return array Modified columns.
|
||||
*/
|
||||
public function add_product_columns( array $columns ): array {
|
||||
$new_columns = array();
|
||||
|
||||
foreach ( $columns as $key => $value ) {
|
||||
$new_columns[ $key ] = $value;
|
||||
|
||||
if ( 'product_type' === $key ) {
|
||||
$new_columns['fedistream_linked'] = __( 'FediStream', 'wp-fedistream' );
|
||||
}
|
||||
}
|
||||
|
||||
return $new_columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render product columns.
|
||||
*
|
||||
* @param string $column Column name.
|
||||
* @param int $post_id Post ID.
|
||||
* @return void
|
||||
*/
|
||||
public function render_product_columns( string $column, int $post_id ): void {
|
||||
if ( 'fedistream_linked' !== $column ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$product_type = \WC_Product_Factory::get_product_type( $post_id );
|
||||
|
||||
if ( 'fedistream_album' === $product_type ) {
|
||||
$album_id = get_post_meta( $post_id, '_fedistream_linked_album', true );
|
||||
if ( $album_id ) {
|
||||
$album = get_post( $album_id );
|
||||
if ( $album ) {
|
||||
echo '<a href="' . esc_url( get_edit_post_link( $album_id ) ) . '">' . esc_html( $album->post_title ) . '</a>';
|
||||
}
|
||||
} else {
|
||||
echo '<span class="na">–</span>';
|
||||
}
|
||||
} elseif ( 'fedistream_track' === $product_type ) {
|
||||
$track_id = get_post_meta( $post_id, '_fedistream_linked_track', true );
|
||||
if ( $track_id ) {
|
||||
$track = get_post( $track_id );
|
||||
if ( $track ) {
|
||||
echo '<a href="' . esc_url( get_edit_post_link( $track_id ) ) . '">' . esc_html( $track->post_title ) . '</a>';
|
||||
}
|
||||
} else {
|
||||
echo '<span class="na">–</span>';
|
||||
}
|
||||
} else {
|
||||
echo '<span class="na">–</span>';
|
||||
}
|
||||
}
|
||||
}
|
||||
416
includes/WooCommerce/StreamingAccess.php
Normal file
416
includes/WooCommerce/StreamingAccess.php
Normal file
@@ -0,0 +1,416 @@
|
||||
<?php
|
||||
/**
|
||||
* Streaming Access Control for WooCommerce.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\WooCommerce;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Controls streaming access based on purchases.
|
||||
*/
|
||||
class StreamingAccess {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
// Filter audio URL access.
|
||||
add_filter( 'fedistream_can_stream_track', array( $this, 'can_stream_track' ), 10, 3 );
|
||||
|
||||
// Add streaming access check to AJAX handler.
|
||||
add_filter( 'fedistream_track_data', array( $this, 'filter_track_data' ), 10, 2 );
|
||||
|
||||
// Handle preview access.
|
||||
add_action( 'init', array( $this, 'handle_preview_request' ) );
|
||||
|
||||
// Add purchase buttons to track display.
|
||||
add_action( 'fedistream_after_track_player', array( $this, 'add_purchase_button' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user can stream a track.
|
||||
*
|
||||
* @param bool $can_stream Default access (true).
|
||||
* @param int $track_id Track ID.
|
||||
* @param int $user_id User ID (0 for guests).
|
||||
* @return bool
|
||||
*/
|
||||
public function can_stream_track( bool $can_stream, int $track_id, int $user_id ): bool {
|
||||
// Check if WooCommerce integration is enabled.
|
||||
if ( ! get_option( 'wp_fedistream_enable_woocommerce', 0 ) ) {
|
||||
return $can_stream;
|
||||
}
|
||||
|
||||
// Check if track requires purchase.
|
||||
$requires_purchase = $this->track_requires_purchase( $track_id );
|
||||
|
||||
if ( ! $requires_purchase ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Guest users can't stream paid content.
|
||||
if ( ! $user_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if user has purchased this track.
|
||||
if ( Integration::user_has_purchased( $user_id, 'track', $track_id ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if user has purchased the album containing this track.
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
if ( $album_id && Integration::user_has_purchased( $user_id, 'album', $album_id ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a track requires purchase to stream.
|
||||
*
|
||||
* @param int $track_id Track ID.
|
||||
* @return bool
|
||||
*/
|
||||
private function track_requires_purchase( int $track_id ): bool {
|
||||
// Find WooCommerce products linked to this track.
|
||||
$products = $this->get_products_for_track( $track_id );
|
||||
|
||||
if ( empty( $products ) ) {
|
||||
// Also check album products.
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
if ( $album_id ) {
|
||||
$products = $this->get_products_for_album( $album_id );
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty( $products ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if any product includes streaming.
|
||||
foreach ( $products as $product ) {
|
||||
$include_streaming = get_post_meta( $product->ID, '_fedistream_include_streaming', true );
|
||||
if ( 'yes' === $include_streaming ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WooCommerce products linked to a track.
|
||||
*
|
||||
* @param int $track_id Track ID.
|
||||
* @return array
|
||||
*/
|
||||
private function get_products_for_track( int $track_id ): array {
|
||||
return get_posts(
|
||||
array(
|
||||
'post_type' => 'product',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
array(
|
||||
'key' => '_fedistream_linked_track',
|
||||
'value' => $track_id,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WooCommerce products linked to an album.
|
||||
*
|
||||
* @param int $album_id Album ID.
|
||||
* @return array
|
||||
*/
|
||||
private function get_products_for_album( int $album_id ): array {
|
||||
return get_posts(
|
||||
array(
|
||||
'post_type' => 'product',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
array(
|
||||
'key' => '_fedistream_linked_album',
|
||||
'value' => $album_id,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter track data for AJAX responses.
|
||||
*
|
||||
* @param array $data Track data.
|
||||
* @param int $track_id Track ID.
|
||||
* @return array Modified track data.
|
||||
*/
|
||||
public function filter_track_data( array $data, int $track_id ): array {
|
||||
$user_id = get_current_user_id();
|
||||
$can_stream = apply_filters( 'fedistream_can_stream_track', true, $track_id, $user_id );
|
||||
|
||||
if ( ! $can_stream ) {
|
||||
// Return preview URL instead of full audio.
|
||||
$data['audio_url'] = $this->get_preview_url( $track_id );
|
||||
$data['preview_only'] = true;
|
||||
$data['purchase_url'] = $this->get_purchase_url( $track_id );
|
||||
} else {
|
||||
$data['preview_only'] = false;
|
||||
}
|
||||
|
||||
// Add purchase status.
|
||||
$data['user_has_purchased'] = $user_id && (
|
||||
Integration::user_has_purchased( $user_id, 'track', $track_id ) ||
|
||||
Integration::user_has_purchased( $user_id, 'album', get_post_meta( $track_id, '_fedistream_album_id', true ) )
|
||||
);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preview URL for a track.
|
||||
*
|
||||
* @param int $track_id Track ID.
|
||||
* @return string
|
||||
*/
|
||||
private function get_preview_url( int $track_id ): string {
|
||||
return add_query_arg(
|
||||
array(
|
||||
'fedistream_preview' => $track_id,
|
||||
'_' => time(), // Cache buster.
|
||||
),
|
||||
home_url( '/' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get purchase URL for a track.
|
||||
*
|
||||
* @param int $track_id Track ID.
|
||||
* @return string
|
||||
*/
|
||||
private function get_purchase_url( int $track_id ): string {
|
||||
// Find product for this track.
|
||||
$products = $this->get_products_for_track( $track_id );
|
||||
|
||||
if ( ! empty( $products ) ) {
|
||||
return get_permalink( $products[0]->ID );
|
||||
}
|
||||
|
||||
// Check for album product.
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
if ( $album_id ) {
|
||||
$album_products = $this->get_products_for_album( $album_id );
|
||||
if ( ! empty( $album_products ) ) {
|
||||
return get_permalink( $album_products[0]->ID );
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle preview request.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle_preview_request(): void {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( ! isset( $_GET['fedistream_preview'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$track_id = absint( $_GET['fedistream_preview'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
|
||||
if ( ! $track_id ) {
|
||||
wp_die( esc_html__( 'Invalid track.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
$track = get_post( $track_id );
|
||||
if ( ! $track || 'fedistream_track' !== $track->post_type ) {
|
||||
wp_die( esc_html__( 'Track not found.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
// Get audio file.
|
||||
$audio_id = get_post_meta( $track_id, '_fedistream_audio_file', true );
|
||||
if ( ! $audio_id ) {
|
||||
wp_die( esc_html__( 'No audio file available.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
$file_path = get_attached_file( $audio_id );
|
||||
if ( ! $file_path || ! file_exists( $file_path ) ) {
|
||||
wp_die( esc_html__( 'File not found.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
// Get preview settings.
|
||||
$preview_start = (int) get_post_meta( $track_id, '_fedistream_preview_start', true );
|
||||
$preview_duration = (int) get_post_meta( $track_id, '_fedistream_preview_duration', true );
|
||||
|
||||
// Default 30 seconds preview.
|
||||
if ( ! $preview_duration ) {
|
||||
$preview_duration = 30;
|
||||
}
|
||||
|
||||
// For now, serve the full file with range headers.
|
||||
// In production, you'd use FFmpeg to extract a preview clip.
|
||||
$this->serve_audio_preview( $file_path, $preview_start, $preview_duration );
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve audio preview with limited duration.
|
||||
*
|
||||
* @param string $file_path File path.
|
||||
* @param int $start_seconds Start time in seconds.
|
||||
* @param int $duration_seconds Duration in seconds.
|
||||
* @return void
|
||||
*/
|
||||
private function serve_audio_preview( string $file_path, int $start_seconds, int $duration_seconds ): void {
|
||||
$file_size = filesize( $file_path );
|
||||
$mime_type = wp_check_filetype( $file_path )['type'] ?: 'audio/mpeg';
|
||||
|
||||
// Calculate byte range for preview (rough approximation).
|
||||
// This is a simplified approach; proper implementation would use FFmpeg.
|
||||
$duration_total = (int) get_post_meta( $this->get_track_id_from_path( $file_path ), '_fedistream_duration', true );
|
||||
|
||||
if ( $duration_total > 0 ) {
|
||||
$bytes_per_second = $file_size / $duration_total;
|
||||
$start_byte = (int) ( $start_seconds * $bytes_per_second );
|
||||
$end_byte = (int) min( ( $start_seconds + $duration_seconds ) * $bytes_per_second, $file_size - 1 );
|
||||
} else {
|
||||
// Serve first 30% of file as fallback.
|
||||
$start_byte = 0;
|
||||
$end_byte = (int) ( $file_size * 0.3 );
|
||||
}
|
||||
|
||||
// Clean output buffer.
|
||||
while ( ob_get_level() ) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
// Set headers for partial content.
|
||||
header( 'HTTP/1.1 206 Partial Content' );
|
||||
header( 'Content-Type: ' . $mime_type );
|
||||
header( 'Accept-Ranges: bytes' );
|
||||
header( 'Content-Length: ' . ( $end_byte - $start_byte + 1 ) );
|
||||
header( "Content-Range: bytes {$start_byte}-{$end_byte}/{$file_size}" );
|
||||
header( 'Content-Disposition: inline' );
|
||||
header( 'Cache-Control: no-cache, no-store, must-revalidate' );
|
||||
|
||||
// Serve partial content.
|
||||
$fp = fopen( $file_path, 'rb' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
|
||||
if ( $fp ) {
|
||||
fseek( $fp, $start_byte );
|
||||
echo fread( $fp, $end_byte - $start_byte + 1 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread,WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
fclose( $fp ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track ID from file path (for cached lookups).
|
||||
*
|
||||
* @param string $file_path File path.
|
||||
* @return int Track ID or 0.
|
||||
*/
|
||||
private function get_track_id_from_path( string $file_path ): int {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$attachment_id = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT ID FROM {$wpdb->posts} WHERE guid LIKE %s",
|
||||
'%' . $wpdb->esc_like( basename( $file_path ) )
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! $attachment_id ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$track_id = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_fedistream_audio_file' AND meta_value = %d",
|
||||
$attachment_id
|
||||
)
|
||||
);
|
||||
|
||||
return (int) $track_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add purchase button after track player.
|
||||
*
|
||||
* @param int $track_id Track ID.
|
||||
* @return void
|
||||
*/
|
||||
public function add_purchase_button( int $track_id ): void {
|
||||
if ( ! get_option( 'wp_fedistream_enable_woocommerce', 0 ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
// Check if already purchased.
|
||||
if ( $user_id ) {
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
|
||||
if ( Integration::user_has_purchased( $user_id, 'track', $track_id ) ) {
|
||||
echo '<p class="fedistream-purchase-status">' . esc_html__( 'You own this track.', 'wp-fedistream' ) . '</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $album_id && Integration::user_has_purchased( $user_id, 'album', $album_id ) ) {
|
||||
echo '<p class="fedistream-purchase-status">' . esc_html__( 'You own this album.', 'wp-fedistream' ) . '</p>';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Find product for this track.
|
||||
$products = $this->get_products_for_track( $track_id );
|
||||
|
||||
if ( ! empty( $products ) ) {
|
||||
$product = wc_get_product( $products[0]->ID );
|
||||
if ( $product ) {
|
||||
echo '<div class="fedistream-purchase-button">';
|
||||
echo '<a href="' . esc_url( get_permalink( $products[0]->ID ) ) . '" class="button">';
|
||||
echo esc_html__( 'Buy Track', 'wp-fedistream' ) . ' - ' . wp_kses_post( $product->get_price_html() );
|
||||
echo '</a>';
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Also show album option if available.
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
if ( $album_id ) {
|
||||
$album_products = $this->get_products_for_album( $album_id );
|
||||
if ( ! empty( $album_products ) ) {
|
||||
$album_product = wc_get_product( $album_products[0]->ID );
|
||||
$album = get_post( $album_id );
|
||||
if ( $album_product && $album ) {
|
||||
echo '<div class="fedistream-purchase-button fedistream-purchase-album">';
|
||||
echo '<a href="' . esc_url( get_permalink( $album_products[0]->ID ) ) . '" class="button button-secondary">';
|
||||
/* translators: %s: Album name */
|
||||
echo esc_html( sprintf( __( 'Buy Full Album: %s', 'wp-fedistream' ), $album->post_title ) );
|
||||
echo ' - ' . wp_kses_post( $album_product->get_price_html() );
|
||||
echo '</a>';
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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