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