check_woocommerce(); // If plugins_loaded hasn't fully completed, hook init at priority 20. // Otherwise, run init directly. if ( ! did_action( 'plugins_loaded' ) || doing_action( 'plugins_loaded' ) ) { add_action( 'plugins_loaded', array( $this, 'init' ), 20 ); } else { $this->init(); } } /** * 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', ) ); ?>

'_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, ) ); ?>
'_fedistream_include_streaming', 'label' => __( 'Include Streaming', 'wp-fedistream' ), 'description' => __( 'Purchase unlocks full-quality streaming access.', 'wp-fedistream' ), 'value' => $include_streaming, ) ); ?>

'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 ); ?>

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 ); ?>

'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; } ?>

    ID, '_fedistream_duration', true ); ?>
  1. post_title ); ?>
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 '' . esc_html( $album->post_title ) . ''; } } else { echo ''; } } 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 '' . esc_html( $track->post_title ) . ''; } } else { echo ''; } } else { echo ''; } } }