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:
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>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user