__( 'You must be logged in.', 'wp-fedistream' ) ) ); } $content_type = isset( $_POST['content_type'] ) ? sanitize_text_field( wp_unslash( $_POST['content_type'] ) ) : ''; $content_id = isset( $_POST['content_id'] ) ? absint( $_POST['content_id'] ) : 0; if ( ! in_array( $content_type, array( 'track', 'album', 'playlist' ), true ) || ! $content_id ) { wp_send_json_error( array( 'message' => __( 'Invalid request.', 'wp-fedistream' ) ) ); } $user_id = get_current_user_id(); $is_favorited = self::is_favorited( $user_id, $content_type, $content_id ); if ( $is_favorited ) { $result = self::remove_favorite( $user_id, $content_type, $content_id ); $action = 'removed'; } else { $result = self::add_favorite( $user_id, $content_type, $content_id ); $action = 'added'; } if ( $result ) { wp_send_json_success( array( 'action' => $action, 'is_favorited' => ! $is_favorited, 'message' => 'added' === $action ? __( 'Added to your library.', 'wp-fedistream' ) : __( 'Removed from your library.', 'wp-fedistream' ), ) ); } else { wp_send_json_error( array( 'message' => __( 'Failed to update library.', 'wp-fedistream' ) ) ); } } /** * Toggle follow via AJAX. * * @return void */ public function ajax_toggle_follow(): void { check_ajax_referer( 'fedistream_library', 'nonce' ); if ( ! is_user_logged_in() ) { wp_send_json_error( array( 'message' => __( 'You must be logged in.', 'wp-fedistream' ) ) ); } $artist_id = isset( $_POST['artist_id'] ) ? absint( $_POST['artist_id'] ) : 0; if ( ! $artist_id ) { wp_send_json_error( array( 'message' => __( 'Invalid artist.', 'wp-fedistream' ) ) ); } $user_id = get_current_user_id(); $is_following = self::is_following( $user_id, $artist_id ); if ( $is_following ) { $result = self::unfollow_artist( $user_id, $artist_id ); $action = 'unfollowed'; } else { $result = self::follow_artist( $user_id, $artist_id ); $action = 'followed'; } if ( $result ) { wp_send_json_success( array( 'action' => $action, 'is_following' => ! $is_following, 'message' => 'followed' === $action ? __( 'You are now following this artist.', 'wp-fedistream' ) : __( 'You unfollowed this artist.', 'wp-fedistream' ), ) ); } else { wp_send_json_error( array( 'message' => __( 'Failed to update follow status.', 'wp-fedistream' ) ) ); } } /** * Get user library via AJAX. * * @return void */ public function ajax_get_library(): void { check_ajax_referer( 'fedistream_library', 'nonce' ); if ( ! is_user_logged_in() ) { wp_send_json_error( array( 'message' => __( 'You must be logged in.', 'wp-fedistream' ) ) ); } $user_id = get_current_user_id(); $type = isset( $_POST['type'] ) ? sanitize_text_field( wp_unslash( $_POST['type'] ) ) : 'all'; $page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1; $per_page = 20; $library = self::get_user_library( $user_id, $type, $page, $per_page ); wp_send_json_success( $library ); } /** * Get followed artists via AJAX. * * @return void */ public function ajax_get_followed_artists(): void { check_ajax_referer( 'fedistream_library', 'nonce' ); if ( ! is_user_logged_in() ) { wp_send_json_error( array( 'message' => __( 'You must be logged in.', 'wp-fedistream' ) ) ); } $user_id = get_current_user_id(); $page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1; $per_page = 20; $artists = self::get_followed_artists( $user_id, $page, $per_page ); wp_send_json_success( $artists ); } /** * Get listening history via AJAX. * * @return void */ public function ajax_get_history(): void { check_ajax_referer( 'fedistream_library', 'nonce' ); if ( ! is_user_logged_in() ) { wp_send_json_error( array( 'message' => __( 'You must be logged in.', 'wp-fedistream' ) ) ); } $user_id = get_current_user_id(); $page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1; $per_page = 50; $history = self::get_listening_history( $user_id, $page, $per_page ); wp_send_json_success( $history ); } /** * Clear listening history via AJAX. * * @return void */ public function ajax_clear_history(): void { check_ajax_referer( 'fedistream_library', 'nonce' ); if ( ! is_user_logged_in() ) { wp_send_json_error( array( 'message' => __( 'You must be logged in.', 'wp-fedistream' ) ) ); } $user_id = get_current_user_id(); $result = self::clear_listening_history( $user_id ); if ( $result ) { wp_send_json_success( array( 'message' => __( 'History cleared.', 'wp-fedistream' ) ) ); } else { wp_send_json_error( array( 'message' => __( 'Failed to clear history.', 'wp-fedistream' ) ) ); } } /** * Add a favorite. * * @param int $user_id User ID. * @param string $content_type Content type (track, album, playlist). * @param int $content_id Content ID. * @return bool */ public static function add_favorite( int $user_id, string $content_type, int $content_id ): bool { global $wpdb; $table = $wpdb->prefix . 'fedistream_favorites'; $result = $wpdb->insert( $table, array( 'user_id' => $user_id, 'content_type' => $content_type, 'content_id' => $content_id, 'created_at' => current_time( 'mysql' ), ), array( '%d', '%s', '%d', '%s' ) ); if ( $result ) { do_action( 'fedistream_favorite_added', $user_id, $content_type, $content_id ); } return (bool) $result; } /** * Remove a favorite. * * @param int $user_id User ID. * @param string $content_type Content type (track, album, playlist). * @param int $content_id Content ID. * @return bool */ public static function remove_favorite( int $user_id, string $content_type, int $content_id ): bool { global $wpdb; $table = $wpdb->prefix . 'fedistream_favorites'; $result = $wpdb->delete( $table, array( 'user_id' => $user_id, 'content_type' => $content_type, 'content_id' => $content_id, ), array( '%d', '%s', '%d' ) ); if ( $result ) { do_action( 'fedistream_favorite_removed', $user_id, $content_type, $content_id ); } return (bool) $result; } /** * Check if content is favorited. * * @param int $user_id User ID. * @param string $content_type Content type. * @param int $content_id Content ID. * @return bool */ public static function is_favorited( int $user_id, string $content_type, int $content_id ): bool { global $wpdb; $table = $wpdb->prefix . 'fedistream_favorites'; $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; } /** * Follow an artist. * * @param int $user_id User ID. * @param int $artist_id Artist post ID. * @return bool */ public static function follow_artist( int $user_id, int $artist_id ): bool { global $wpdb; $table = $wpdb->prefix . 'fedistream_user_follows'; $result = $wpdb->insert( $table, array( 'user_id' => $user_id, 'artist_id' => $artist_id, 'created_at' => current_time( 'mysql' ), ), array( '%d', '%d', '%s' ) ); if ( $result ) { do_action( 'fedistream_artist_followed', $user_id, $artist_id ); } return (bool) $result; } /** * Unfollow an artist. * * @param int $user_id User ID. * @param int $artist_id Artist post ID. * @return bool */ public static function unfollow_artist( int $user_id, int $artist_id ): bool { global $wpdb; $table = $wpdb->prefix . 'fedistream_user_follows'; $result = $wpdb->delete( $table, array( 'user_id' => $user_id, 'artist_id' => $artist_id, ), array( '%d', '%d' ) ); if ( $result ) { do_action( 'fedistream_artist_unfollowed', $user_id, $artist_id ); } return (bool) $result; } /** * Check if user is following an artist. * * @param int $user_id User ID. * @param int $artist_id Artist post ID. * @return bool */ public static function is_following( int $user_id, int $artist_id ): bool { global $wpdb; $table = $wpdb->prefix . 'fedistream_user_follows'; $exists = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$table} WHERE user_id = %d AND artist_id = %d", $user_id, $artist_id ) ); return (bool) $exists; } /** * Record play history. * * @param int $track_id Track post ID. * @param int $user_id User ID (0 for anonymous). * @return void */ public function record_play_history( int $track_id, int $user_id ): void { if ( ! $user_id ) { return; } global $wpdb; $table = $wpdb->prefix . 'fedistream_listening_history'; // Check if recently played (within last 30 seconds) to avoid duplicates. $recent = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$table} WHERE user_id = %d AND track_id = %d AND played_at > DATE_SUB(NOW(), INTERVAL 30 SECOND)", $user_id, $track_id ) ); if ( $recent ) { return; } $wpdb->insert( $table, array( 'user_id' => $user_id, 'track_id' => $track_id, 'played_at' => current_time( 'mysql' ), ), array( '%d', '%d', '%s' ) ); } /** * Get user library. * * @param int $user_id User ID. * @param string $type Content type filter (all, tracks, albums, playlists). * @param int $page Page number. * @param int $per_page Items per page. * @return array */ public static function get_user_library( int $user_id, string $type = 'all', int $page = 1, int $per_page = 20 ): array { global $wpdb; $table = $wpdb->prefix . 'fedistream_favorites'; $offset = ( $page - 1 ) * $per_page; $where = 'WHERE user_id = %d'; $params = array( $user_id ); if ( 'all' !== $type && in_array( $type, array( 'track', 'album', 'playlist' ), true ) ) { $where .= ' AND content_type = %s'; $params[] = $type; } $total = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} {$where}", ...$params ) ); $params[] = $per_page; $params[] = $offset; $favorites = $wpdb->get_results( $wpdb->prepare( "SELECT content_type, content_id, created_at FROM {$table} {$where} ORDER BY created_at DESC LIMIT %d OFFSET %d", ...$params ) ); $items = array(); foreach ( $favorites as $favorite ) { $post = get_post( $favorite->content_id ); if ( ! $post || 'publish' !== $post->post_status ) { continue; } $item = array( 'id' => $post->ID, 'type' => $favorite->content_type, 'title' => $post->post_title, 'permalink' => get_permalink( $post ), 'added_at' => $favorite->created_at, 'thumbnail' => get_the_post_thumbnail_url( $post, 'medium' ), ); if ( 'track' === $favorite->content_type ) { $item['duration'] = get_post_meta( $post->ID, '_fedistream_duration', true ); $item['artist'] = self::get_track_artist_name( $post->ID ); } elseif ( 'album' === $favorite->content_type ) { $item['artist'] = self::get_album_artist_name( $post->ID ); $item['track_count'] = get_post_meta( $post->ID, '_fedistream_total_tracks', true ); } $items[] = $item; } return array( 'items' => $items, 'total' => (int) $total, 'page' => $page, 'per_page' => $per_page, 'total_pages' => (int) ceil( $total / $per_page ), ); } /** * Get followed artists. * * @param int $user_id User ID. * @param int $page Page number. * @param int $per_page Items per page. * @return array */ public static function get_followed_artists( int $user_id, int $page = 1, int $per_page = 20 ): array { global $wpdb; $table = $wpdb->prefix . 'fedistream_user_follows'; $offset = ( $page - 1 ) * $per_page; $total = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE user_id = %d", $user_id ) ); $follows = $wpdb->get_results( $wpdb->prepare( "SELECT artist_id, created_at FROM {$table} WHERE user_id = %d ORDER BY created_at DESC LIMIT %d OFFSET %d", $user_id, $per_page, $offset ) ); $artists = array(); foreach ( $follows as $follow ) { $post = get_post( $follow->artist_id ); if ( ! $post || 'publish' !== $post->post_status ) { continue; } $artists[] = array( 'id' => $post->ID, 'name' => $post->post_title, 'permalink' => get_permalink( $post ), 'followed_at' => $follow->created_at, 'thumbnail' => get_the_post_thumbnail_url( $post, 'thumbnail' ), 'type' => get_post_meta( $post->ID, '_fedistream_artist_type', true ) ?: 'solo', ); } return array( 'artists' => $artists, 'total' => (int) $total, 'page' => $page, 'per_page' => $per_page, 'total_pages' => (int) ceil( $total / $per_page ), ); } /** * Get listening history. * * @param int $user_id User ID. * @param int $page Page number. * @param int $per_page Items per page. * @return array */ public static function get_listening_history( int $user_id, int $page = 1, int $per_page = 50 ): array { global $wpdb; $table = $wpdb->prefix . 'fedistream_listening_history'; $offset = ( $page - 1 ) * $per_page; $total = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE user_id = %d", $user_id ) ); $history = $wpdb->get_results( $wpdb->prepare( "SELECT track_id, played_at FROM {$table} WHERE user_id = %d ORDER BY played_at DESC LIMIT %d OFFSET %d", $user_id, $per_page, $offset ) ); $tracks = array(); foreach ( $history as $item ) { $post = get_post( $item->track_id ); if ( ! $post || 'publish' !== $post->post_status ) { continue; } $tracks[] = array( 'id' => $post->ID, 'title' => $post->post_title, 'permalink' => get_permalink( $post ), 'played_at' => $item->played_at, 'thumbnail' => get_the_post_thumbnail_url( $post, 'thumbnail' ), 'duration' => get_post_meta( $post->ID, '_fedistream_duration', true ), 'artist' => self::get_track_artist_name( $post->ID ), ); } return array( 'tracks' => $tracks, 'total' => (int) $total, 'page' => $page, 'per_page' => $per_page, 'total_pages' => (int) ceil( $total / $per_page ), ); } /** * Clear listening history. * * @param int $user_id User ID. * @return bool */ public static function clear_listening_history( int $user_id ): bool { global $wpdb; $table = $wpdb->prefix . 'fedistream_listening_history'; $result = $wpdb->delete( $table, array( 'user_id' => $user_id ), array( '%d' ) ); return false !== $result; } /** * Get track artist name. * * @param int $track_id Track post ID. * @return string */ private static function get_track_artist_name( int $track_id ): string { $artist_ids = get_post_meta( $track_id, '_fedistream_artist_ids', true ); if ( is_array( $artist_ids ) && ! empty( $artist_ids ) ) { $names = array(); foreach ( $artist_ids as $artist_id ) { $artist = get_post( $artist_id ); if ( $artist ) { $names[] = $artist->post_title; } } return implode( ', ', $names ); } $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 ''; } /** * Get album artist name. * * @param int $album_id Album post ID. * @return string */ private static function get_album_artist_name( int $album_id ): string { $artist_id = get_post_meta( $album_id, '_fedistream_album_artist', true ); if ( $artist_id ) { $artist = get_post( $artist_id ); return $artist ? $artist->post_title : ''; } return ''; } /** * Add favorite button to track/album actions. * * @param string $actions HTML actions. * @param int $post_id Post ID. * @return string */ public function add_favorite_button( string $actions, int $post_id ): string { if ( ! is_user_logged_in() ) { return $actions; } $post = get_post( $post_id ); if ( ! $post ) { return $actions; } $content_type = str_replace( 'fedistream_', '', $post->post_type ); $user_id = get_current_user_id(); $is_favorited = self::is_favorited( $user_id, $content_type, $post_id ); $button = sprintf( '', $is_favorited ? ' is-favorited' : '', esc_attr( $content_type ), $post_id, $is_favorited ? esc_attr__( 'Remove from library', 'wp-fedistream' ) : esc_attr__( 'Add to library', 'wp-fedistream' ) ); return $actions . $button; } /** * Add follow button to artist actions. * * @param string $actions HTML actions. * @param int $artist_id Artist post ID. * @return string */ public function add_follow_button( string $actions, int $artist_id ): string { if ( ! is_user_logged_in() ) { return $actions; } $user_id = get_current_user_id(); $is_following = self::is_following( $user_id, $artist_id ); $button = sprintf( '', $is_following ? ' is-following' : '', $artist_id, $is_following ? 'yes' : 'plus', $is_following ? esc_html__( 'Following', 'wp-fedistream' ) : esc_html__( 'Follow', 'wp-fedistream' ) ); return $actions . $button; } /** * Get user's favorite count. * * @param int $user_id User ID. * @param string $content_type Optional content type filter. * @return int */ public static function get_favorite_count( int $user_id, string $content_type = '' ): int { global $wpdb; $table = $wpdb->prefix . 'fedistream_favorites'; if ( $content_type ) { $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE user_id = %d AND content_type = %s", $user_id, $content_type ) ); } else { $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE user_id = %d", $user_id ) ); } return (int) $count; } /** * Get user's followed artist count. * * @param int $user_id User ID. * @return int */ public static function get_following_count( int $user_id ): int { global $wpdb; $table = $wpdb->prefix . 'fedistream_user_follows'; $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE user_id = %d", $user_id ) ); return (int) $count; } }