__( '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;
}
}