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:
2026-01-28 23:23:05 +01:00
commit 4a5d7b9f4d
91 changed files with 22750 additions and 0 deletions

View File

@@ -0,0 +1,604 @@
<?php
/**
* Custom list table columns for admin.
*
* @package WP_FediStream
*/
namespace WP_FediStream\Admin;
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* ListColumns class.
*
* Handles custom columns in admin list tables for all post types.
*/
class ListColumns {
/**
* Constructor.
*/
public function __construct() {
// Artist columns.
add_filter( 'manage_fedistream_artist_posts_columns', array( $this, 'artist_columns' ) );
add_action( 'manage_fedistream_artist_posts_custom_column', array( $this, 'artist_column_content' ), 10, 2 );
add_filter( 'manage_edit-fedistream_artist_sortable_columns', array( $this, 'artist_sortable_columns' ) );
// Album columns.
add_filter( 'manage_fedistream_album_posts_columns', array( $this, 'album_columns' ) );
add_action( 'manage_fedistream_album_posts_custom_column', array( $this, 'album_column_content' ), 10, 2 );
add_filter( 'manage_edit-fedistream_album_sortable_columns', array( $this, 'album_sortable_columns' ) );
// Track columns.
add_filter( 'manage_fedistream_track_posts_columns', array( $this, 'track_columns' ) );
add_action( 'manage_fedistream_track_posts_custom_column', array( $this, 'track_column_content' ), 10, 2 );
add_filter( 'manage_edit-fedistream_track_sortable_columns', array( $this, 'track_sortable_columns' ) );
// Playlist columns.
add_filter( 'manage_fedistream_playlist_posts_columns', array( $this, 'playlist_columns' ) );
add_action( 'manage_fedistream_playlist_posts_custom_column', array( $this, 'playlist_column_content' ), 10, 2 );
add_filter( 'manage_edit-fedistream_playlist_sortable_columns', array( $this, 'playlist_sortable_columns' ) );
// Handle sorting.
add_action( 'pre_get_posts', array( $this, 'handle_sorting' ) );
}
/**
* Define artist list columns.
*
* @param array $columns Default columns.
* @return array Modified columns.
*/
public function artist_columns( array $columns ): array {
$new_columns = array();
foreach ( $columns as $key => $value ) {
if ( 'title' === $key ) {
$new_columns['fedistream_photo'] = '';
}
$new_columns[ $key ] = $value;
if ( 'title' === $key ) {
$new_columns['fedistream_type'] = __( 'Type', 'wp-fedistream' );
$new_columns['fedistream_albums'] = __( 'Albums', 'wp-fedistream' );
$new_columns['fedistream_tracks'] = __( 'Tracks', 'wp-fedistream' );
}
}
// Remove date, we'll add it back at the end.
unset( $new_columns['date'] );
$new_columns['date'] = __( 'Date', 'wp-fedistream' );
return $new_columns;
}
/**
* Render artist column content.
*
* @param string $column Column name.
* @param int $post_id Post ID.
* @return void
*/
public function artist_column_content( string $column, int $post_id ): void {
switch ( $column ) {
case 'fedistream_photo':
$thumbnail = get_the_post_thumbnail( $post_id, array( 40, 40 ) );
if ( $thumbnail ) {
echo wp_kses_post( $thumbnail );
} else {
echo '<span class="dashicons dashicons-admin-users" style="font-size: 40px; width: 40px; height: 40px; color: #ccc;"></span>';
}
break;
case 'fedistream_type':
$type = get_post_meta( $post_id, '_fedistream_artist_type', true );
$types = array(
'solo' => __( 'Solo', 'wp-fedistream' ),
'band' => __( 'Band', 'wp-fedistream' ),
'duo' => __( 'Duo', 'wp-fedistream' ),
'collective' => __( 'Collective', 'wp-fedistream' ),
);
echo esc_html( $types[ $type ] ?? __( 'Solo', 'wp-fedistream' ) );
break;
case 'fedistream_albums':
$count = $this->count_posts_by_meta( 'fedistream_album', '_fedistream_album_artist', $post_id );
echo '<a href="' . esc_url( admin_url( 'edit.php?post_type=fedistream_album&artist=' . $post_id ) ) . '">' . esc_html( $count ) . '</a>';
break;
case 'fedistream_tracks':
$count = $this->count_tracks_by_artist( $post_id );
echo esc_html( $count );
break;
}
}
/**
* Define sortable artist columns.
*
* @param array $columns Sortable columns.
* @return array Modified columns.
*/
public function artist_sortable_columns( array $columns ): array {
$columns['fedistream_type'] = 'fedistream_type';
return $columns;
}
/**
* Define album list columns.
*
* @param array $columns Default columns.
* @return array Modified columns.
*/
public function album_columns( array $columns ): array {
$new_columns = array();
foreach ( $columns as $key => $value ) {
if ( 'title' === $key ) {
$new_columns['fedistream_artwork'] = '';
}
$new_columns[ $key ] = $value;
if ( 'title' === $key ) {
$new_columns['fedistream_artist'] = __( 'Artist', 'wp-fedistream' );
$new_columns['fedistream_type'] = __( 'Type', 'wp-fedistream' );
$new_columns['fedistream_tracks'] = __( 'Tracks', 'wp-fedistream' );
$new_columns['fedistream_release_date'] = __( 'Release Date', 'wp-fedistream' );
}
}
unset( $new_columns['date'] );
$new_columns['date'] = __( 'Date', 'wp-fedistream' );
return $new_columns;
}
/**
* Render album column content.
*
* @param string $column Column name.
* @param int $post_id Post ID.
* @return void
*/
public function album_column_content( string $column, int $post_id ): void {
switch ( $column ) {
case 'fedistream_artwork':
$thumbnail = get_the_post_thumbnail( $post_id, array( 40, 40 ) );
if ( $thumbnail ) {
echo wp_kses_post( $thumbnail );
} else {
echo '<span class="dashicons dashicons-album" style="font-size: 40px; width: 40px; height: 40px; color: #ccc;"></span>';
}
break;
case 'fedistream_artist':
$artist_id = get_post_meta( $post_id, '_fedistream_album_artist', true );
if ( $artist_id ) {
$artist = get_post( $artist_id );
if ( $artist ) {
echo '<a href="' . esc_url( get_edit_post_link( $artist_id ) ) . '">' . esc_html( $artist->post_title ) . '</a>';
}
} else {
echo '<span class="description">' . esc_html__( 'No artist', 'wp-fedistream' ) . '</span>';
}
break;
case 'fedistream_type':
$type = get_post_meta( $post_id, '_fedistream_album_type', true );
$types = array(
'album' => __( 'Album', 'wp-fedistream' ),
'ep' => __( 'EP', 'wp-fedistream' ),
'single' => __( 'Single', 'wp-fedistream' ),
'compilation' => __( 'Compilation', 'wp-fedistream' ),
'live' => __( 'Live', 'wp-fedistream' ),
'remix' => __( 'Remix', 'wp-fedistream' ),
);
echo esc_html( $types[ $type ] ?? __( 'Album', 'wp-fedistream' ) );
break;
case 'fedistream_tracks':
$count = get_post_meta( $post_id, '_fedistream_album_total_tracks', true );
echo '<a href="' . esc_url( admin_url( 'edit.php?post_type=fedistream_track&album=' . $post_id ) ) . '">' . esc_html( $count ?: 0 ) . '</a>';
break;
case 'fedistream_release_date':
$date = get_post_meta( $post_id, '_fedistream_album_release_date', true );
if ( $date ) {
echo esc_html( date_i18n( get_option( 'date_format' ), strtotime( $date ) ) );
} else {
echo '<span class="description">—</span>';
}
break;
}
}
/**
* Define sortable album columns.
*
* @param array $columns Sortable columns.
* @return array Modified columns.
*/
public function album_sortable_columns( array $columns ): array {
$columns['fedistream_type'] = 'fedistream_type';
$columns['fedistream_release_date'] = 'fedistream_release_date';
$columns['fedistream_artist'] = 'fedistream_artist';
return $columns;
}
/**
* Define track list columns.
*
* @param array $columns Default columns.
* @return array Modified columns.
*/
public function track_columns( array $columns ): array {
$new_columns = array();
foreach ( $columns as $key => $value ) {
if ( 'title' === $key ) {
$new_columns['fedistream_artwork'] = '';
}
$new_columns[ $key ] = $value;
if ( 'title' === $key ) {
$new_columns['fedistream_artists'] = __( 'Artists', 'wp-fedistream' );
$new_columns['fedistream_album'] = __( 'Album', 'wp-fedistream' );
$new_columns['fedistream_duration'] = __( 'Duration', 'wp-fedistream' );
$new_columns['fedistream_plays'] = __( 'Plays', 'wp-fedistream' );
}
}
unset( $new_columns['date'] );
$new_columns['date'] = __( 'Date', 'wp-fedistream' );
return $new_columns;
}
/**
* Render track column content.
*
* @param string $column Column name.
* @param int $post_id Post ID.
* @return void
*/
public function track_column_content( string $column, int $post_id ): void {
switch ( $column ) {
case 'fedistream_artwork':
$thumbnail = get_the_post_thumbnail( $post_id, array( 40, 40 ) );
if ( ! $thumbnail ) {
// Try album artwork.
$album_id = get_post_meta( $post_id, '_fedistream_track_album', true );
if ( $album_id ) {
$thumbnail = get_the_post_thumbnail( $album_id, array( 40, 40 ) );
}
}
if ( $thumbnail ) {
echo wp_kses_post( $thumbnail );
} else {
echo '<span class="dashicons dashicons-format-audio" style="font-size: 40px; width: 40px; height: 40px; color: #ccc;"></span>';
}
break;
case 'fedistream_artists':
$artists = get_post_meta( $post_id, '_fedistream_track_artists', true );
if ( is_array( $artists ) && ! empty( $artists ) ) {
$artist_links = array();
foreach ( $artists as $artist_id ) {
$artist = get_post( $artist_id );
if ( $artist ) {
$artist_links[] = '<a href="' . esc_url( get_edit_post_link( $artist_id ) ) . '">' . esc_html( $artist->post_title ) . '</a>';
}
}
echo wp_kses_post( implode( ', ', $artist_links ) );
} else {
echo '<span class="description">' . esc_html__( 'No artist', 'wp-fedistream' ) . '</span>';
}
break;
case 'fedistream_album':
$album_id = get_post_meta( $post_id, '_fedistream_track_album', true );
if ( $album_id ) {
$album = get_post( $album_id );
if ( $album ) {
echo '<a href="' . esc_url( get_edit_post_link( $album_id ) ) . '">' . esc_html( $album->post_title ) . '</a>';
}
} else {
echo '<span class="description">' . esc_html__( 'Single', 'wp-fedistream' ) . '</span>';
}
break;
case 'fedistream_duration':
$duration = get_post_meta( $post_id, '_fedistream_track_duration', true );
if ( $duration ) {
$minutes = floor( $duration / 60 );
$seconds = $duration % 60;
echo esc_html( sprintf( '%d:%02d', $minutes, $seconds ) );
} else {
echo '<span class="description">—</span>';
}
break;
case 'fedistream_plays':
$plays = $this->get_track_plays( $post_id );
echo esc_html( number_format_i18n( $plays ) );
break;
}
}
/**
* Define sortable track columns.
*
* @param array $columns Sortable columns.
* @return array Modified columns.
*/
public function track_sortable_columns( array $columns ): array {
$columns['fedistream_duration'] = 'fedistream_duration';
$columns['fedistream_plays'] = 'fedistream_plays';
$columns['fedistream_album'] = 'fedistream_album';
return $columns;
}
/**
* Define playlist list columns.
*
* @param array $columns Default columns.
* @return array Modified columns.
*/
public function playlist_columns( array $columns ): array {
$new_columns = array();
foreach ( $columns as $key => $value ) {
if ( 'title' === $key ) {
$new_columns['fedistream_cover'] = '';
}
$new_columns[ $key ] = $value;
if ( 'title' === $key ) {
$new_columns['fedistream_tracks'] = __( 'Tracks', 'wp-fedistream' );
$new_columns['fedistream_duration'] = __( 'Duration', 'wp-fedistream' );
$new_columns['fedistream_visibility'] = __( 'Visibility', 'wp-fedistream' );
}
}
unset( $new_columns['date'] );
$new_columns['date'] = __( 'Date', 'wp-fedistream' );
return $new_columns;
}
/**
* Render playlist column content.
*
* @param string $column Column name.
* @param int $post_id Post ID.
* @return void
*/
public function playlist_column_content( string $column, int $post_id ): void {
switch ( $column ) {
case 'fedistream_cover':
$thumbnail = get_the_post_thumbnail( $post_id, array( 40, 40 ) );
if ( $thumbnail ) {
echo wp_kses_post( $thumbnail );
} else {
echo '<span class="dashicons dashicons-playlist-audio" style="font-size: 40px; width: 40px; height: 40px; color: #ccc;"></span>';
}
break;
case 'fedistream_tracks':
$count = get_post_meta( $post_id, '_fedistream_playlist_track_count', true );
echo esc_html( $count ?: 0 );
break;
case 'fedistream_duration':
$duration = get_post_meta( $post_id, '_fedistream_playlist_total_duration', true );
if ( $duration ) {
if ( $duration >= 3600 ) {
$hours = floor( $duration / 3600 );
$minutes = floor( ( $duration % 3600 ) / 60 );
echo esc_html( sprintf( '%d:%02d:%02d', $hours, $minutes, $duration % 60 ) );
} else {
$minutes = floor( $duration / 60 );
$seconds = $duration % 60;
echo esc_html( sprintf( '%d:%02d', $minutes, $seconds ) );
}
} else {
echo '<span class="description">—</span>';
}
break;
case 'fedistream_visibility':
$visibility = get_post_meta( $post_id, '_fedistream_playlist_visibility', true ) ?: 'public';
$labels = array(
'public' => __( 'Public', 'wp-fedistream' ),
'unlisted' => __( 'Unlisted', 'wp-fedistream' ),
'private' => __( 'Private', 'wp-fedistream' ),
);
$icons = array(
'public' => 'dashicons-visibility',
'unlisted' => 'dashicons-hidden',
'private' => 'dashicons-lock',
);
echo '<span class="dashicons ' . esc_attr( $icons[ $visibility ] ?? 'dashicons-visibility' ) . '" title="' . esc_attr( $labels[ $visibility ] ?? '' ) . '"></span> ';
echo esc_html( $labels[ $visibility ] ?? __( 'Public', 'wp-fedistream' ) );
break;
}
}
/**
* Define sortable playlist columns.
*
* @param array $columns Sortable columns.
* @return array Modified columns.
*/
public function playlist_sortable_columns( array $columns ): array {
$columns['fedistream_tracks'] = 'fedistream_track_count';
$columns['fedistream_duration'] = 'fedistream_duration';
$columns['fedistream_visibility'] = 'fedistream_visibility';
return $columns;
}
/**
* Handle custom column sorting.
*
* @param \WP_Query $query The query object.
* @return void
*/
public function handle_sorting( \WP_Query $query ): void {
if ( ! is_admin() || ! $query->is_main_query() ) {
return;
}
$orderby = $query->get( 'orderby' );
switch ( $orderby ) {
case 'fedistream_type':
$post_type = $query->get( 'post_type' );
if ( 'fedistream_artist' === $post_type ) {
$query->set( 'meta_key', '_fedistream_artist_type' );
} elseif ( 'fedistream_album' === $post_type ) {
$query->set( 'meta_key', '_fedistream_album_type' );
}
$query->set( 'orderby', 'meta_value' );
break;
case 'fedistream_release_date':
$query->set( 'meta_key', '_fedistream_album_release_date' );
$query->set( 'orderby', 'meta_value' );
break;
case 'fedistream_artist':
$query->set( 'meta_key', '_fedistream_album_artist' );
$query->set( 'orderby', 'meta_value_num' );
break;
case 'fedistream_duration':
$post_type = $query->get( 'post_type' );
if ( 'fedistream_track' === $post_type ) {
$query->set( 'meta_key', '_fedistream_track_duration' );
} elseif ( 'fedistream_playlist' === $post_type ) {
$query->set( 'meta_key', '_fedistream_playlist_total_duration' );
}
$query->set( 'orderby', 'meta_value_num' );
break;
case 'fedistream_track_count':
$query->set( 'meta_key', '_fedistream_playlist_track_count' );
$query->set( 'orderby', 'meta_value_num' );
break;
case 'fedistream_visibility':
$query->set( 'meta_key', '_fedistream_playlist_visibility' );
$query->set( 'orderby', 'meta_value' );
break;
case 'fedistream_album':
$query->set( 'meta_key', '_fedistream_track_album' );
$query->set( 'orderby', 'meta_value_num' );
break;
}
}
/**
* Count posts by meta value.
*
* @param string $post_type Post type.
* @param string $meta_key Meta key.
* @param mixed $meta_value Meta value.
* @return int Count.
*/
private function count_posts_by_meta( string $post_type, string $meta_key, $meta_value ): int {
$query = new \WP_Query(
array(
'post_type' => $post_type,
'posts_per_page' => -1,
'post_status' => 'publish',
'meta_key' => $meta_key,
'meta_value' => $meta_value,
'fields' => 'ids',
)
);
return $query->found_posts;
}
/**
* Count tracks by artist.
*
* @param int $artist_id Artist post ID.
* @return int Track count.
*/
private function count_tracks_by_artist( int $artist_id ): int {
global $wpdb;
// Count tracks where artist is in the artists array.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM $wpdb->posts p
INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id
WHERE p.post_type = 'fedistream_track'
AND p.post_status = 'publish'
AND pm.meta_key = '_fedistream_track_artists'
AND pm.meta_value LIKE %s",
'%"' . $artist_id . '"%'
)
);
// Also check serialized format.
if ( ! $count ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM $wpdb->posts p
INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id
WHERE p.post_type = 'fedistream_track'
AND p.post_status = 'publish'
AND pm.meta_key = '_fedistream_track_artists'
AND pm.meta_value LIKE %s",
'%i:' . $artist_id . ';%'
)
);
}
return (int) $count;
}
/**
* Get track play count.
*
* @param int $track_id Track post ID.
* @return int Play count.
*/
private function get_track_plays( int $track_id ): int {
global $wpdb;
$table = $wpdb->prefix . 'fedistream_plays';
// Check if table exists.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$table_exists = $wpdb->get_var(
$wpdb->prepare(
'SHOW TABLES LIKE %s',
$table
)
);
if ( ! $table_exists ) {
return 0;
}
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM $table WHERE track_id = %d",
$track_id
)
);
return (int) $count;
}
}

1
includes/Admin/index.php Normal file
View File

@@ -0,0 +1 @@
<?php // Silence is golden.