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:
433
includes/ActivityPub/AlbumTransformer.php
Normal file
433
includes/ActivityPub/AlbumTransformer.php
Normal file
@@ -0,0 +1,433 @@
|
||||
<?php
|
||||
/**
|
||||
* Album Transformer for ActivityPub.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\ActivityPub;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms Album posts to ActivityPub Collection objects.
|
||||
*/
|
||||
class AlbumTransformer {
|
||||
|
||||
/**
|
||||
* The album post.
|
||||
*
|
||||
* @var \WP_Post
|
||||
*/
|
||||
protected \WP_Post $post;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \WP_Post $post The album post.
|
||||
*/
|
||||
public function __construct( \WP_Post $post ) {
|
||||
$this->post = $post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ActivityPub object type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_type(): string {
|
||||
return 'Collection';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the object ID (URI).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_id(): string {
|
||||
return get_permalink( $this->post->ID );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the object name (title).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string {
|
||||
$album_type = get_post_meta( $this->post->ID, '_fedistream_album_type', true );
|
||||
$type_label = '';
|
||||
|
||||
switch ( $album_type ) {
|
||||
case 'ep':
|
||||
$type_label = ' (EP)';
|
||||
break;
|
||||
case 'single':
|
||||
$type_label = ' (Single)';
|
||||
break;
|
||||
case 'compilation':
|
||||
$type_label = ' (Compilation)';
|
||||
break;
|
||||
}
|
||||
|
||||
return $this->post->post_title . $type_label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content (liner notes/description).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_content(): string {
|
||||
$content = $this->post->post_content;
|
||||
|
||||
// Apply content filters for proper formatting.
|
||||
$content = apply_filters( 'the_content', $content );
|
||||
|
||||
return wp_kses_post( $content );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the summary (excerpt).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_summary(): string {
|
||||
if ( ! empty( $this->post->post_excerpt ) ) {
|
||||
return wp_strip_all_tags( $this->post->post_excerpt );
|
||||
}
|
||||
|
||||
// Build summary from album info.
|
||||
$artist_id = get_post_meta( $this->post->ID, '_fedistream_album_artist', true );
|
||||
$artist = $artist_id ? get_post( $artist_id ) : null;
|
||||
|
||||
$release_date = get_post_meta( $this->post->ID, '_fedistream_release_date', true );
|
||||
$track_count = (int) get_post_meta( $this->post->ID, '_fedistream_total_tracks', true );
|
||||
|
||||
$summary = '';
|
||||
if ( $artist ) {
|
||||
$summary .= sprintf( __( 'By %s', 'wp-fedistream' ), $artist->post_title );
|
||||
}
|
||||
if ( $release_date ) {
|
||||
/* translators: %s: release date */
|
||||
$summary .= ' ' . sprintf( __( '- Released %s', 'wp-fedistream' ), $release_date );
|
||||
}
|
||||
if ( $track_count ) {
|
||||
/* translators: %d: number of tracks */
|
||||
$summary .= ' ' . sprintf( _n( '- %d track', '- %d tracks', $track_count, 'wp-fedistream' ), $track_count );
|
||||
}
|
||||
|
||||
return trim( $summary );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL (permalink).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_url(): string {
|
||||
return get_permalink( $this->post->ID );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attributed actor.
|
||||
*
|
||||
* @return string Artist URI.
|
||||
*/
|
||||
public function get_attributed_to(): string {
|
||||
$artist_id = get_post_meta( $this->post->ID, '_fedistream_album_artist', true );
|
||||
|
||||
if ( ! $artist_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return get_permalink( $artist_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the published date.
|
||||
*
|
||||
* @return string ISO 8601 date.
|
||||
*/
|
||||
public function get_published(): string {
|
||||
// Use release date if available, otherwise post date.
|
||||
$release_date = get_post_meta( $this->post->ID, '_fedistream_release_date', true );
|
||||
|
||||
if ( $release_date ) {
|
||||
$date = \DateTime::createFromFormat( 'Y-m-d', $release_date );
|
||||
if ( $date ) {
|
||||
return $date->format( 'c' );
|
||||
}
|
||||
}
|
||||
|
||||
return get_the_date( 'c', $this->post );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the updated date.
|
||||
*
|
||||
* @return string ISO 8601 date.
|
||||
*/
|
||||
public function get_updated(): string {
|
||||
return get_the_modified_date( 'c', $this->post );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total duration.
|
||||
*
|
||||
* @return string ISO 8601 duration.
|
||||
*/
|
||||
public function get_duration(): string {
|
||||
$seconds = (int) get_post_meta( $this->post->ID, '_fedistream_total_duration', true );
|
||||
|
||||
if ( ! $seconds ) {
|
||||
// Calculate from tracks.
|
||||
$tracks = $this->get_tracks();
|
||||
foreach ( $tracks as $track ) {
|
||||
$seconds += (int) get_post_meta( $track->ID, '_fedistream_duration', true );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $seconds ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->format_duration_iso8601( $seconds );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tracks in this album.
|
||||
*
|
||||
* @return array Array of WP_Post objects.
|
||||
*/
|
||||
public function get_tracks(): array {
|
||||
$args = 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' => $this->post->ID,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$query = new \WP_Query( $args );
|
||||
|
||||
return $query->posts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total item count.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_total_items(): int {
|
||||
$count = (int) get_post_meta( $this->post->ID, '_fedistream_total_tracks', true );
|
||||
|
||||
if ( ! $count ) {
|
||||
$count = count( $this->get_tracks() );
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the collection items (track URIs).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_items(): array {
|
||||
$tracks = $this->get_tracks();
|
||||
$items = array();
|
||||
|
||||
foreach ( $tracks as $track ) {
|
||||
$transformer = new TrackTransformer( $track );
|
||||
$items[] = $transformer->to_object();
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the image/artwork attachment.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_image_attachment(): ?array {
|
||||
$thumbnail_id = get_post_thumbnail_id( $this->post->ID );
|
||||
|
||||
if ( ! $thumbnail_id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$image = wp_get_attachment_image_src( $thumbnail_id, 'medium' );
|
||||
if ( ! $image ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'type' => 'Image',
|
||||
'mediaType' => get_post_mime_type( $thumbnail_id ),
|
||||
'url' => $image[0],
|
||||
'width' => $image[1],
|
||||
'height' => $image[2],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tags (genres).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_tags(): array {
|
||||
$tags = array();
|
||||
|
||||
// Get genres.
|
||||
$genres = get_the_terms( $this->post->ID, 'fedistream_genre' );
|
||||
if ( $genres && ! is_wp_error( $genres ) ) {
|
||||
foreach ( $genres as $genre ) {
|
||||
$tags[] = array(
|
||||
'type' => 'Hashtag',
|
||||
'name' => '#' . sanitize_title( $genre->name ),
|
||||
'href' => get_term_link( $genre ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform to ActivityPub object array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_object(): array {
|
||||
$object = array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => $this->get_type(),
|
||||
'id' => $this->get_id(),
|
||||
'name' => $this->get_name(),
|
||||
'summary' => $this->get_summary(),
|
||||
'content' => $this->get_content(),
|
||||
'url' => $this->get_url(),
|
||||
'attributedTo' => $this->get_attributed_to(),
|
||||
'published' => $this->get_published(),
|
||||
'updated' => $this->get_updated(),
|
||||
'totalItems' => $this->get_total_items(),
|
||||
'items' => $this->get_items(),
|
||||
);
|
||||
|
||||
// Add duration.
|
||||
$duration = $this->get_duration();
|
||||
if ( $duration ) {
|
||||
$object['duration'] = $duration;
|
||||
}
|
||||
|
||||
// Add image.
|
||||
$image = $this->get_image_attachment();
|
||||
if ( $image ) {
|
||||
$object['image'] = $image;
|
||||
}
|
||||
|
||||
// Add tags.
|
||||
$tags = $this->get_tags();
|
||||
if ( ! empty( $tags ) ) {
|
||||
$object['tag'] = $tags;
|
||||
}
|
||||
|
||||
// Add album-specific metadata.
|
||||
$album_type = get_post_meta( $this->post->ID, '_fedistream_album_type', true );
|
||||
if ( $album_type ) {
|
||||
$object['albumType'] = $album_type;
|
||||
}
|
||||
|
||||
$upc = get_post_meta( $this->post->ID, '_fedistream_upc', true );
|
||||
if ( $upc ) {
|
||||
$object['upc'] = $upc;
|
||||
}
|
||||
|
||||
$catalog = get_post_meta( $this->post->ID, '_fedistream_catalog_number', true );
|
||||
if ( $catalog ) {
|
||||
$object['catalogNumber'] = $catalog;
|
||||
}
|
||||
|
||||
$release_date = get_post_meta( $this->post->ID, '_fedistream_release_date', true );
|
||||
if ( $release_date ) {
|
||||
$object['releaseDate'] = $release_date;
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Create activity for this album.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_create_activity(): array {
|
||||
$actor = $this->get_attributed_to();
|
||||
|
||||
return array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Create',
|
||||
'id' => $this->get_id() . '#activity-create',
|
||||
'actor' => $actor,
|
||||
'published' => $this->get_published(),
|
||||
'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ),
|
||||
'cc' => array( $actor . '/followers' ),
|
||||
'object' => $this->to_object(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Announce activity for this album.
|
||||
*
|
||||
* @param string $actor_uri The actor announcing the album.
|
||||
* @return array
|
||||
*/
|
||||
public function to_announce_activity( string $actor_uri ): array {
|
||||
return array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Announce',
|
||||
'id' => $this->get_id() . '#activity-announce-' . time(),
|
||||
'actor' => $actor_uri,
|
||||
'published' => gmdate( 'c' ),
|
||||
'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ),
|
||||
'cc' => array( $actor_uri . '/followers' ),
|
||||
'object' => $this->get_id(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration as ISO 8601.
|
||||
*
|
||||
* @param int $seconds The duration in seconds.
|
||||
* @return string ISO 8601 duration.
|
||||
*/
|
||||
private function format_duration_iso8601( int $seconds ): string {
|
||||
$hours = floor( $seconds / 3600 );
|
||||
$minutes = floor( ( $seconds % 3600 ) / 60 );
|
||||
$secs = $seconds % 60;
|
||||
|
||||
$duration = 'PT';
|
||||
if ( $hours > 0 ) {
|
||||
$duration .= $hours . 'H';
|
||||
}
|
||||
if ( $minutes > 0 ) {
|
||||
$duration .= $minutes . 'M';
|
||||
}
|
||||
if ( $secs > 0 || ( $hours === 0 && $minutes === 0 ) ) {
|
||||
$duration .= $secs . 'S';
|
||||
}
|
||||
|
||||
return $duration;
|
||||
}
|
||||
}
|
||||
614
includes/ActivityPub/ArtistActor.php
Normal file
614
includes/ActivityPub/ArtistActor.php
Normal file
@@ -0,0 +1,614 @@
|
||||
<?php
|
||||
/**
|
||||
* Artist ActivityPub Actor.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\ActivityPub;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an artist as an ActivityPub actor.
|
||||
*/
|
||||
class ArtistActor {
|
||||
|
||||
/**
|
||||
* The artist post.
|
||||
*
|
||||
* @var \WP_Post
|
||||
*/
|
||||
private \WP_Post $artist;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \WP_Post $artist The artist post.
|
||||
*/
|
||||
public function __construct( \WP_Post $artist ) {
|
||||
$this->artist = $artist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actor ID (URI).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_id(): string {
|
||||
return get_permalink( $this->artist->ID );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actor type.
|
||||
*
|
||||
* @return string Person for solo, Group for bands.
|
||||
*/
|
||||
public function get_type(): string {
|
||||
$artist_type = get_post_meta( $this->artist->ID, '_fedistream_artist_type', true );
|
||||
|
||||
// Bands, duos, collectives are Groups.
|
||||
if ( in_array( $artist_type, array( 'band', 'duo', 'collective', 'orchestra' ), true ) ) {
|
||||
return 'Group';
|
||||
}
|
||||
|
||||
return 'Person';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the preferred username.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_preferred_username(): string {
|
||||
return 'artist-' . $this->artist->ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the display name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return $this->artist->post_title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actor summary/bio.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_summary(): string {
|
||||
if ( ! empty( $this->artist->post_excerpt ) ) {
|
||||
return wp_kses_post( $this->artist->post_excerpt );
|
||||
}
|
||||
|
||||
// Use first paragraph of content as summary.
|
||||
$content = wp_strip_all_tags( $this->artist->post_content );
|
||||
$parts = explode( "\n\n", $content, 2 );
|
||||
|
||||
return ! empty( $parts[0] ) ? wp_trim_words( $parts[0], 50 ) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actor URL (profile page).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_url(): string {
|
||||
return get_permalink( $this->artist->ID );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the inbox URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_inbox(): string {
|
||||
return trailingslashit( get_permalink( $this->artist->ID ) ) . 'inbox';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the outbox URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_outbox(): string {
|
||||
return trailingslashit( get_permalink( $this->artist->ID ) ) . 'outbox';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the followers URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_followers(): string {
|
||||
return trailingslashit( get_permalink( $this->artist->ID ) ) . 'followers';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the following URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_following(): string {
|
||||
return trailingslashit( get_permalink( $this->artist->ID ) ) . 'following';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the avatar/icon.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_icon(): ?array {
|
||||
$thumbnail_id = get_post_thumbnail_id( $this->artist->ID );
|
||||
if ( ! $thumbnail_id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$image = wp_get_attachment_image_src( $thumbnail_id, 'thumbnail' );
|
||||
if ( ! $image ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'type' => 'Image',
|
||||
'mediaType' => get_post_mime_type( $thumbnail_id ),
|
||||
'url' => $image[0],
|
||||
'width' => $image[1],
|
||||
'height' => $image[2],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the header/banner image.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_image(): ?array {
|
||||
$banner_id = get_post_meta( $this->artist->ID, '_fedistream_artist_banner', true );
|
||||
if ( ! $banner_id ) {
|
||||
// Fall back to featured image at larger size.
|
||||
$banner_id = get_post_thumbnail_id( $this->artist->ID );
|
||||
}
|
||||
|
||||
if ( ! $banner_id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$image = wp_get_attachment_image_src( $banner_id, 'large' );
|
||||
if ( ! $image ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'type' => 'Image',
|
||||
'mediaType' => get_post_mime_type( $banner_id ),
|
||||
'url' => $image[0],
|
||||
'width' => $image[1],
|
||||
'height' => $image[2],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the public key.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_public_key(): array {
|
||||
$key = get_post_meta( $this->artist->ID, '_fedistream_activitypub_public_key', true );
|
||||
|
||||
if ( ! $key ) {
|
||||
// Generate key pair if not exists.
|
||||
$this->generate_keys();
|
||||
$key = get_post_meta( $this->artist->ID, '_fedistream_activitypub_public_key', true );
|
||||
}
|
||||
|
||||
return array(
|
||||
'id' => $this->get_id() . '#main-key',
|
||||
'owner' => $this->get_id(),
|
||||
'publicKeyPem' => $key,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate RSA key pair for the artist.
|
||||
*
|
||||
* @return bool True on success.
|
||||
*/
|
||||
private function generate_keys(): bool {
|
||||
$config = array(
|
||||
'private_key_bits' => 2048,
|
||||
'private_key_type' => OPENSSL_KEYTYPE_RSA,
|
||||
);
|
||||
|
||||
$resource = openssl_pkey_new( $config );
|
||||
if ( ! $resource ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Export private key.
|
||||
openssl_pkey_export( $resource, $private_key );
|
||||
|
||||
// Get public key.
|
||||
$details = openssl_pkey_get_details( $resource );
|
||||
$public_key = $details['key'] ?? '';
|
||||
|
||||
if ( ! $private_key || ! $public_key ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
update_post_meta( $this->artist->ID, '_fedistream_activitypub_private_key', $private_key );
|
||||
update_post_meta( $this->artist->ID, '_fedistream_activitypub_public_key', $public_key );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attachment properties (social links, etc.).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_attachment(): array {
|
||||
$attachments = array();
|
||||
|
||||
// Add website.
|
||||
$website = get_post_meta( $this->artist->ID, '_fedistream_artist_website', true );
|
||||
if ( $website ) {
|
||||
$attachments[] = array(
|
||||
'type' => 'PropertyValue',
|
||||
'name' => __( 'Website', 'wp-fedistream' ),
|
||||
'value' => sprintf( '<a href="%s" rel="me nofollow noopener" target="_blank">%s</a>', esc_url( $website ), esc_html( $website ) ),
|
||||
);
|
||||
}
|
||||
|
||||
// Add location.
|
||||
$location = get_post_meta( $this->artist->ID, '_fedistream_artist_location', true );
|
||||
if ( $location ) {
|
||||
$attachments[] = array(
|
||||
'type' => 'PropertyValue',
|
||||
'name' => __( 'Location', 'wp-fedistream' ),
|
||||
'value' => esc_html( $location ),
|
||||
);
|
||||
}
|
||||
|
||||
// Add social links.
|
||||
$social_links = get_post_meta( $this->artist->ID, '_fedistream_artist_social_links', true );
|
||||
if ( is_array( $social_links ) ) {
|
||||
foreach ( $social_links as $link ) {
|
||||
$platform = $link['platform'] ?? '';
|
||||
$url = $link['url'] ?? '';
|
||||
|
||||
if ( $platform && $url ) {
|
||||
$attachments[] = array(
|
||||
'type' => 'PropertyValue',
|
||||
'name' => esc_html( ucfirst( $platform ) ),
|
||||
'value' => sprintf( '<a href="%s" rel="me nofollow noopener" target="_blank">%s</a>', esc_url( $url ), esc_html( $url ) ),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add formed date.
|
||||
$formed = get_post_meta( $this->artist->ID, '_fedistream_artist_formed_date', true );
|
||||
if ( $formed ) {
|
||||
$attachments[] = array(
|
||||
'type' => 'PropertyValue',
|
||||
'name' => __( 'Active Since', 'wp-fedistream' ),
|
||||
'value' => esc_html( $formed ),
|
||||
);
|
||||
}
|
||||
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full actor object as an array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_array(): array {
|
||||
$actor = array(
|
||||
'@context' => array(
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1',
|
||||
array(
|
||||
'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
|
||||
'PropertyValue' => 'schema:PropertyValue',
|
||||
'value' => 'schema:value',
|
||||
),
|
||||
),
|
||||
'id' => $this->get_id(),
|
||||
'type' => $this->get_type(),
|
||||
'preferredUsername' => $this->get_preferred_username(),
|
||||
'name' => $this->get_name(),
|
||||
'summary' => $this->get_summary(),
|
||||
'url' => $this->get_url(),
|
||||
'inbox' => $this->get_inbox(),
|
||||
'outbox' => $this->get_outbox(),
|
||||
'followers' => $this->get_followers(),
|
||||
'following' => $this->get_following(),
|
||||
'publicKey' => $this->get_public_key(),
|
||||
'manuallyApprovesFollowers' => false,
|
||||
'published' => get_the_date( 'c', $this->artist ),
|
||||
'updated' => get_the_modified_date( 'c', $this->artist ),
|
||||
);
|
||||
|
||||
// Add icon if available.
|
||||
$icon = $this->get_icon();
|
||||
if ( $icon ) {
|
||||
$actor['icon'] = $icon;
|
||||
}
|
||||
|
||||
// Add image if available.
|
||||
$image = $this->get_image();
|
||||
if ( $image ) {
|
||||
$actor['image'] = $image;
|
||||
}
|
||||
|
||||
// Add attachments.
|
||||
$attachments = $this->get_attachment();
|
||||
if ( ! empty( $attachments ) ) {
|
||||
$actor['attachment'] = $attachments;
|
||||
}
|
||||
|
||||
return $actor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get webfinger data for the artist.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_webfinger(): array {
|
||||
$host = wp_parse_url( home_url(), PHP_URL_HOST );
|
||||
$resource = 'acct:' . $this->get_preferred_username() . '@' . $host;
|
||||
|
||||
return array(
|
||||
'subject' => $resource,
|
||||
'aliases' => array(
|
||||
$this->get_id(),
|
||||
$this->get_url(),
|
||||
),
|
||||
'links' => array(
|
||||
array(
|
||||
'rel' => 'self',
|
||||
'type' => 'application/activity+json',
|
||||
'href' => $this->get_id(),
|
||||
),
|
||||
array(
|
||||
'rel' => 'http://webfinger.net/rel/profile-page',
|
||||
'type' => 'text/html',
|
||||
'href' => $this->get_url(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the outbox collection.
|
||||
*
|
||||
* @param int $page The page number (0 for summary).
|
||||
* @param int $per_page Items per page.
|
||||
* @return array
|
||||
*/
|
||||
public function get_outbox_collection( int $page = 0, int $per_page = 20 ): array {
|
||||
// Get all tracks and albums by this artist.
|
||||
$args = array(
|
||||
'post_type' => array( 'fedistream_track', 'fedistream_album' ),
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'relation' => 'OR',
|
||||
array(
|
||||
'key' => '_fedistream_album_artist',
|
||||
'value' => $this->artist->ID,
|
||||
),
|
||||
array(
|
||||
'key' => '_fedistream_artist_ids',
|
||||
'value' => $this->artist->ID,
|
||||
'compare' => 'LIKE',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$query = new \WP_Query( $args );
|
||||
$total = $query->found_posts;
|
||||
|
||||
// Return summary if page 0.
|
||||
if ( $page === 0 ) {
|
||||
return array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $this->get_outbox(),
|
||||
'type' => 'OrderedCollection',
|
||||
'totalItems' => $total,
|
||||
'first' => $this->get_outbox() . '?page=1',
|
||||
'last' => $this->get_outbox() . '?page=' . max( 1, ceil( $total / $per_page ) ),
|
||||
);
|
||||
}
|
||||
|
||||
// Get paginated items.
|
||||
$args['posts_per_page'] = $per_page;
|
||||
$args['paged'] = $page;
|
||||
$args['orderby'] = 'date';
|
||||
$args['order'] = 'DESC';
|
||||
|
||||
$query = new \WP_Query( $args );
|
||||
$items = array();
|
||||
|
||||
foreach ( $query->posts as $post ) {
|
||||
$items[] = $this->post_to_create_activity( $post );
|
||||
}
|
||||
|
||||
$collection = array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $this->get_outbox() . '?page=' . $page,
|
||||
'type' => 'OrderedCollectionPage',
|
||||
'partOf' => $this->get_outbox(),
|
||||
'orderedItems' => $items,
|
||||
);
|
||||
|
||||
// Add prev/next links.
|
||||
if ( $page > 1 ) {
|
||||
$collection['prev'] = $this->get_outbox() . '?page=' . ( $page - 1 );
|
||||
}
|
||||
|
||||
$total_pages = ceil( $total / $per_page );
|
||||
if ( $page < $total_pages ) {
|
||||
$collection['next'] = $this->get_outbox() . '?page=' . ( $page + 1 );
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a post to a Create activity.
|
||||
*
|
||||
* @param \WP_Post $post The post.
|
||||
* @return array
|
||||
*/
|
||||
private function post_to_create_activity( \WP_Post $post ): array {
|
||||
$object = array(
|
||||
'type' => 'fedistream_track' === $post->post_type ? 'Audio' : 'Collection',
|
||||
'id' => get_permalink( $post->ID ),
|
||||
'name' => $post->post_title,
|
||||
'content' => wp_strip_all_tags( $post->post_content ),
|
||||
'attributedTo' => $this->get_id(),
|
||||
'published' => get_the_date( 'c', $post ),
|
||||
'url' => get_permalink( $post->ID ),
|
||||
);
|
||||
|
||||
// Add track-specific properties.
|
||||
if ( 'fedistream_track' === $post->post_type ) {
|
||||
$duration = get_post_meta( $post->ID, '_fedistream_duration', true );
|
||||
if ( $duration ) {
|
||||
$object['duration'] = $this->format_duration_iso8601( (int) $duration );
|
||||
}
|
||||
|
||||
$audio_id = get_post_meta( $post->ID, '_fedistream_audio_file', true );
|
||||
$audio_url = $audio_id ? wp_get_attachment_url( $audio_id ) : '';
|
||||
if ( $audio_url ) {
|
||||
$object['attachment'] = array(
|
||||
'type' => 'Audio',
|
||||
'mediaType' => 'audio/mpeg',
|
||||
'url' => $audio_url,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add thumbnail.
|
||||
$thumbnail_id = get_post_thumbnail_id( $post->ID );
|
||||
if ( $thumbnail_id ) {
|
||||
$image = wp_get_attachment_image_src( $thumbnail_id, 'medium' );
|
||||
if ( $image ) {
|
||||
$object['image'] = array(
|
||||
'type' => 'Image',
|
||||
'mediaType' => get_post_mime_type( $thumbnail_id ),
|
||||
'url' => $image[0],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'type' => 'Create',
|
||||
'id' => get_permalink( $post->ID ) . '#activity-create',
|
||||
'actor' => $this->get_id(),
|
||||
'published' => get_the_date( 'c', $post ),
|
||||
'object' => $object,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration as ISO 8601.
|
||||
*
|
||||
* @param int $seconds The duration in seconds.
|
||||
* @return string ISO 8601 duration.
|
||||
*/
|
||||
private function format_duration_iso8601( int $seconds ): string {
|
||||
$hours = floor( $seconds / 3600 );
|
||||
$minutes = floor( ( $seconds % 3600 ) / 60 );
|
||||
$secs = $seconds % 60;
|
||||
|
||||
$duration = 'PT';
|
||||
if ( $hours > 0 ) {
|
||||
$duration .= $hours . 'H';
|
||||
}
|
||||
if ( $minutes > 0 ) {
|
||||
$duration .= $minutes . 'M';
|
||||
}
|
||||
if ( $secs > 0 || ( $hours === 0 && $minutes === 0 ) ) {
|
||||
$duration .= $secs . 'S';
|
||||
}
|
||||
|
||||
return $duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the followers collection.
|
||||
*
|
||||
* @param int $page The page number (0 for summary).
|
||||
* @param int $per_page Items per page.
|
||||
* @return array
|
||||
*/
|
||||
public function get_followers_collection( int $page = 0, int $per_page = 20 ): array {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_followers';
|
||||
|
||||
// Get total count.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$total = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE artist_id = %d",
|
||||
$this->artist->ID
|
||||
)
|
||||
);
|
||||
|
||||
// Return summary if page 0.
|
||||
if ( $page === 0 ) {
|
||||
return array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $this->get_followers(),
|
||||
'type' => 'OrderedCollection',
|
||||
'totalItems' => $total,
|
||||
'first' => $this->get_followers() . '?page=1',
|
||||
);
|
||||
}
|
||||
|
||||
// Get paginated items.
|
||||
$offset = ( $page - 1 ) * $per_page;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$followers = $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"SELECT follower_uri FROM {$table} WHERE artist_id = %d ORDER BY followed_at DESC LIMIT %d OFFSET %d",
|
||||
$this->artist->ID,
|
||||
$per_page,
|
||||
$offset
|
||||
)
|
||||
);
|
||||
|
||||
$collection = array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $this->get_followers() . '?page=' . $page,
|
||||
'type' => 'OrderedCollectionPage',
|
||||
'partOf' => $this->get_followers(),
|
||||
'orderedItems' => $followers ?: array(),
|
||||
);
|
||||
|
||||
// Add prev/next links.
|
||||
if ( $page > 1 ) {
|
||||
$collection['prev'] = $this->get_followers() . '?page=' . ( $page - 1 );
|
||||
}
|
||||
|
||||
$total_pages = ceil( $total / $per_page );
|
||||
if ( $page < $total_pages ) {
|
||||
$collection['next'] = $this->get_followers() . '?page=' . ( $page + 1 );
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
}
|
||||
477
includes/ActivityPub/FollowerHandler.php
Normal file
477
includes/ActivityPub/FollowerHandler.php
Normal file
@@ -0,0 +1,477 @@
|
||||
<?php
|
||||
/**
|
||||
* Follower Handler for ActivityPub.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\ActivityPub;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles ActivityPub follower management.
|
||||
*/
|
||||
class FollowerHandler {
|
||||
|
||||
/**
|
||||
* The table name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private string $table;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
global $wpdb;
|
||||
$this->table = $wpdb->prefix . 'fedistream_followers';
|
||||
|
||||
// Register inbox handlers for follow activities.
|
||||
add_action( 'activitypub_inbox_follow', array( $this, 'handle_follow' ), 10, 2 );
|
||||
add_action( 'activitypub_inbox_undo', array( $this, 'handle_undo' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming Follow activity.
|
||||
*
|
||||
* @param array $activity The activity data.
|
||||
* @param int $user_id The local user ID (if applicable).
|
||||
* @return void
|
||||
*/
|
||||
public function handle_follow( array $activity, int $user_id ): void {
|
||||
$actor = $activity['actor'] ?? '';
|
||||
$object = $activity['object'] ?? '';
|
||||
|
||||
if ( ! $actor || ! $object ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the artist being followed.
|
||||
$artist_id = $this->get_artist_from_object( $object );
|
||||
if ( ! $artist_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add follower.
|
||||
$result = $this->add_follower( $artist_id, $actor, $activity );
|
||||
|
||||
if ( $result ) {
|
||||
// Send Accept activity.
|
||||
$this->send_accept( $artist_id, $activity );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming Undo activity (for Unfollow).
|
||||
*
|
||||
* @param array $activity The activity data.
|
||||
* @param int $user_id The local user ID (if applicable).
|
||||
* @return void
|
||||
*/
|
||||
public function handle_undo( array $activity, int $user_id ): void {
|
||||
$actor = $activity['actor'] ?? '';
|
||||
$object = $activity['object'] ?? array();
|
||||
|
||||
if ( ! $actor || ! is_array( $object ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is an Undo Follow.
|
||||
$type = $object['type'] ?? '';
|
||||
if ( 'Follow' !== $type ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$follow_object = $object['object'] ?? '';
|
||||
if ( ! $follow_object ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the artist being unfollowed.
|
||||
$artist_id = $this->get_artist_from_object( $follow_object );
|
||||
if ( ! $artist_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove follower.
|
||||
$this->remove_follower( $artist_id, $actor );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get artist ID from an object URI.
|
||||
*
|
||||
* @param string $object The object URI.
|
||||
* @return int|null Artist ID or null.
|
||||
*/
|
||||
private function get_artist_from_object( string $object ): ?int {
|
||||
// Try to find by permalink.
|
||||
$post_id = url_to_postid( $object );
|
||||
|
||||
if ( $post_id ) {
|
||||
$post = get_post( $post_id );
|
||||
if ( $post && 'fedistream_artist' === $post->post_type ) {
|
||||
return $post_id;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find by artist handle pattern.
|
||||
if ( preg_match( '/artist-(\d+)/', $object, $matches ) ) {
|
||||
$artist_id = absint( $matches[1] );
|
||||
$post = get_post( $artist_id );
|
||||
if ( $post && 'fedistream_artist' === $post->post_type ) {
|
||||
return $artist_id;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a follower.
|
||||
*
|
||||
* @param int $artist_id The artist ID.
|
||||
* @param string $follower_uri The follower's actor URI.
|
||||
* @param array $activity_data The original activity data.
|
||||
* @return bool True on success.
|
||||
*/
|
||||
public function add_follower( int $artist_id, string $follower_uri, array $activity_data = array() ): bool {
|
||||
global $wpdb;
|
||||
|
||||
// Check if already following.
|
||||
if ( $this->is_following( $artist_id, $follower_uri ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fetch follower info.
|
||||
$follower_info = $this->fetch_actor( $follower_uri );
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$result = $wpdb->insert(
|
||||
$this->table,
|
||||
array(
|
||||
'artist_id' => $artist_id,
|
||||
'follower_uri' => $follower_uri,
|
||||
'follower_name' => $follower_info['name'] ?? '',
|
||||
'follower_icon' => $follower_info['icon']['url'] ?? '',
|
||||
'inbox' => $follower_info['inbox'] ?? '',
|
||||
'shared_inbox' => $follower_info['endpoints']['sharedInbox'] ?? $follower_info['sharedInbox'] ?? '',
|
||||
'activity_data' => wp_json_encode( $activity_data ),
|
||||
'followed_at' => current_time( 'mysql' ),
|
||||
),
|
||||
array( '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
|
||||
);
|
||||
|
||||
if ( $result ) {
|
||||
// Update follower count.
|
||||
$count = $this->get_follower_count( $artist_id );
|
||||
update_post_meta( $artist_id, '_fedistream_follower_count', $count );
|
||||
|
||||
do_action( 'fedistream_artist_followed', $artist_id, $follower_uri, $follower_info );
|
||||
}
|
||||
|
||||
return (bool) $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a follower.
|
||||
*
|
||||
* @param int $artist_id The artist ID.
|
||||
* @param string $follower_uri The follower's actor URI.
|
||||
* @return bool True on success.
|
||||
*/
|
||||
public function remove_follower( int $artist_id, string $follower_uri ): bool {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$result = $wpdb->delete(
|
||||
$this->table,
|
||||
array(
|
||||
'artist_id' => $artist_id,
|
||||
'follower_uri' => $follower_uri,
|
||||
),
|
||||
array( '%d', '%s' )
|
||||
);
|
||||
|
||||
if ( $result ) {
|
||||
// Update follower count.
|
||||
$count = $this->get_follower_count( $artist_id );
|
||||
update_post_meta( $artist_id, '_fedistream_follower_count', $count );
|
||||
|
||||
do_action( 'fedistream_artist_unfollowed', $artist_id, $follower_uri );
|
||||
}
|
||||
|
||||
return (bool) $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an actor is following an artist.
|
||||
*
|
||||
* @param int $artist_id The artist ID.
|
||||
* @param string $follower_uri The follower's actor URI.
|
||||
* @return bool
|
||||
*/
|
||||
public function is_following( int $artist_id, string $follower_uri ): bool {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$exists = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$this->table} WHERE artist_id = %d AND follower_uri = %s",
|
||||
$artist_id,
|
||||
$follower_uri
|
||||
)
|
||||
);
|
||||
|
||||
return $exists > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get follower count for an artist.
|
||||
*
|
||||
* @param int $artist_id The artist ID.
|
||||
* @return int
|
||||
*/
|
||||
public function get_follower_count( int $artist_id ): int {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$this->table} WHERE artist_id = %d",
|
||||
$artist_id
|
||||
)
|
||||
);
|
||||
|
||||
return (int) $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get followers for an artist.
|
||||
*
|
||||
* @param int $artist_id The artist ID.
|
||||
* @param int $limit Maximum number to return.
|
||||
* @param int $offset Offset for pagination.
|
||||
* @return array
|
||||
*/
|
||||
public function get_followers( int $artist_id, int $limit = 20, int $offset = 0 ): array {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$followers = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$this->table} WHERE artist_id = %d ORDER BY followed_at DESC LIMIT %d OFFSET %d",
|
||||
$artist_id,
|
||||
$limit,
|
||||
$offset
|
||||
)
|
||||
);
|
||||
|
||||
return $followers ?: array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all follower inboxes for an artist.
|
||||
*
|
||||
* @param int $artist_id The artist ID.
|
||||
* @return array Array of inbox URLs, with shared inboxes deduplicated.
|
||||
*/
|
||||
public function get_follower_inboxes( int $artist_id ): array {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$results = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT inbox, shared_inbox FROM {$this->table} WHERE artist_id = %d",
|
||||
$artist_id
|
||||
)
|
||||
);
|
||||
|
||||
$inboxes = array();
|
||||
$shared_inboxes = array();
|
||||
|
||||
foreach ( $results as $row ) {
|
||||
// Prefer shared inbox for efficiency.
|
||||
if ( ! empty( $row->shared_inbox ) ) {
|
||||
$shared_inboxes[ $row->shared_inbox ] = true;
|
||||
} elseif ( ! empty( $row->inbox ) ) {
|
||||
$inboxes[ $row->inbox ] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Return unique inboxes (shared inboxes + individual inboxes).
|
||||
return array_merge( array_keys( $shared_inboxes ), array_keys( $inboxes ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Accept activity in response to Follow.
|
||||
*
|
||||
* @param int $artist_id The artist ID.
|
||||
* @param array $activity The original Follow activity.
|
||||
* @return bool True on success.
|
||||
*/
|
||||
private function send_accept( int $artist_id, array $activity ): bool {
|
||||
$artist = get_post( $artist_id );
|
||||
if ( ! $artist ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$actor = new ArtistActor( $artist );
|
||||
$follower_uri = $activity['actor'] ?? '';
|
||||
|
||||
if ( ! $follower_uri ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create Accept activity.
|
||||
$accept = array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Accept',
|
||||
'id' => $actor->get_id() . '#accept-' . time(),
|
||||
'actor' => $actor->get_id(),
|
||||
'object' => $activity,
|
||||
);
|
||||
|
||||
// Get follower's inbox.
|
||||
$follower_info = $this->fetch_actor( $follower_uri );
|
||||
$inbox = $follower_info['inbox'] ?? '';
|
||||
|
||||
if ( ! $inbox ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Send the Accept activity.
|
||||
return $this->send_activity( $accept, $inbox, $artist_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch an actor from a remote server.
|
||||
*
|
||||
* @param string $actor_uri The actor URI.
|
||||
* @return array The actor data.
|
||||
*/
|
||||
private function fetch_actor( string $actor_uri ): array {
|
||||
$response = wp_remote_get(
|
||||
$actor_uri,
|
||||
array(
|
||||
'headers' => array(
|
||||
'Accept' => 'application/activity+json, application/ld+json',
|
||||
),
|
||||
'timeout' => 10,
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
$data = json_decode( $body, true );
|
||||
|
||||
return is_array( $data ) ? $data : array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an activity to an inbox.
|
||||
*
|
||||
* @param array $activity The activity to send.
|
||||
* @param string $inbox The inbox URL.
|
||||
* @param int $artist_id The artist ID (for signing).
|
||||
* @return bool True on success.
|
||||
*/
|
||||
private function send_activity( array $activity, string $inbox, int $artist_id ): bool {
|
||||
$artist = get_post( $artist_id );
|
||||
if ( ! $artist ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the artist's private key for signing.
|
||||
$private_key = get_post_meta( $artist_id, '_fedistream_activitypub_private_key', true );
|
||||
if ( ! $private_key ) {
|
||||
// Generate keys if not exists.
|
||||
$actor = new ArtistActor( $artist );
|
||||
$actor->get_public_key(); // This triggers key generation.
|
||||
$private_key = get_post_meta( $artist_id, '_fedistream_activitypub_private_key', true );
|
||||
}
|
||||
|
||||
if ( ! $private_key ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$body = wp_json_encode( $activity );
|
||||
$date = gmdate( 'D, d M Y H:i:s T' );
|
||||
|
||||
// Parse inbox URL.
|
||||
$parsed = wp_parse_url( $inbox );
|
||||
$host = $parsed['host'] ?? '';
|
||||
$path = $parsed['path'] ?? '/';
|
||||
|
||||
// Create signature string.
|
||||
$string_to_sign = "(request-target): post {$path}\n";
|
||||
$string_to_sign .= "host: {$host}\n";
|
||||
$string_to_sign .= "date: {$date}\n";
|
||||
$string_to_sign .= 'digest: SHA-256=' . base64_encode( hash( 'sha256', $body, true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
|
||||
// Sign the string.
|
||||
$signature = '';
|
||||
openssl_sign( $string_to_sign, $signature, $private_key, OPENSSL_ALGO_SHA256 );
|
||||
$signature_b64 = base64_encode( $signature ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
|
||||
// Build signature header.
|
||||
$actor = new ArtistActor( $artist );
|
||||
$key_id = $actor->get_id() . '#main-key';
|
||||
$signature_header = sprintf(
|
||||
'keyId="%s",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="%s"',
|
||||
$key_id,
|
||||
$signature_b64
|
||||
);
|
||||
|
||||
// Send the request.
|
||||
$response = wp_remote_post(
|
||||
$inbox,
|
||||
array(
|
||||
'headers' => array(
|
||||
'Content-Type' => 'application/activity+json',
|
||||
'Accept' => 'application/activity+json',
|
||||
'Host' => $host,
|
||||
'Date' => $date,
|
||||
'Digest' => 'SHA-256=' . base64_encode( hash( 'sha256', $body, true ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
'Signature' => $signature_header,
|
||||
),
|
||||
'body' => $body,
|
||||
'timeout' => 30,
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$code = wp_remote_retrieve_response_code( $response );
|
||||
|
||||
// Accept responses: 200, 201, 202 are all valid.
|
||||
return $code >= 200 && $code < 300;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast an activity to all followers.
|
||||
*
|
||||
* @param int $artist_id The artist ID.
|
||||
* @param array $activity The activity to broadcast.
|
||||
* @return array Results with inbox => success/failure.
|
||||
*/
|
||||
public function broadcast_activity( int $artist_id, array $activity ): array {
|
||||
$inboxes = $this->get_follower_inboxes( $artist_id );
|
||||
$results = array();
|
||||
|
||||
foreach ( $inboxes as $inbox ) {
|
||||
$results[ $inbox ] = $this->send_activity( $activity, $inbox, $artist_id );
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
480
includes/ActivityPub/Integration.php
Normal file
480
includes/ActivityPub/Integration.php
Normal file
@@ -0,0 +1,480 @@
|
||||
<?php
|
||||
/**
|
||||
* ActivityPub integration main class.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\ActivityPub;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main ActivityPub integration class.
|
||||
*
|
||||
* Integrates WP FediStream with the WordPress ActivityPub plugin.
|
||||
*/
|
||||
class Integration {
|
||||
|
||||
/**
|
||||
* Whether the ActivityPub plugin is active.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private bool $activitypub_active = false;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
// Check if ActivityPub plugin is available.
|
||||
add_action( 'plugins_loaded', array( $this, 'check_activitypub' ), 5 );
|
||||
|
||||
// Initialize integration after ActivityPub is loaded.
|
||||
add_action( 'plugins_loaded', array( $this, 'init' ), 20 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the ActivityPub plugin is active.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function check_activitypub(): void {
|
||||
$this->activitypub_active = defined( 'ACTIVITYPUB_PLUGIN_VERSION' )
|
||||
|| class_exists( '\Activitypub\Activitypub' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the ActivityPub integration.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init(): void {
|
||||
if ( ! $this->activitypub_active ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Register custom post types for ActivityPub.
|
||||
add_filter( 'activitypub_post_types', array( $this, 'register_post_types' ) );
|
||||
|
||||
// Register custom transformers.
|
||||
add_filter( 'activitypub_transformers', array( $this, 'register_transformers' ) );
|
||||
|
||||
// Register artist actors.
|
||||
add_action( 'init', array( $this, 'register_artist_actors' ), 20 );
|
||||
|
||||
// Add custom properties to ActivityPub objects.
|
||||
add_filter( 'activitypub_activity_object_array', array( $this, 'add_audio_properties' ), 10, 3 );
|
||||
|
||||
// Handle incoming activities.
|
||||
add_action( 'activitypub_inbox_like', array( $this, 'handle_like' ), 10, 2 );
|
||||
add_action( 'activitypub_inbox_announce', array( $this, 'handle_announce' ), 10, 2 );
|
||||
add_action( 'activitypub_inbox_create', array( $this, 'handle_create' ), 10, 2 );
|
||||
|
||||
// Add hooks for publishing.
|
||||
add_action( 'publish_fedistream_track', array( $this, 'on_publish_track' ), 10, 2 );
|
||||
add_action( 'publish_fedistream_album', array( $this, 'on_publish_album' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if ActivityPub is active.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_active(): bool {
|
||||
return $this->activitypub_active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register FediStream post types with ActivityPub.
|
||||
*
|
||||
* @param array $post_types The registered post types.
|
||||
* @return array Modified post types.
|
||||
*/
|
||||
public function register_post_types( array $post_types ): array {
|
||||
$post_types[] = 'fedistream_track';
|
||||
$post_types[] = 'fedistream_album';
|
||||
$post_types[] = 'fedistream_playlist';
|
||||
|
||||
return array_unique( $post_types );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register custom transformers for FediStream post types.
|
||||
*
|
||||
* @param array $transformers The registered transformers.
|
||||
* @return array Modified transformers.
|
||||
*/
|
||||
public function register_transformers( array $transformers ): array {
|
||||
$transformers['fedistream_track'] = TrackTransformer::class;
|
||||
$transformers['fedistream_album'] = AlbumTransformer::class;
|
||||
$transformers['fedistream_playlist'] = PlaylistTransformer::class;
|
||||
|
||||
return $transformers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register artist actors for ActivityPub.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_artist_actors(): void {
|
||||
// Hook into ActivityPub actor discovery.
|
||||
add_filter( 'activitypub_actor', array( $this, 'get_artist_actor' ), 10, 2 );
|
||||
|
||||
// Add artist webfinger handler.
|
||||
add_filter( 'webfinger_data', array( $this, 'add_artist_webfinger' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get artist actor for ActivityPub.
|
||||
*
|
||||
* @param mixed $actor The current actor.
|
||||
* @param string $id The actor ID or handle.
|
||||
* @return mixed The actor object or original.
|
||||
*/
|
||||
public function get_artist_actor( $actor, string $id ) {
|
||||
// Check if this is an artist handle.
|
||||
if ( strpos( $id, 'artist-' ) === 0 ) {
|
||||
$artist_id = absint( str_replace( 'artist-', '', $id ) );
|
||||
$artist = get_post( $artist_id );
|
||||
|
||||
if ( $artist && 'fedistream_artist' === $artist->post_type ) {
|
||||
return new ArtistActor( $artist );
|
||||
}
|
||||
}
|
||||
|
||||
return $actor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add artist to webfinger data.
|
||||
*
|
||||
* @param array $data The webfinger data.
|
||||
* @param string $resource The requested resource.
|
||||
* @return array Modified webfinger data.
|
||||
*/
|
||||
public function add_artist_webfinger( array $data, string $resource ): array {
|
||||
// Parse acct: URI.
|
||||
if ( preg_match( '/^acct:artist-(\d+)@/', $resource, $matches ) ) {
|
||||
$artist_id = absint( $matches[1] );
|
||||
$artist = get_post( $artist_id );
|
||||
|
||||
if ( $artist && 'fedistream_artist' === $artist->post_type ) {
|
||||
$actor = new ArtistActor( $artist );
|
||||
$data = $actor->get_webfinger();
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add audio-specific properties to ActivityPub objects.
|
||||
*
|
||||
* @param array $array The object array.
|
||||
* @param object $object The ActivityPub object.
|
||||
* @param int $post_id The post ID.
|
||||
* @return array Modified array.
|
||||
*/
|
||||
public function add_audio_properties( array $array, $object, int $post_id ): array {
|
||||
$post = get_post( $post_id );
|
||||
|
||||
if ( ! $post ) {
|
||||
return $array;
|
||||
}
|
||||
|
||||
// Add audio-specific properties for tracks.
|
||||
if ( 'fedistream_track' === $post->post_type ) {
|
||||
$duration = get_post_meta( $post_id, '_fedistream_duration', true );
|
||||
if ( $duration ) {
|
||||
$array['duration'] = $this->format_duration_iso8601( (int) $duration );
|
||||
}
|
||||
|
||||
$audio_id = get_post_meta( $post_id, '_fedistream_audio_file', true );
|
||||
$audio_url = $audio_id ? wp_get_attachment_url( $audio_id ) : '';
|
||||
if ( $audio_url ) {
|
||||
$array['attachment'][] = array(
|
||||
'type' => 'Audio',
|
||||
'mediaType' => 'audio/mpeg',
|
||||
'url' => $audio_url,
|
||||
'name' => $post->post_title,
|
||||
'duration' => $this->format_duration_iso8601( (int) $duration ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming Like activity.
|
||||
*
|
||||
* @param array $activity The activity data.
|
||||
* @param int $user_id The user ID (actor).
|
||||
* @return void
|
||||
*/
|
||||
public function handle_like( array $activity, int $user_id ): void {
|
||||
$object_id = $activity['object'] ?? '';
|
||||
if ( ! $object_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find local post from object ID.
|
||||
$post_id = url_to_postid( $object_id );
|
||||
if ( ! $post_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post || ! in_array( $post->post_type, array( 'fedistream_track', 'fedistream_album' ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Record the like.
|
||||
$this->record_reaction( $post_id, $activity['actor'] ?? '', 'like', $activity );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming Announce (boost) activity.
|
||||
*
|
||||
* @param array $activity The activity data.
|
||||
* @param int $user_id The user ID (actor).
|
||||
* @return void
|
||||
*/
|
||||
public function handle_announce( array $activity, int $user_id ): void {
|
||||
$object_id = $activity['object'] ?? '';
|
||||
if ( ! $object_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find local post from object ID.
|
||||
$post_id = url_to_postid( $object_id );
|
||||
if ( ! $post_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post || ! in_array( $post->post_type, array( 'fedistream_track', 'fedistream_album' ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Record the boost.
|
||||
$this->record_reaction( $post_id, $activity['actor'] ?? '', 'boost', $activity );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming Create activity (comments/replies).
|
||||
*
|
||||
* @param array $activity The activity data.
|
||||
* @param int $user_id The user ID (actor).
|
||||
* @return void
|
||||
*/
|
||||
public function handle_create( array $activity, int $user_id ): void {
|
||||
$object = $activity['object'] ?? array();
|
||||
if ( empty( $object ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a reply to our content.
|
||||
$in_reply_to = $object['inReplyTo'] ?? '';
|
||||
if ( ! $in_reply_to ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find local post from reply target.
|
||||
$post_id = url_to_postid( $in_reply_to );
|
||||
if ( ! $post_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post || ! in_array( $post->post_type, array( 'fedistream_track', 'fedistream_album' ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Record the reply.
|
||||
$this->record_reaction( $post_id, $activity['actor'] ?? '', 'reply', $activity );
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a reaction from the Fediverse.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
* @param string $actor The actor URI.
|
||||
* @param string $type The reaction type (like, boost, reply).
|
||||
* @param array $activity The full activity data.
|
||||
* @return bool True on success.
|
||||
*/
|
||||
private function record_reaction( int $post_id, string $actor, string $type, array $activity ): bool {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_reactions';
|
||||
|
||||
// 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 ) {
|
||||
$this->create_reactions_table();
|
||||
}
|
||||
|
||||
// Insert reaction.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$result = $wpdb->insert(
|
||||
$table,
|
||||
array(
|
||||
'post_id' => $post_id,
|
||||
'actor_uri' => $actor,
|
||||
'reaction_type' => $type,
|
||||
'activity_data' => wp_json_encode( $activity ),
|
||||
'created_at' => current_time( 'mysql' ),
|
||||
),
|
||||
array( '%d', '%s', '%s', '%s', '%s' )
|
||||
);
|
||||
|
||||
// Update reaction count meta.
|
||||
if ( $result ) {
|
||||
$meta_key = '_fedistream_' . $type . '_count';
|
||||
$count = (int) get_post_meta( $post_id, $meta_key, true );
|
||||
update_post_meta( $post_id, $meta_key, $count + 1 );
|
||||
}
|
||||
|
||||
return (bool) $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the reactions table.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function create_reactions_table(): void {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_reactions';
|
||||
$charset = $wpdb->get_charset_collate();
|
||||
|
||||
$sql = "CREATE TABLE IF NOT EXISTS {$table} (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
post_id bigint(20) unsigned NOT NULL,
|
||||
actor_uri varchar(2083) NOT NULL,
|
||||
reaction_type varchar(50) NOT NULL,
|
||||
activity_data longtext,
|
||||
created_at datetime NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY post_id (post_id),
|
||||
KEY actor_uri (actor_uri(191)),
|
||||
KEY reaction_type (reaction_type)
|
||||
) {$charset};";
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
dbDelta( $sql );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle track publishing.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
* @param \WP_Post $post The post object.
|
||||
* @return void
|
||||
*/
|
||||
public function on_publish_track( int $post_id, \WP_Post $post ): void {
|
||||
// The ActivityPub plugin will handle the publishing automatically.
|
||||
// This hook is for any additional custom logic.
|
||||
do_action( 'fedistream_track_published_activitypub', $post_id, $post );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle album publishing.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
* @param \WP_Post $post The post object.
|
||||
* @return void
|
||||
*/
|
||||
public function on_publish_album( int $post_id, \WP_Post $post ): void {
|
||||
// The ActivityPub plugin will handle the publishing automatically.
|
||||
// This hook is for any additional custom logic.
|
||||
do_action( 'fedistream_album_published_activitypub', $post_id, $post );
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration as ISO 8601.
|
||||
*
|
||||
* @param int $seconds The duration in seconds.
|
||||
* @return string ISO 8601 duration.
|
||||
*/
|
||||
private function format_duration_iso8601( int $seconds ): string {
|
||||
$hours = floor( $seconds / 3600 );
|
||||
$minutes = floor( ( $seconds % 3600 ) / 60 );
|
||||
$secs = $seconds % 60;
|
||||
|
||||
$duration = 'PT';
|
||||
if ( $hours > 0 ) {
|
||||
$duration .= $hours . 'H';
|
||||
}
|
||||
if ( $minutes > 0 ) {
|
||||
$duration .= $minutes . 'M';
|
||||
}
|
||||
if ( $secs > 0 || ( $hours === 0 && $minutes === 0 ) ) {
|
||||
$duration .= $secs . 'S';
|
||||
}
|
||||
|
||||
return $duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reactions for a post.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
* @param string $type Optional reaction type filter.
|
||||
* @return array The reactions.
|
||||
*/
|
||||
public function get_reactions( int $post_id, string $type = '' ): array {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_reactions';
|
||||
|
||||
if ( $type ) {
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$results = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$table} WHERE post_id = %d AND reaction_type = %s ORDER BY created_at DESC",
|
||||
$post_id,
|
||||
$type
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$results = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$table} WHERE post_id = %d ORDER BY created_at DESC",
|
||||
$post_id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $results ?: array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reaction counts for a post.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
* @return array The reaction counts.
|
||||
*/
|
||||
public function get_reaction_counts( int $post_id ): array {
|
||||
return array(
|
||||
'likes' => (int) get_post_meta( $post_id, '_fedistream_like_count', true ),
|
||||
'boosts' => (int) get_post_meta( $post_id, '_fedistream_boost_count', true ),
|
||||
'replies' => (int) get_post_meta( $post_id, '_fedistream_reply_count', true ),
|
||||
);
|
||||
}
|
||||
}
|
||||
415
includes/ActivityPub/Outbox.php
Normal file
415
includes/ActivityPub/Outbox.php
Normal file
@@ -0,0 +1,415 @@
|
||||
<?php
|
||||
/**
|
||||
* Outbox handler for ActivityPub.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\ActivityPub;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles publishing activities to followers.
|
||||
*/
|
||||
class Outbox {
|
||||
|
||||
/**
|
||||
* The follower handler.
|
||||
*
|
||||
* @var FollowerHandler
|
||||
*/
|
||||
private FollowerHandler $follower_handler;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->follower_handler = new FollowerHandler();
|
||||
|
||||
// Hook into post publishing.
|
||||
add_action( 'transition_post_status', array( $this, 'on_post_status_change' ), 10, 3 );
|
||||
|
||||
// Hook into post update.
|
||||
add_action( 'post_updated', array( $this, 'on_post_updated' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle post status changes.
|
||||
*
|
||||
* @param string $new_status The new post status.
|
||||
* @param string $old_status The old post status.
|
||||
* @param \WP_Post $post The post object.
|
||||
* @return void
|
||||
*/
|
||||
public function on_post_status_change( string $new_status, string $old_status, \WP_Post $post ): void {
|
||||
// Only handle FediStream post types.
|
||||
if ( ! in_array( $post->post_type, array( 'fedistream_track', 'fedistream_album', 'fedistream_playlist' ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if publishing is enabled for this post.
|
||||
$enabled = get_post_meta( $post->ID, '_fedistream_activitypub_publish', true );
|
||||
if ( ! $enabled && $enabled !== '' ) {
|
||||
// Default to enabled for new posts.
|
||||
if ( $old_status === 'new' || $old_status === 'auto-draft' ) {
|
||||
update_post_meta( $post->ID, '_fedistream_activitypub_publish', '1' );
|
||||
$enabled = '1';
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Publish on status change to 'publish'.
|
||||
if ( 'publish' === $new_status && 'publish' !== $old_status ) {
|
||||
$this->publish_create( $post );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle post updates.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
* @param \WP_Post $post_after The post object after update.
|
||||
* @param \WP_Post $post_before The post object before update.
|
||||
* @return void
|
||||
*/
|
||||
public function on_post_updated( int $post_id, \WP_Post $post_after, \WP_Post $post_before ): void {
|
||||
// Only handle published FediStream post types.
|
||||
if ( $post_after->post_status !== 'publish' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! in_array( $post_after->post_type, array( 'fedistream_track', 'fedistream_album', 'fedistream_playlist' ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if already published to ActivityPub.
|
||||
$published = get_post_meta( $post_id, '_fedistream_activitypub_published', true );
|
||||
if ( ! $published ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if update publishing is enabled.
|
||||
$publish_updates = get_post_meta( $post_id, '_fedistream_activitypub_publish_updates', true );
|
||||
if ( ! $publish_updates ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only publish updates for significant changes.
|
||||
if ( $this->has_significant_changes( $post_before, $post_after ) ) {
|
||||
$this->publish_update( $post_after );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a post has significant changes.
|
||||
*
|
||||
* @param \WP_Post $before The post before update.
|
||||
* @param \WP_Post $after The post after update.
|
||||
* @return bool
|
||||
*/
|
||||
private function has_significant_changes( \WP_Post $before, \WP_Post $after ): bool {
|
||||
// Check title change.
|
||||
if ( $before->post_title !== $after->post_title ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check content change.
|
||||
if ( $before->post_content !== $after->post_content ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for audio file change (tracks).
|
||||
if ( 'fedistream_track' === $after->post_type ) {
|
||||
$audio_before = get_post_meta( $after->ID, '_fedistream_audio_file_previous', true );
|
||||
$audio_after = get_post_meta( $after->ID, '_fedistream_audio_file', true );
|
||||
if ( $audio_before !== $audio_after ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a Create activity for a post.
|
||||
*
|
||||
* @param \WP_Post $post The post to publish.
|
||||
* @return bool True on success.
|
||||
*/
|
||||
public function publish_create( \WP_Post $post ): bool {
|
||||
$transformer = $this->get_transformer( $post );
|
||||
if ( ! $transformer ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$activity = $transformer->to_create_activity();
|
||||
|
||||
// Get the artist for this content.
|
||||
$artist_id = $this->get_artist_for_post( $post );
|
||||
if ( ! $artist_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Broadcast to followers.
|
||||
$results = $this->follower_handler->broadcast_activity( $artist_id, $activity );
|
||||
|
||||
// Mark as published.
|
||||
update_post_meta( $post->ID, '_fedistream_activitypub_published', current_time( 'mysql' ) );
|
||||
|
||||
// Log results.
|
||||
$success_count = count( array_filter( $results ) );
|
||||
$total_count = count( $results );
|
||||
|
||||
do_action( 'fedistream_activitypub_published', $post->ID, $success_count, $total_count );
|
||||
|
||||
return $success_count > 0 || $total_count === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish an Update activity for a post.
|
||||
*
|
||||
* @param \WP_Post $post The post to update.
|
||||
* @return bool True on success.
|
||||
*/
|
||||
public function publish_update( \WP_Post $post ): bool {
|
||||
$transformer = $this->get_transformer( $post );
|
||||
if ( ! $transformer ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build Update activity.
|
||||
$object = $transformer->to_object();
|
||||
$actor = $transformer->get_attributed_to();
|
||||
$actor = is_array( $actor ) ? $actor[0] : $actor;
|
||||
|
||||
$activity = array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Update',
|
||||
'id' => get_permalink( $post->ID ) . '#activity-update-' . time(),
|
||||
'actor' => $actor,
|
||||
'published' => gmdate( 'c' ),
|
||||
'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ),
|
||||
'cc' => array( $actor . '/followers' ),
|
||||
'object' => $object,
|
||||
);
|
||||
|
||||
// Get the artist for this content.
|
||||
$artist_id = $this->get_artist_for_post( $post );
|
||||
if ( ! $artist_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Broadcast to followers.
|
||||
$results = $this->follower_handler->broadcast_activity( $artist_id, $activity );
|
||||
|
||||
// Update timestamp.
|
||||
update_post_meta( $post->ID, '_fedistream_activitypub_updated', current_time( 'mysql' ) );
|
||||
|
||||
$success_count = count( array_filter( $results ) );
|
||||
|
||||
return $success_count > 0 || count( $results ) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish an Announce (boost) activity.
|
||||
*
|
||||
* @param int $artist_id The artist ID doing the announcing.
|
||||
* @param string $object_uri The object URI to announce.
|
||||
* @return bool True on success.
|
||||
*/
|
||||
public function publish_announce( int $artist_id, string $object_uri ): bool {
|
||||
$artist = get_post( $artist_id );
|
||||
if ( ! $artist || 'fedistream_artist' !== $artist->post_type ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$actor = new ArtistActor( $artist );
|
||||
|
||||
$activity = array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Announce',
|
||||
'id' => $actor->get_id() . '#announce-' . time(),
|
||||
'actor' => $actor->get_id(),
|
||||
'published' => gmdate( 'c' ),
|
||||
'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ),
|
||||
'cc' => array( $actor->get_followers() ),
|
||||
'object' => $object_uri,
|
||||
);
|
||||
|
||||
// Broadcast to followers.
|
||||
$results = $this->follower_handler->broadcast_activity( $artist_id, $activity );
|
||||
|
||||
return count( array_filter( $results ) ) > 0 || count( $results ) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a Delete activity for a post.
|
||||
*
|
||||
* @param \WP_Post $post The post being deleted.
|
||||
* @return bool True on success.
|
||||
*/
|
||||
public function publish_delete( \WP_Post $post ): bool {
|
||||
// Check if was published to ActivityPub.
|
||||
$published = get_post_meta( $post->ID, '_fedistream_activitypub_published', true );
|
||||
if ( ! $published ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$actor = $this->get_attributed_to( $post );
|
||||
if ( ! $actor ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$activity = array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Delete',
|
||||
'id' => get_permalink( $post->ID ) . '#activity-delete-' . time(),
|
||||
'actor' => $actor,
|
||||
'published' => gmdate( 'c' ),
|
||||
'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ),
|
||||
'object' => array(
|
||||
'type' => 'Tombstone',
|
||||
'id' => get_permalink( $post->ID ),
|
||||
),
|
||||
);
|
||||
|
||||
// Get the artist for this content.
|
||||
$artist_id = $this->get_artist_for_post( $post );
|
||||
if ( ! $artist_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Broadcast to followers.
|
||||
$results = $this->follower_handler->broadcast_activity( $artist_id, $activity );
|
||||
|
||||
return count( array_filter( $results ) ) > 0 || count( $results ) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate transformer for a post.
|
||||
*
|
||||
* @param \WP_Post $post The post.
|
||||
* @return TrackTransformer|AlbumTransformer|PlaylistTransformer|null
|
||||
*/
|
||||
private function get_transformer( \WP_Post $post ) {
|
||||
switch ( $post->post_type ) {
|
||||
case 'fedistream_track':
|
||||
return new TrackTransformer( $post );
|
||||
|
||||
case 'fedistream_album':
|
||||
return new AlbumTransformer( $post );
|
||||
|
||||
case 'fedistream_playlist':
|
||||
return new PlaylistTransformer( $post );
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the artist ID associated with a post.
|
||||
*
|
||||
* @param \WP_Post $post The post.
|
||||
* @return int|null
|
||||
*/
|
||||
private function get_artist_for_post( \WP_Post $post ): ?int {
|
||||
switch ( $post->post_type ) {
|
||||
case 'fedistream_track':
|
||||
$artist_ids = get_post_meta( $post->ID, '_fedistream_artist_ids', true );
|
||||
if ( is_array( $artist_ids ) && ! empty( $artist_ids ) ) {
|
||||
return absint( $artist_ids[0] );
|
||||
}
|
||||
|
||||
// Fall back to album artist.
|
||||
$album_id = get_post_meta( $post->ID, '_fedistream_album_id', true );
|
||||
if ( $album_id ) {
|
||||
return absint( get_post_meta( $album_id, '_fedistream_album_artist', true ) );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'fedistream_album':
|
||||
$artist_id = get_post_meta( $post->ID, '_fedistream_album_artist', true );
|
||||
return $artist_id ? absint( $artist_id ) : null;
|
||||
|
||||
case 'fedistream_playlist':
|
||||
// For playlists, try to find an artist owned by the author.
|
||||
$artist_args = array(
|
||||
'post_type' => 'fedistream_artist',
|
||||
'posts_per_page' => 1,
|
||||
'author' => $post->post_author,
|
||||
);
|
||||
|
||||
$artists = get_posts( $artist_args );
|
||||
if ( ! empty( $artists ) ) {
|
||||
return $artists[0]->ID;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attributed actor for a post.
|
||||
*
|
||||
* @param \WP_Post $post The post.
|
||||
* @return string|null Actor URI.
|
||||
*/
|
||||
private function get_attributed_to( \WP_Post $post ): ?string {
|
||||
$artist_id = $this->get_artist_for_post( $post );
|
||||
|
||||
if ( $artist_id ) {
|
||||
return get_permalink( $artist_id );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually trigger publication for a post.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
* @return bool True on success.
|
||||
*/
|
||||
public function manual_publish( int $post_id ): bool {
|
||||
$post = get_post( $post_id );
|
||||
|
||||
if ( ! $post || 'publish' !== $post->post_status ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if already published.
|
||||
$published = get_post_meta( $post_id, '_fedistream_activitypub_published', true );
|
||||
|
||||
if ( $published ) {
|
||||
return $this->publish_update( $post );
|
||||
}
|
||||
|
||||
return $this->publish_create( $post );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the outbox collection for an artist.
|
||||
*
|
||||
* @param int $artist_id The artist ID.
|
||||
* @param int $page The page number (0 for summary).
|
||||
* @param int $per_page Items per page.
|
||||
* @return array
|
||||
*/
|
||||
public function get_collection( int $artist_id, int $page = 0, int $per_page = 20 ): array {
|
||||
$artist = get_post( $artist_id );
|
||||
if ( ! $artist || 'fedistream_artist' !== $artist->post_type ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$actor = new ArtistActor( $artist );
|
||||
|
||||
return $actor->get_outbox_collection( $page, $per_page );
|
||||
}
|
||||
}
|
||||
433
includes/ActivityPub/PlaylistTransformer.php
Normal file
433
includes/ActivityPub/PlaylistTransformer.php
Normal file
@@ -0,0 +1,433 @@
|
||||
<?php
|
||||
/**
|
||||
* Playlist Transformer for ActivityPub.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\ActivityPub;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms Playlist posts to ActivityPub OrderedCollection objects.
|
||||
*/
|
||||
class PlaylistTransformer {
|
||||
|
||||
/**
|
||||
* The playlist post.
|
||||
*
|
||||
* @var \WP_Post
|
||||
*/
|
||||
protected \WP_Post $post;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \WP_Post $post The playlist post.
|
||||
*/
|
||||
public function __construct( \WP_Post $post ) {
|
||||
$this->post = $post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ActivityPub object type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_type(): string {
|
||||
return 'OrderedCollection';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the object ID (URI).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_id(): string {
|
||||
return get_permalink( $this->post->ID );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the object name (title).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return $this->post->post_title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content (description).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_content(): string {
|
||||
$content = $this->post->post_content;
|
||||
|
||||
// Apply content filters for proper formatting.
|
||||
$content = apply_filters( 'the_content', $content );
|
||||
|
||||
return wp_kses_post( $content );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the summary (excerpt).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_summary(): string {
|
||||
if ( ! empty( $this->post->post_excerpt ) ) {
|
||||
return wp_strip_all_tags( $this->post->post_excerpt );
|
||||
}
|
||||
|
||||
// Generate excerpt from content.
|
||||
return wp_trim_words( wp_strip_all_tags( $this->post->post_content ), 30 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL (permalink).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_url(): string {
|
||||
return get_permalink( $this->post->ID );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attributed actor (playlist creator).
|
||||
*
|
||||
* @return string User's ActivityPub actor URI.
|
||||
*/
|
||||
public function get_attributed_to(): string {
|
||||
$user_id = $this->post->post_author;
|
||||
|
||||
// Check if there's an artist associated with this user.
|
||||
$artist_args = array(
|
||||
'post_type' => 'fedistream_artist',
|
||||
'posts_per_page' => 1,
|
||||
'author' => $user_id,
|
||||
);
|
||||
|
||||
$artists = get_posts( $artist_args );
|
||||
|
||||
if ( ! empty( $artists ) ) {
|
||||
return get_permalink( $artists[0]->ID );
|
||||
}
|
||||
|
||||
// Fall back to user profile URL.
|
||||
return get_author_posts_url( $user_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the published date.
|
||||
*
|
||||
* @return string ISO 8601 date.
|
||||
*/
|
||||
public function get_published(): string {
|
||||
return get_the_date( 'c', $this->post );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the updated date.
|
||||
*
|
||||
* @return string ISO 8601 date.
|
||||
*/
|
||||
public function get_updated(): string {
|
||||
return get_the_modified_date( 'c', $this->post );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total duration.
|
||||
*
|
||||
* @return string ISO 8601 duration.
|
||||
*/
|
||||
public function get_duration(): string {
|
||||
$seconds = (int) get_post_meta( $this->post->ID, '_fedistream_playlist_duration', true );
|
||||
|
||||
if ( ! $seconds ) {
|
||||
// Calculate from tracks.
|
||||
$tracks = $this->get_tracks();
|
||||
foreach ( $tracks as $track ) {
|
||||
$seconds += (int) get_post_meta( $track->ID, '_fedistream_duration', true );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $seconds ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->format_duration_iso8601( $seconds );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tracks in this playlist.
|
||||
*
|
||||
* @return array Array of WP_Post objects.
|
||||
*/
|
||||
public function get_tracks(): array {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_playlist_tracks';
|
||||
|
||||
// Get track IDs in order.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$track_ids = $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"SELECT track_id FROM {$table} WHERE playlist_id = %d ORDER BY position ASC",
|
||||
$this->post->ID
|
||||
)
|
||||
);
|
||||
|
||||
if ( empty( $track_ids ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// Get track posts in order.
|
||||
$args = array(
|
||||
'post_type' => 'fedistream_track',
|
||||
'post_status' => 'publish',
|
||||
'post__in' => $track_ids,
|
||||
'orderby' => 'post__in',
|
||||
'posts_per_page' => -1,
|
||||
);
|
||||
|
||||
$query = new \WP_Query( $args );
|
||||
|
||||
return $query->posts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total item count.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_total_items(): int {
|
||||
$count = (int) get_post_meta( $this->post->ID, '_fedistream_track_count', true );
|
||||
|
||||
if ( ! $count ) {
|
||||
$count = count( $this->get_tracks() );
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ordered items (track objects).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_ordered_items(): array {
|
||||
$tracks = $this->get_tracks();
|
||||
$items = array();
|
||||
|
||||
foreach ( $tracks as $track ) {
|
||||
$transformer = new TrackTransformer( $track );
|
||||
$items[] = $transformer->to_object();
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the image/cover attachment.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_image_attachment(): ?array {
|
||||
$thumbnail_id = get_post_thumbnail_id( $this->post->ID );
|
||||
|
||||
if ( ! $thumbnail_id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$image = wp_get_attachment_image_src( $thumbnail_id, 'medium' );
|
||||
if ( ! $image ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'type' => 'Image',
|
||||
'mediaType' => get_post_mime_type( $thumbnail_id ),
|
||||
'url' => $image[0],
|
||||
'width' => $image[1],
|
||||
'height' => $image[2],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tags (moods).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_tags(): array {
|
||||
$tags = array();
|
||||
|
||||
// Get moods.
|
||||
$moods = get_the_terms( $this->post->ID, 'fedistream_mood' );
|
||||
if ( $moods && ! is_wp_error( $moods ) ) {
|
||||
foreach ( $moods as $mood ) {
|
||||
$tags[] = array(
|
||||
'type' => 'Hashtag',
|
||||
'name' => '#' . sanitize_title( $mood->name ),
|
||||
'href' => get_term_link( $mood ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the playlist is public.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_public(): bool {
|
||||
$visibility = get_post_meta( $this->post->ID, '_fedistream_playlist_visibility', true );
|
||||
|
||||
return 'public' === $visibility || empty( $visibility );
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform to ActivityPub object array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_object(): array {
|
||||
$object = array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => $this->get_type(),
|
||||
'id' => $this->get_id(),
|
||||
'name' => $this->get_name(),
|
||||
'summary' => $this->get_summary(),
|
||||
'content' => $this->get_content(),
|
||||
'url' => $this->get_url(),
|
||||
'attributedTo' => $this->get_attributed_to(),
|
||||
'published' => $this->get_published(),
|
||||
'updated' => $this->get_updated(),
|
||||
'totalItems' => $this->get_total_items(),
|
||||
'orderedItems' => $this->get_ordered_items(),
|
||||
);
|
||||
|
||||
// Add duration.
|
||||
$duration = $this->get_duration();
|
||||
if ( $duration ) {
|
||||
$object['duration'] = $duration;
|
||||
}
|
||||
|
||||
// Add image.
|
||||
$image = $this->get_image_attachment();
|
||||
if ( $image ) {
|
||||
$object['image'] = $image;
|
||||
}
|
||||
|
||||
// Add tags.
|
||||
$tags = $this->get_tags();
|
||||
if ( ! empty( $tags ) ) {
|
||||
$object['tag'] = $tags;
|
||||
}
|
||||
|
||||
// Add visibility indicator.
|
||||
if ( ! $this->is_public() ) {
|
||||
$object['sensitive'] = false;
|
||||
$visibility = get_post_meta( $this->post->ID, '_fedistream_playlist_visibility', true );
|
||||
if ( 'unlisted' === $visibility ) {
|
||||
$object['visibility'] = 'unlisted';
|
||||
}
|
||||
}
|
||||
|
||||
// Add collaborative flag.
|
||||
$collaborative = get_post_meta( $this->post->ID, '_fedistream_playlist_collaborative', true );
|
||||
if ( $collaborative ) {
|
||||
$object['collaborative'] = true;
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Create activity for this playlist.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_create_activity(): array {
|
||||
$actor = $this->get_attributed_to();
|
||||
|
||||
// Determine audience based on visibility.
|
||||
$visibility = get_post_meta( $this->post->ID, '_fedistream_playlist_visibility', true );
|
||||
|
||||
$to = array();
|
||||
$cc = array();
|
||||
|
||||
if ( 'public' === $visibility || empty( $visibility ) ) {
|
||||
$to[] = 'https://www.w3.org/ns/activitystreams#Public';
|
||||
$cc[] = $actor . '/followers';
|
||||
} elseif ( 'unlisted' === $visibility ) {
|
||||
$to[] = $actor . '/followers';
|
||||
$cc[] = 'https://www.w3.org/ns/activitystreams#Public';
|
||||
} else {
|
||||
// Private - only followers.
|
||||
$to[] = $actor . '/followers';
|
||||
}
|
||||
|
||||
return array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Create',
|
||||
'id' => $this->get_id() . '#activity-create',
|
||||
'actor' => $actor,
|
||||
'published' => $this->get_published(),
|
||||
'to' => $to,
|
||||
'cc' => $cc,
|
||||
'object' => $this->to_object(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Update activity for this playlist.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_update_activity(): array {
|
||||
$actor = $this->get_attributed_to();
|
||||
|
||||
return array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Update',
|
||||
'id' => $this->get_id() . '#activity-update-' . time(),
|
||||
'actor' => $actor,
|
||||
'published' => gmdate( 'c' ),
|
||||
'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ),
|
||||
'cc' => array( $actor . '/followers' ),
|
||||
'object' => $this->to_object(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration as ISO 8601.
|
||||
*
|
||||
* @param int $seconds The duration in seconds.
|
||||
* @return string ISO 8601 duration.
|
||||
*/
|
||||
private function format_duration_iso8601( int $seconds ): string {
|
||||
$hours = floor( $seconds / 3600 );
|
||||
$minutes = floor( ( $seconds % 3600 ) / 60 );
|
||||
$secs = $seconds % 60;
|
||||
|
||||
$duration = 'PT';
|
||||
if ( $hours > 0 ) {
|
||||
$duration .= $hours . 'H';
|
||||
}
|
||||
if ( $minutes > 0 ) {
|
||||
$duration .= $minutes . 'M';
|
||||
}
|
||||
if ( $secs > 0 || ( $hours === 0 && $minutes === 0 ) ) {
|
||||
$duration .= $secs . 'S';
|
||||
}
|
||||
|
||||
return $duration;
|
||||
}
|
||||
}
|
||||
476
includes/ActivityPub/RestApi.php
Normal file
476
includes/ActivityPub/RestApi.php
Normal file
@@ -0,0 +1,476 @@
|
||||
<?php
|
||||
/**
|
||||
* REST API endpoints for ActivityPub.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\ActivityPub;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* REST API handler for ActivityPub endpoints.
|
||||
*/
|
||||
class RestApi {
|
||||
|
||||
/**
|
||||
* The namespace for REST routes.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private const NAMESPACE = 'fedistream/v1';
|
||||
|
||||
/**
|
||||
* The follower handler.
|
||||
*
|
||||
* @var FollowerHandler
|
||||
*/
|
||||
private FollowerHandler $follower_handler;
|
||||
|
||||
/**
|
||||
* The outbox handler.
|
||||
*
|
||||
* @var Outbox
|
||||
*/
|
||||
private Outbox $outbox;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->follower_handler = new FollowerHandler();
|
||||
$this->outbox = new Outbox();
|
||||
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
|
||||
// Add ActivityPub content type to allowed responses.
|
||||
add_filter( 'rest_pre_serve_request', array( $this, 'serve_activitypub' ), 10, 4 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register REST API routes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_routes(): void {
|
||||
// Artist actor endpoint.
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/artist/(?P<id>\d+)',
|
||||
array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array( $this, 'get_artist_actor' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'id' => array(
|
||||
'required' => true,
|
||||
'validate_callback' => array( $this, 'validate_artist_id' ),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Artist inbox endpoint.
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/artist/(?P<id>\d+)/inbox',
|
||||
array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array( $this, 'handle_inbox' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'id' => array(
|
||||
'required' => true,
|
||||
'validate_callback' => array( $this, 'validate_artist_id' ),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Artist outbox endpoint.
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/artist/(?P<id>\d+)/outbox',
|
||||
array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array( $this, 'get_outbox' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'id' => array(
|
||||
'required' => true,
|
||||
'validate_callback' => array( $this, 'validate_artist_id' ),
|
||||
),
|
||||
'page' => array(
|
||||
'default' => 0,
|
||||
'sanitize_callback' => 'absint',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Artist followers endpoint.
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/artist/(?P<id>\d+)/followers',
|
||||
array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array( $this, 'get_followers' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'id' => array(
|
||||
'required' => true,
|
||||
'validate_callback' => array( $this, 'validate_artist_id' ),
|
||||
),
|
||||
'page' => array(
|
||||
'default' => 0,
|
||||
'sanitize_callback' => 'absint',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Track/Album/Playlist object endpoint.
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/object/(?P<type>track|album|playlist)/(?P<id>\d+)',
|
||||
array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array( $this, 'get_object' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'type' => array(
|
||||
'required' => true,
|
||||
),
|
||||
'id' => array(
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Reactions endpoint.
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/reactions/(?P<post_id>\d+)',
|
||||
array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array( $this, 'get_reactions' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'post_id' => array(
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
),
|
||||
'type' => array(
|
||||
'default' => '',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Manual publish endpoint (requires auth).
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/publish/(?P<post_id>\d+)',
|
||||
array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array( $this, 'manual_publish' ),
|
||||
'permission_callback' => array( $this, 'can_edit_post' ),
|
||||
'args' => array(
|
||||
'post_id' => array(
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate artist ID.
|
||||
*
|
||||
* @param mixed $id The ID to validate.
|
||||
* @return bool
|
||||
*/
|
||||
public function validate_artist_id( $id ): bool {
|
||||
$artist = get_post( absint( $id ) );
|
||||
|
||||
return $artist && 'fedistream_artist' === $artist->post_type && 'publish' === $artist->post_status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user can edit the post.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request.
|
||||
* @return bool
|
||||
*/
|
||||
public function can_edit_post( \WP_REST_Request $request ): bool {
|
||||
$post_id = absint( $request->get_param( 'post_id' ) );
|
||||
|
||||
return current_user_can( 'edit_post', $post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get artist actor.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function get_artist_actor( \WP_REST_Request $request ): \WP_REST_Response {
|
||||
$artist_id = absint( $request->get_param( 'id' ) );
|
||||
$artist = get_post( $artist_id );
|
||||
|
||||
if ( ! $artist ) {
|
||||
return new \WP_REST_Response( array( 'error' => 'Artist not found' ), 404 );
|
||||
}
|
||||
|
||||
$actor = new ArtistActor( $artist );
|
||||
|
||||
$response = new \WP_REST_Response( $actor->to_array() );
|
||||
$response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle inbox requests.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function handle_inbox( \WP_REST_Request $request ): \WP_REST_Response {
|
||||
$artist_id = absint( $request->get_param( 'id' ) );
|
||||
$artist = get_post( $artist_id );
|
||||
|
||||
if ( ! $artist ) {
|
||||
return new \WP_REST_Response( array( 'error' => 'Artist not found' ), 404 );
|
||||
}
|
||||
|
||||
// Get the activity from request body.
|
||||
$activity = $request->get_json_params();
|
||||
|
||||
if ( empty( $activity ) ) {
|
||||
return new \WP_REST_Response( array( 'error' => 'Invalid activity' ), 400 );
|
||||
}
|
||||
|
||||
// Verify HTTP signature (basic verification).
|
||||
$signature = $request->get_header( 'Signature' );
|
||||
if ( ! $signature ) {
|
||||
// Allow unsigned requests for now, but log it.
|
||||
do_action( 'fedistream_unsigned_activity', $activity, $artist_id );
|
||||
}
|
||||
|
||||
// Process the activity based on type.
|
||||
$type = $activity['type'] ?? '';
|
||||
|
||||
switch ( $type ) {
|
||||
case 'Follow':
|
||||
$this->follower_handler->handle_follow( $activity, 0 );
|
||||
break;
|
||||
|
||||
case 'Undo':
|
||||
$this->follower_handler->handle_undo( $activity, 0 );
|
||||
break;
|
||||
|
||||
case 'Like':
|
||||
do_action( 'activitypub_inbox_like', $activity, 0 );
|
||||
break;
|
||||
|
||||
case 'Announce':
|
||||
do_action( 'activitypub_inbox_announce', $activity, 0 );
|
||||
break;
|
||||
|
||||
case 'Create':
|
||||
do_action( 'activitypub_inbox_create', $activity, 0 );
|
||||
break;
|
||||
|
||||
case 'Delete':
|
||||
do_action( 'activitypub_inbox_delete', $activity, 0 );
|
||||
break;
|
||||
|
||||
default:
|
||||
do_action( 'fedistream_inbox_activity', $activity, $artist_id, $type );
|
||||
break;
|
||||
}
|
||||
|
||||
return new \WP_REST_Response( null, 202 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get outbox collection.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function get_outbox( \WP_REST_Request $request ): \WP_REST_Response {
|
||||
$artist_id = absint( $request->get_param( 'id' ) );
|
||||
$page = absint( $request->get_param( 'page' ) );
|
||||
|
||||
$artist = get_post( $artist_id );
|
||||
if ( ! $artist ) {
|
||||
return new \WP_REST_Response( array( 'error' => 'Artist not found' ), 404 );
|
||||
}
|
||||
|
||||
$actor = new ArtistActor( $artist );
|
||||
$collection = $actor->get_outbox_collection( $page );
|
||||
|
||||
$response = new \WP_REST_Response( $collection );
|
||||
$response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get followers collection.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function get_followers( \WP_REST_Request $request ): \WP_REST_Response {
|
||||
$artist_id = absint( $request->get_param( 'id' ) );
|
||||
$page = absint( $request->get_param( 'page' ) );
|
||||
|
||||
$artist = get_post( $artist_id );
|
||||
if ( ! $artist ) {
|
||||
return new \WP_REST_Response( array( 'error' => 'Artist not found' ), 404 );
|
||||
}
|
||||
|
||||
$actor = new ArtistActor( $artist );
|
||||
$collection = $actor->get_followers_collection( $page );
|
||||
|
||||
$response = new \WP_REST_Response( $collection );
|
||||
$response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ActivityPub object.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function get_object( \WP_REST_Request $request ): \WP_REST_Response {
|
||||
$type = $request->get_param( 'type' );
|
||||
$post_id = absint( $request->get_param( 'id' ) );
|
||||
|
||||
$post_type = 'fedistream_' . $type;
|
||||
$post = get_post( $post_id );
|
||||
|
||||
if ( ! $post || $post->post_type !== $post_type || 'publish' !== $post->post_status ) {
|
||||
return new \WP_REST_Response( array( 'error' => 'Object not found' ), 404 );
|
||||
}
|
||||
|
||||
$transformer = null;
|
||||
switch ( $type ) {
|
||||
case 'track':
|
||||
$transformer = new TrackTransformer( $post );
|
||||
break;
|
||||
|
||||
case 'album':
|
||||
$transformer = new AlbumTransformer( $post );
|
||||
break;
|
||||
|
||||
case 'playlist':
|
||||
$transformer = new PlaylistTransformer( $post );
|
||||
break;
|
||||
}
|
||||
|
||||
if ( ! $transformer ) {
|
||||
return new \WP_REST_Response( array( 'error' => 'Invalid object type' ), 400 );
|
||||
}
|
||||
|
||||
$object = $transformer->to_object();
|
||||
$response = new \WP_REST_Response( $object );
|
||||
$response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reactions for a post.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function get_reactions( \WP_REST_Request $request ): \WP_REST_Response {
|
||||
$post_id = absint( $request->get_param( 'post_id' ) );
|
||||
$type = sanitize_text_field( $request->get_param( 'type' ) );
|
||||
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post ) {
|
||||
return new \WP_REST_Response( array( 'error' => 'Post not found' ), 404 );
|
||||
}
|
||||
|
||||
$integration = new Integration();
|
||||
$reactions = $integration->get_reactions( $post_id, $type );
|
||||
$counts = $integration->get_reaction_counts( $post_id );
|
||||
|
||||
return new \WP_REST_Response(
|
||||
array(
|
||||
'reactions' => $reactions,
|
||||
'counts' => $counts,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually publish a post to ActivityPub.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function manual_publish( \WP_REST_Request $request ): \WP_REST_Response {
|
||||
$post_id = absint( $request->get_param( 'post_id' ) );
|
||||
|
||||
$result = $this->outbox->manual_publish( $post_id );
|
||||
|
||||
if ( $result ) {
|
||||
return new \WP_REST_Response(
|
||||
array(
|
||||
'success' => true,
|
||||
'message' => __( 'Published to ActivityPub', 'wp-fedistream' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return new \WP_REST_Response(
|
||||
array(
|
||||
'success' => false,
|
||||
'message' => __( 'Failed to publish', 'wp-fedistream' ),
|
||||
),
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve ActivityPub content type when requested.
|
||||
*
|
||||
* @param bool $served Whether the request has been served.
|
||||
* @param \WP_REST_Response $result The response.
|
||||
* @param \WP_REST_Request $request The request.
|
||||
* @param \WP_REST_Server $server The server.
|
||||
* @return bool
|
||||
*/
|
||||
public function serve_activitypub( bool $served, $result, \WP_REST_Request $request, \WP_REST_Server $server ): bool {
|
||||
// Check if this is a FediStream route.
|
||||
$route = $request->get_route();
|
||||
if ( strpos( $route, '/fedistream/' ) === false ) {
|
||||
return $served;
|
||||
}
|
||||
|
||||
// Check Accept header for ActivityPub.
|
||||
$accept = $request->get_header( 'Accept' );
|
||||
if ( $accept && ( strpos( $accept, 'application/activity+json' ) !== false || strpos( $accept, 'application/ld+json' ) !== false ) ) {
|
||||
// Will be handled by our response headers.
|
||||
}
|
||||
|
||||
return $served;
|
||||
}
|
||||
}
|
||||
412
includes/ActivityPub/TrackTransformer.php
Normal file
412
includes/ActivityPub/TrackTransformer.php
Normal file
@@ -0,0 +1,412 @@
|
||||
<?php
|
||||
/**
|
||||
* Track Transformer for ActivityPub.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\ActivityPub;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms Track posts to ActivityPub Audio objects.
|
||||
*/
|
||||
class TrackTransformer {
|
||||
|
||||
/**
|
||||
* The track post.
|
||||
*
|
||||
* @var \WP_Post
|
||||
*/
|
||||
protected \WP_Post $post;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \WP_Post $post The track post.
|
||||
*/
|
||||
public function __construct( \WP_Post $post ) {
|
||||
$this->post = $post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ActivityPub object type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_type(): string {
|
||||
return 'Audio';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the object ID (URI).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_id(): string {
|
||||
return get_permalink( $this->post->ID );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the object name (title).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return $this->post->post_title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content (lyrics or description).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_content(): string {
|
||||
$content = $this->post->post_content;
|
||||
|
||||
// Apply content filters for proper formatting.
|
||||
$content = apply_filters( 'the_content', $content );
|
||||
|
||||
return wp_kses_post( $content );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the summary (excerpt).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_summary(): string {
|
||||
if ( ! empty( $this->post->post_excerpt ) ) {
|
||||
return wp_strip_all_tags( $this->post->post_excerpt );
|
||||
}
|
||||
|
||||
// Generate excerpt from content.
|
||||
return wp_trim_words( wp_strip_all_tags( $this->post->post_content ), 30 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL (permalink).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_url(): string {
|
||||
return get_permalink( $this->post->ID );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attributed actor(s).
|
||||
*
|
||||
* @return array|string Artist(s) URIs.
|
||||
*/
|
||||
public function get_attributed_to() {
|
||||
$artist_ids = get_post_meta( $this->post->ID, '_fedistream_artist_ids', true );
|
||||
|
||||
if ( ! is_array( $artist_ids ) || empty( $artist_ids ) ) {
|
||||
// Fall back to album artist.
|
||||
$album_id = get_post_meta( $this->post->ID, '_fedistream_album_id', true );
|
||||
$artist_id = $album_id ? get_post_meta( $album_id, '_fedistream_album_artist', true ) : 0;
|
||||
|
||||
if ( $artist_id ) {
|
||||
return get_permalink( $artist_id );
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
// Return single artist or array of artists.
|
||||
if ( count( $artist_ids ) === 1 ) {
|
||||
return get_permalink( $artist_ids[0] );
|
||||
}
|
||||
|
||||
return array_map( 'get_permalink', $artist_ids );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the published date.
|
||||
*
|
||||
* @return string ISO 8601 date.
|
||||
*/
|
||||
public function get_published(): string {
|
||||
return get_the_date( 'c', $this->post );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the updated date.
|
||||
*
|
||||
* @return string ISO 8601 date.
|
||||
*/
|
||||
public function get_updated(): string {
|
||||
return get_the_modified_date( 'c', $this->post );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the duration.
|
||||
*
|
||||
* @return string ISO 8601 duration.
|
||||
*/
|
||||
public function get_duration(): string {
|
||||
$seconds = (int) get_post_meta( $this->post->ID, '_fedistream_duration', true );
|
||||
|
||||
if ( ! $seconds ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->format_duration_iso8601( $seconds );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the audio attachment.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_audio_attachment(): ?array {
|
||||
$audio_id = get_post_meta( $this->post->ID, '_fedistream_audio_file', true );
|
||||
|
||||
if ( ! $audio_id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$audio_url = wp_get_attachment_url( $audio_id );
|
||||
$mime_type = get_post_mime_type( $audio_id );
|
||||
$audio_meta = wp_get_attachment_metadata( $audio_id );
|
||||
|
||||
if ( ! $audio_url ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$attachment = array(
|
||||
'type' => 'Audio',
|
||||
'mediaType' => $mime_type ?: 'audio/mpeg',
|
||||
'url' => $audio_url,
|
||||
'name' => $this->post->post_title,
|
||||
);
|
||||
|
||||
// Add duration if available.
|
||||
$duration = $this->get_duration();
|
||||
if ( $duration ) {
|
||||
$attachment['duration'] = $duration;
|
||||
}
|
||||
|
||||
return $attachment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the image/thumbnail attachment.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_image_attachment(): ?array {
|
||||
$thumbnail_id = get_post_thumbnail_id( $this->post->ID );
|
||||
|
||||
// Fall back to album artwork.
|
||||
if ( ! $thumbnail_id ) {
|
||||
$album_id = get_post_meta( $this->post->ID, '_fedistream_album_id', true );
|
||||
$thumbnail_id = $album_id ? get_post_thumbnail_id( $album_id ) : 0;
|
||||
}
|
||||
|
||||
if ( ! $thumbnail_id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$image = wp_get_attachment_image_src( $thumbnail_id, 'medium' );
|
||||
if ( ! $image ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'type' => 'Image',
|
||||
'mediaType' => get_post_mime_type( $thumbnail_id ),
|
||||
'url' => $image[0],
|
||||
'width' => $image[1],
|
||||
'height' => $image[2],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tags (genres, moods).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_tags(): array {
|
||||
$tags = array();
|
||||
|
||||
// Get genres.
|
||||
$genres = get_the_terms( $this->post->ID, 'fedistream_genre' );
|
||||
if ( $genres && ! is_wp_error( $genres ) ) {
|
||||
foreach ( $genres as $genre ) {
|
||||
$tags[] = array(
|
||||
'type' => 'Hashtag',
|
||||
'name' => '#' . sanitize_title( $genre->name ),
|
||||
'href' => get_term_link( $genre ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Get moods.
|
||||
$moods = get_the_terms( $this->post->ID, 'fedistream_mood' );
|
||||
if ( $moods && ! is_wp_error( $moods ) ) {
|
||||
foreach ( $moods as $mood ) {
|
||||
$tags[] = array(
|
||||
'type' => 'Hashtag',
|
||||
'name' => '#' . sanitize_title( $mood->name ),
|
||||
'href' => get_term_link( $mood ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the context (album).
|
||||
*
|
||||
* @return string|null Album URI if available.
|
||||
*/
|
||||
public function get_context(): ?string {
|
||||
$album_id = get_post_meta( $this->post->ID, '_fedistream_album_id', true );
|
||||
|
||||
if ( ! $album_id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return get_permalink( $album_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform to ActivityPub object array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_object(): array {
|
||||
$object = array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => $this->get_type(),
|
||||
'id' => $this->get_id(),
|
||||
'name' => $this->get_name(),
|
||||
'summary' => $this->get_summary(),
|
||||
'content' => $this->get_content(),
|
||||
'url' => $this->get_url(),
|
||||
'attributedTo' => $this->get_attributed_to(),
|
||||
'published' => $this->get_published(),
|
||||
'updated' => $this->get_updated(),
|
||||
);
|
||||
|
||||
// Add duration.
|
||||
$duration = $this->get_duration();
|
||||
if ( $duration ) {
|
||||
$object['duration'] = $duration;
|
||||
}
|
||||
|
||||
// Add attachments.
|
||||
$attachments = array();
|
||||
|
||||
$audio = $this->get_audio_attachment();
|
||||
if ( $audio ) {
|
||||
$attachments[] = $audio;
|
||||
}
|
||||
|
||||
$image = $this->get_image_attachment();
|
||||
if ( $image ) {
|
||||
$attachments[] = $image;
|
||||
}
|
||||
|
||||
if ( ! empty( $attachments ) ) {
|
||||
$object['attachment'] = $attachments;
|
||||
}
|
||||
|
||||
// Add tags.
|
||||
$tags = $this->get_tags();
|
||||
if ( ! empty( $tags ) ) {
|
||||
$object['tag'] = $tags;
|
||||
}
|
||||
|
||||
// Add context (album).
|
||||
$context = $this->get_context();
|
||||
if ( $context ) {
|
||||
$object['context'] = $context;
|
||||
}
|
||||
|
||||
// Add additional metadata.
|
||||
$explicit = get_post_meta( $this->post->ID, '_fedistream_explicit', true );
|
||||
if ( $explicit ) {
|
||||
$object['sensitive'] = true;
|
||||
}
|
||||
|
||||
// Add ISRC if available.
|
||||
$isrc = get_post_meta( $this->post->ID, '_fedistream_isrc', true );
|
||||
if ( $isrc ) {
|
||||
$object['isrc'] = $isrc;
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Create activity for this track.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_create_activity(): array {
|
||||
$attributed_to = $this->get_attributed_to();
|
||||
$actor = is_array( $attributed_to ) ? $attributed_to[0] : $attributed_to;
|
||||
|
||||
return array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Create',
|
||||
'id' => $this->get_id() . '#activity-create',
|
||||
'actor' => $actor,
|
||||
'published' => $this->get_published(),
|
||||
'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ),
|
||||
'cc' => array( $actor . '/followers' ),
|
||||
'object' => $this->to_object(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Announce activity for this track.
|
||||
*
|
||||
* @param string $actor_uri The actor announcing the track.
|
||||
* @return array
|
||||
*/
|
||||
public function to_announce_activity( string $actor_uri ): array {
|
||||
return array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Announce',
|
||||
'id' => $this->get_id() . '#activity-announce-' . time(),
|
||||
'actor' => $actor_uri,
|
||||
'published' => gmdate( 'c' ),
|
||||
'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ),
|
||||
'cc' => array( $actor_uri . '/followers' ),
|
||||
'object' => $this->get_id(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration as ISO 8601.
|
||||
*
|
||||
* @param int $seconds The duration in seconds.
|
||||
* @return string ISO 8601 duration.
|
||||
*/
|
||||
private function format_duration_iso8601( int $seconds ): string {
|
||||
$hours = floor( $seconds / 3600 );
|
||||
$minutes = floor( ( $seconds % 3600 ) / 60 );
|
||||
$secs = $seconds % 60;
|
||||
|
||||
$duration = 'PT';
|
||||
if ( $hours > 0 ) {
|
||||
$duration .= $hours . 'H';
|
||||
}
|
||||
if ( $minutes > 0 ) {
|
||||
$duration .= $minutes . 'M';
|
||||
}
|
||||
if ( $secs > 0 || ( $hours === 0 && $minutes === 0 ) ) {
|
||||
$duration .= $secs . 'S';
|
||||
}
|
||||
|
||||
return $duration;
|
||||
}
|
||||
}
|
||||
604
includes/Admin/ListColumns.php
Normal file
604
includes/Admin/ListColumns.php
Normal 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
1
includes/Admin/index.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php // Silence is golden.
|
||||
172
includes/Frontend/Ajax.php
Normal file
172
includes/Frontend/Ajax.php
Normal file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
/**
|
||||
* AJAX handlers for frontend functionality.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\Frontend;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles AJAX requests for the frontend.
|
||||
*/
|
||||
class Ajax {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
// Track data endpoint (public).
|
||||
add_action( 'wp_ajax_fedistream_get_track', array( $this, 'get_track' ) );
|
||||
add_action( 'wp_ajax_nopriv_fedistream_get_track', array( $this, 'get_track' ) );
|
||||
|
||||
// Record play endpoint (public).
|
||||
add_action( 'wp_ajax_fedistream_record_play', array( $this, 'record_play' ) );
|
||||
add_action( 'wp_ajax_nopriv_fedistream_record_play', array( $this, 'record_play' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track data via AJAX.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function get_track(): void {
|
||||
// Verify nonce.
|
||||
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['nonce'] ), 'wp-fedistream-nonce' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Invalid nonce.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
$track_id = isset( $_POST['track_id'] ) ? absint( $_POST['track_id'] ) : 0;
|
||||
|
||||
if ( ! $track_id ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Invalid track ID.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
$track = get_post( $track_id );
|
||||
|
||||
if ( ! $track || 'fedistream_track' !== $track->post_type ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Track not found.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
// Check if track is published.
|
||||
if ( 'publish' !== $track->post_status ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Track not available.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
// Get audio file.
|
||||
$audio_id = get_post_meta( $track_id, '_fedistream_audio_file', true );
|
||||
$audio_url = $audio_id ? wp_get_attachment_url( $audio_id ) : '';
|
||||
|
||||
if ( ! $audio_url ) {
|
||||
wp_send_json_error( array( 'message' => __( 'No audio file available.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
// Get track metadata.
|
||||
$thumbnail_id = get_post_thumbnail_id( $track_id );
|
||||
$thumbnail = $thumbnail_id ? wp_get_attachment_image_url( $thumbnail_id, 'medium' ) : '';
|
||||
|
||||
// Get album info.
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
$album = $album_id ? get_post( $album_id ) : null;
|
||||
|
||||
// Get artists.
|
||||
$artist_ids = get_post_meta( $track_id, '_fedistream_artist_ids', true );
|
||||
$artists = array();
|
||||
|
||||
if ( is_array( $artist_ids ) && ! empty( $artist_ids ) ) {
|
||||
foreach ( $artist_ids as $artist_id ) {
|
||||
$artist = get_post( $artist_id );
|
||||
if ( $artist && 'fedistream_artist' === $artist->post_type ) {
|
||||
$artists[] = array(
|
||||
'id' => $artist->ID,
|
||||
'name' => $artist->post_title,
|
||||
'link' => get_permalink( $artist->ID ),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get duration.
|
||||
$duration = get_post_meta( $track_id, '_fedistream_duration', true );
|
||||
$duration_formatted = '';
|
||||
if ( $duration ) {
|
||||
$mins = floor( $duration / 60 );
|
||||
$secs = $duration % 60;
|
||||
$duration_formatted = $mins . ':' . str_pad( $secs, 2, '0', STR_PAD_LEFT );
|
||||
}
|
||||
|
||||
$data = array(
|
||||
'id' => $track->ID,
|
||||
'title' => $track->post_title,
|
||||
'permalink' => get_permalink( $track->ID ),
|
||||
'audio_url' => $audio_url,
|
||||
'thumbnail' => $thumbnail,
|
||||
'artists' => $artists,
|
||||
'artist' => ! empty( $artists ) ? $artists[0]['name'] : '',
|
||||
'album' => $album ? $album->post_title : '',
|
||||
'album_id' => $album ? $album->ID : 0,
|
||||
'album_link' => $album ? get_permalink( $album->ID ) : '',
|
||||
'duration' => $duration,
|
||||
'duration_formatted' => $duration_formatted,
|
||||
'explicit' => (bool) get_post_meta( $track_id, '_fedistream_explicit', true ),
|
||||
);
|
||||
|
||||
wp_send_json_success( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a track play via AJAX.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function record_play(): void {
|
||||
// Verify nonce.
|
||||
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['nonce'] ), 'wp-fedistream-nonce' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Invalid nonce.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
$track_id = isset( $_POST['track_id'] ) ? absint( $_POST['track_id'] ) : 0;
|
||||
|
||||
if ( ! $track_id ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Invalid track ID.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
$track = get_post( $track_id );
|
||||
|
||||
if ( ! $track || 'fedistream_track' !== $track->post_type ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Track not found.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
// Insert play record.
|
||||
$table = $wpdb->prefix . 'fedistream_plays';
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$wpdb->insert(
|
||||
$table,
|
||||
array(
|
||||
'track_id' => $track_id,
|
||||
'user_id' => get_current_user_id() ?: null,
|
||||
'played_at' => current_time( 'mysql' ),
|
||||
),
|
||||
array( '%d', '%d', '%s' )
|
||||
);
|
||||
|
||||
// Update play count in post meta.
|
||||
$play_count = (int) get_post_meta( $track_id, '_fedistream_play_count', true );
|
||||
update_post_meta( $track_id, '_fedistream_play_count', $play_count + 1 );
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'message' => __( 'Play recorded.', 'wp-fedistream' ),
|
||||
'play_count' => $play_count + 1,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
513
includes/Frontend/Shortcodes.php
Normal file
513
includes/Frontend/Shortcodes.php
Normal file
@@ -0,0 +1,513 @@
|
||||
<?php
|
||||
/**
|
||||
* Shortcodes handler.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\Frontend;
|
||||
|
||||
use WP_FediStream\Plugin;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers and handles all plugin shortcodes.
|
||||
*/
|
||||
class Shortcodes {
|
||||
|
||||
/**
|
||||
* Plugin instance.
|
||||
*
|
||||
* @var Plugin
|
||||
*/
|
||||
private Plugin $plugin;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->plugin = Plugin::get_instance();
|
||||
$this->register_shortcodes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all shortcodes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function register_shortcodes(): void {
|
||||
add_shortcode( 'fedistream_artist', array( $this, 'render_artist' ) );
|
||||
add_shortcode( 'fedistream_album', array( $this, 'render_album' ) );
|
||||
add_shortcode( 'fedistream_track', array( $this, 'render_track' ) );
|
||||
add_shortcode( 'fedistream_playlist', array( $this, 'render_playlist' ) );
|
||||
add_shortcode( 'fedistream_latest_releases', array( $this, 'render_latest_releases' ) );
|
||||
add_shortcode( 'fedistream_popular_tracks', array( $this, 'render_popular_tracks' ) );
|
||||
add_shortcode( 'fedistream_artists', array( $this, 'render_artists_grid' ) );
|
||||
add_shortcode( 'fedistream_player', array( $this, 'render_player' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render single artist shortcode.
|
||||
*
|
||||
* [fedistream_artist id="123" show_albums="true" show_tracks="true"]
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
* @return string
|
||||
*/
|
||||
public function render_artist( array $atts ): string {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'id' => 0,
|
||||
'slug' => '',
|
||||
'show_albums' => 'true',
|
||||
'show_tracks' => 'true',
|
||||
'layout' => 'full', // full, card, compact
|
||||
),
|
||||
$atts,
|
||||
'fedistream_artist'
|
||||
);
|
||||
|
||||
$post = $this->get_post( $atts, 'fedistream_artist' );
|
||||
if ( ! $post ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$context = array(
|
||||
'post' => TemplateLoader::get_artist_data( $post ),
|
||||
'show_albums' => filter_var( $atts['show_albums'], FILTER_VALIDATE_BOOLEAN ),
|
||||
'show_tracks' => filter_var( $atts['show_tracks'], FILTER_VALIDATE_BOOLEAN ),
|
||||
'layout' => sanitize_key( $atts['layout'] ),
|
||||
);
|
||||
|
||||
$template = 'card' === $atts['layout'] ? 'partials/card-artist' : 'shortcodes/artist';
|
||||
|
||||
return $this->render_template( $template, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render single album shortcode.
|
||||
*
|
||||
* [fedistream_album id="123" show_tracks="true"]
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
* @return string
|
||||
*/
|
||||
public function render_album( array $atts ): string {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'id' => 0,
|
||||
'slug' => '',
|
||||
'show_tracks' => 'true',
|
||||
'layout' => 'full', // full, card, compact
|
||||
),
|
||||
$atts,
|
||||
'fedistream_album'
|
||||
);
|
||||
|
||||
$post = $this->get_post( $atts, 'fedistream_album' );
|
||||
if ( ! $post ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$context = array(
|
||||
'post' => TemplateLoader::get_album_data( $post ),
|
||||
'show_tracks' => filter_var( $atts['show_tracks'], FILTER_VALIDATE_BOOLEAN ),
|
||||
'layout' => sanitize_key( $atts['layout'] ),
|
||||
);
|
||||
|
||||
$template = 'card' === $atts['layout'] ? 'partials/card-album' : 'shortcodes/album';
|
||||
|
||||
return $this->render_template( $template, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render single track shortcode.
|
||||
*
|
||||
* [fedistream_track id="123" show_player="true"]
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
* @return string
|
||||
*/
|
||||
public function render_track( array $atts ): string {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'id' => 0,
|
||||
'slug' => '',
|
||||
'show_player' => 'true',
|
||||
'layout' => 'full', // full, card, compact
|
||||
),
|
||||
$atts,
|
||||
'fedistream_track'
|
||||
);
|
||||
|
||||
$post = $this->get_post( $atts, 'fedistream_track' );
|
||||
if ( ! $post ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$context = array(
|
||||
'post' => TemplateLoader::get_track_data( $post ),
|
||||
'show_player' => filter_var( $atts['show_player'], FILTER_VALIDATE_BOOLEAN ),
|
||||
'layout' => sanitize_key( $atts['layout'] ),
|
||||
);
|
||||
|
||||
$template = 'card' === $atts['layout'] ? 'partials/card-track' : 'shortcodes/track';
|
||||
|
||||
return $this->render_template( $template, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render playlist shortcode.
|
||||
*
|
||||
* [fedistream_playlist id="123" show_tracks="true"]
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
* @return string
|
||||
*/
|
||||
public function render_playlist( array $atts ): string {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'id' => 0,
|
||||
'slug' => '',
|
||||
'show_tracks' => 'true',
|
||||
'layout' => 'full', // full, card, compact
|
||||
),
|
||||
$atts,
|
||||
'fedistream_playlist'
|
||||
);
|
||||
|
||||
$post = $this->get_post( $atts, 'fedistream_playlist' );
|
||||
if ( ! $post ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$context = array(
|
||||
'post' => TemplateLoader::get_playlist_data( $post ),
|
||||
'show_tracks' => filter_var( $atts['show_tracks'], FILTER_VALIDATE_BOOLEAN ),
|
||||
'layout' => sanitize_key( $atts['layout'] ),
|
||||
);
|
||||
|
||||
$template = 'card' === $atts['layout'] ? 'partials/card-playlist' : 'shortcodes/playlist';
|
||||
|
||||
return $this->render_template( $template, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render latest releases shortcode.
|
||||
*
|
||||
* [fedistream_latest_releases count="6" type="album" columns="3"]
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
* @return string
|
||||
*/
|
||||
public function render_latest_releases( array $atts ): string {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'count' => 6,
|
||||
'type' => '', // album, ep, single, compilation or empty for all
|
||||
'columns' => 3,
|
||||
'artist' => 0, // Filter by artist ID
|
||||
),
|
||||
$atts,
|
||||
'fedistream_latest_releases'
|
||||
);
|
||||
|
||||
$query_args = array(
|
||||
'post_type' => 'fedistream_album',
|
||||
'posts_per_page' => absint( $atts['count'] ),
|
||||
'orderby' => 'meta_value',
|
||||
'meta_key' => '_fedistream_release_date',
|
||||
'order' => 'DESC',
|
||||
'post_status' => 'publish',
|
||||
);
|
||||
|
||||
// Filter by album type.
|
||||
if ( ! empty( $atts['type'] ) ) {
|
||||
$query_args['meta_query'][] = array(
|
||||
'key' => '_fedistream_album_type',
|
||||
'value' => sanitize_key( $atts['type'] ),
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by artist.
|
||||
if ( ! empty( $atts['artist'] ) ) {
|
||||
$query_args['meta_query'][] = array(
|
||||
'key' => '_fedistream_artist_id',
|
||||
'value' => absint( $atts['artist'] ),
|
||||
);
|
||||
}
|
||||
|
||||
$query = new \WP_Query( $query_args );
|
||||
$posts = array();
|
||||
|
||||
if ( $query->have_posts() ) {
|
||||
while ( $query->have_posts() ) {
|
||||
$query->the_post();
|
||||
$posts[] = TemplateLoader::get_album_data( get_post() );
|
||||
}
|
||||
wp_reset_postdata();
|
||||
}
|
||||
|
||||
$context = array(
|
||||
'posts' => $posts,
|
||||
'columns' => absint( $atts['columns'] ),
|
||||
'title' => __( 'Latest Releases', 'wp-fedistream' ),
|
||||
);
|
||||
|
||||
return $this->render_template( 'shortcodes/releases-grid', $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render popular tracks shortcode.
|
||||
*
|
||||
* [fedistream_popular_tracks count="10" columns="1"]
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
* @return string
|
||||
*/
|
||||
public function render_popular_tracks( array $atts ): string {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'count' => 10,
|
||||
'columns' => 1,
|
||||
'artist' => 0, // Filter by artist ID
|
||||
'genre' => '', // Filter by genre slug
|
||||
),
|
||||
$atts,
|
||||
'fedistream_popular_tracks'
|
||||
);
|
||||
|
||||
$query_args = array(
|
||||
'post_type' => 'fedistream_track',
|
||||
'posts_per_page' => absint( $atts['count'] ),
|
||||
'orderby' => 'meta_value_num',
|
||||
'meta_key' => '_fedistream_play_count',
|
||||
'order' => 'DESC',
|
||||
'post_status' => 'publish',
|
||||
);
|
||||
|
||||
// Filter by artist.
|
||||
if ( ! empty( $atts['artist'] ) ) {
|
||||
$query_args['meta_query'][] = array(
|
||||
'key' => '_fedistream_artist_ids',
|
||||
'value' => absint( $atts['artist'] ),
|
||||
'compare' => 'LIKE',
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by genre.
|
||||
if ( ! empty( $atts['genre'] ) ) {
|
||||
$query_args['tax_query'][] = array(
|
||||
'taxonomy' => 'fedistream_genre',
|
||||
'field' => 'slug',
|
||||
'terms' => sanitize_title( $atts['genre'] ),
|
||||
);
|
||||
}
|
||||
|
||||
$query = new \WP_Query( $query_args );
|
||||
$posts = array();
|
||||
|
||||
if ( $query->have_posts() ) {
|
||||
while ( $query->have_posts() ) {
|
||||
$query->the_post();
|
||||
$posts[] = TemplateLoader::get_track_data( get_post() );
|
||||
}
|
||||
wp_reset_postdata();
|
||||
}
|
||||
|
||||
$context = array(
|
||||
'posts' => $posts,
|
||||
'columns' => absint( $atts['columns'] ),
|
||||
'title' => __( 'Popular Tracks', 'wp-fedistream' ),
|
||||
);
|
||||
|
||||
return $this->render_template( 'shortcodes/tracks-list', $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render artists grid shortcode.
|
||||
*
|
||||
* [fedistream_artists count="12" columns="4" type="band"]
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
* @return string
|
||||
*/
|
||||
public function render_artists_grid( array $atts ): string {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'count' => 12,
|
||||
'columns' => 4,
|
||||
'type' => '', // solo, band, duo, collective, or empty for all
|
||||
'genre' => '', // Filter by genre slug
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
),
|
||||
$atts,
|
||||
'fedistream_artists'
|
||||
);
|
||||
|
||||
$query_args = array(
|
||||
'post_type' => 'fedistream_artist',
|
||||
'posts_per_page' => absint( $atts['count'] ),
|
||||
'orderby' => sanitize_key( $atts['orderby'] ),
|
||||
'order' => 'DESC' === strtoupper( $atts['order'] ) ? 'DESC' : 'ASC',
|
||||
'post_status' => 'publish',
|
||||
);
|
||||
|
||||
// Filter by artist type.
|
||||
if ( ! empty( $atts['type'] ) ) {
|
||||
$query_args['meta_query'][] = array(
|
||||
'key' => '_fedistream_artist_type',
|
||||
'value' => sanitize_key( $atts['type'] ),
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by genre.
|
||||
if ( ! empty( $atts['genre'] ) ) {
|
||||
$query_args['tax_query'][] = array(
|
||||
'taxonomy' => 'fedistream_genre',
|
||||
'field' => 'slug',
|
||||
'terms' => sanitize_title( $atts['genre'] ),
|
||||
);
|
||||
}
|
||||
|
||||
$query = new \WP_Query( $query_args );
|
||||
$posts = array();
|
||||
|
||||
if ( $query->have_posts() ) {
|
||||
while ( $query->have_posts() ) {
|
||||
$query->the_post();
|
||||
$posts[] = TemplateLoader::get_artist_data( get_post() );
|
||||
}
|
||||
wp_reset_postdata();
|
||||
}
|
||||
|
||||
$context = array(
|
||||
'posts' => $posts,
|
||||
'columns' => absint( $atts['columns'] ),
|
||||
'title' => __( 'Artists', 'wp-fedistream' ),
|
||||
);
|
||||
|
||||
return $this->render_template( 'shortcodes/artists-grid', $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render audio player shortcode.
|
||||
*
|
||||
* [fedistream_player track="123" autoplay="false"]
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
* @return string
|
||||
*/
|
||||
public function render_player( array $atts ): string {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'track' => 0,
|
||||
'playlist' => 0,
|
||||
'album' => 0,
|
||||
'autoplay' => 'false',
|
||||
'style' => 'default', // default, compact, mini
|
||||
),
|
||||
$atts,
|
||||
'fedistream_player'
|
||||
);
|
||||
|
||||
$tracks = array();
|
||||
|
||||
// Get tracks based on source.
|
||||
if ( ! empty( $atts['track'] ) ) {
|
||||
$post = get_post( absint( $atts['track'] ) );
|
||||
if ( $post && 'fedistream_track' === $post->post_type ) {
|
||||
$tracks[] = TemplateLoader::get_track_data( $post );
|
||||
}
|
||||
} elseif ( ! empty( $atts['album'] ) ) {
|
||||
$album_id = absint( $atts['album'] );
|
||||
$track_ids = get_post_meta( $album_id, '_fedistream_track_ids', true );
|
||||
if ( is_array( $track_ids ) ) {
|
||||
foreach ( $track_ids as $track_id ) {
|
||||
$post = get_post( $track_id );
|
||||
if ( $post && 'fedistream_track' === $post->post_type ) {
|
||||
$tracks[] = TemplateLoader::get_track_data( $post );
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ( ! empty( $atts['playlist'] ) ) {
|
||||
$playlist_id = absint( $atts['playlist'] );
|
||||
$track_ids = get_post_meta( $playlist_id, '_fedistream_track_ids', true );
|
||||
if ( is_array( $track_ids ) ) {
|
||||
foreach ( $track_ids as $track_id ) {
|
||||
$post = get_post( $track_id );
|
||||
if ( $post && 'fedistream_track' === $post->post_type ) {
|
||||
$tracks[] = TemplateLoader::get_track_data( $post );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty( $tracks ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$context = array(
|
||||
'tracks' => $tracks,
|
||||
'autoplay' => filter_var( $atts['autoplay'], FILTER_VALIDATE_BOOLEAN ),
|
||||
'style' => sanitize_key( $atts['style'] ),
|
||||
);
|
||||
|
||||
return $this->render_template( 'shortcodes/player', $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get post by ID or slug.
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
* @param string $post_type Post type.
|
||||
* @return \WP_Post|null
|
||||
*/
|
||||
private function get_post( array $atts, string $post_type ): ?\WP_Post {
|
||||
if ( ! empty( $atts['id'] ) ) {
|
||||
$post = get_post( absint( $atts['id'] ) );
|
||||
if ( $post && $post->post_type === $post_type ) {
|
||||
return $post;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $atts['slug'] ) ) {
|
||||
$posts = get_posts(
|
||||
array(
|
||||
'name' => sanitize_title( $atts['slug'] ),
|
||||
'post_type' => $post_type,
|
||||
'posts_per_page' => 1,
|
||||
'post_status' => 'publish',
|
||||
)
|
||||
);
|
||||
if ( ! empty( $posts ) ) {
|
||||
return $posts[0];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a Twig template.
|
||||
*
|
||||
* @param string $template Template name.
|
||||
* @param array $context Template context.
|
||||
* @return string
|
||||
*/
|
||||
private function render_template( string $template, array $context ): string {
|
||||
try {
|
||||
return $this->plugin->render( $template, $context );
|
||||
} catch ( \Exception $e ) {
|
||||
if ( WP_DEBUG ) {
|
||||
return '<p class="fedistream-error">' . esc_html( $e->getMessage() ) . '</p>';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
528
includes/Frontend/TemplateLoader.php
Normal file
528
includes/Frontend/TemplateLoader.php
Normal file
@@ -0,0 +1,528 @@
|
||||
<?php
|
||||
/**
|
||||
* Template loader for frontend display.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\Frontend;
|
||||
|
||||
use WP_FediStream\Plugin;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* TemplateLoader class.
|
||||
*
|
||||
* Handles loading custom templates for FediStream post types.
|
||||
*/
|
||||
class TemplateLoader {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_filter( 'template_include', array( $this, 'template_include' ) );
|
||||
add_filter( 'body_class', array( $this, 'body_class' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Include custom templates for FediStream post types.
|
||||
*
|
||||
* @param string $template Template path.
|
||||
* @return string Modified template path.
|
||||
*/
|
||||
public function template_include( string $template ): string {
|
||||
// Check if we're on a FediStream page.
|
||||
if ( is_singular( 'fedistream_artist' ) ) {
|
||||
return $this->get_template( 'single-artist' );
|
||||
}
|
||||
|
||||
if ( is_singular( 'fedistream_album' ) ) {
|
||||
return $this->get_template( 'single-album' );
|
||||
}
|
||||
|
||||
if ( is_singular( 'fedistream_track' ) ) {
|
||||
return $this->get_template( 'single-track' );
|
||||
}
|
||||
|
||||
if ( is_singular( 'fedistream_playlist' ) ) {
|
||||
return $this->get_template( 'single-playlist' );
|
||||
}
|
||||
|
||||
if ( is_post_type_archive( 'fedistream_artist' ) ) {
|
||||
return $this->get_template( 'archive-artist' );
|
||||
}
|
||||
|
||||
if ( is_post_type_archive( 'fedistream_album' ) ) {
|
||||
return $this->get_template( 'archive-album' );
|
||||
}
|
||||
|
||||
if ( is_post_type_archive( 'fedistream_track' ) ) {
|
||||
return $this->get_template( 'archive-track' );
|
||||
}
|
||||
|
||||
if ( is_post_type_archive( 'fedistream_playlist' ) ) {
|
||||
return $this->get_template( 'archive-playlist' );
|
||||
}
|
||||
|
||||
if ( is_tax( 'fedistream_genre' ) ) {
|
||||
return $this->get_template( 'taxonomy-genre' );
|
||||
}
|
||||
|
||||
if ( is_tax( 'fedistream_mood' ) ) {
|
||||
return $this->get_template( 'taxonomy-mood' );
|
||||
}
|
||||
|
||||
return $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get template file path.
|
||||
*
|
||||
* First checks theme for override, then uses plugin template.
|
||||
*
|
||||
* @param string $template_name Template name without extension.
|
||||
* @return string Template path.
|
||||
*/
|
||||
private function get_template( string $template_name ): string {
|
||||
// Check theme for override.
|
||||
$theme_template = locate_template(
|
||||
array(
|
||||
"fedistream/{$template_name}.php",
|
||||
"fedistream/{$template_name}.twig",
|
||||
)
|
||||
);
|
||||
|
||||
if ( $theme_template ) {
|
||||
return $theme_template;
|
||||
}
|
||||
|
||||
// Use plugin template wrapper.
|
||||
return WP_FEDISTREAM_PATH . 'includes/Frontend/template-wrapper.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Add FediStream classes to body.
|
||||
*
|
||||
* @param array $classes Body classes.
|
||||
* @return array Modified classes.
|
||||
*/
|
||||
public function body_class( array $classes ): array {
|
||||
if ( $this->is_fedistream_page() ) {
|
||||
$classes[] = 'fedistream';
|
||||
|
||||
if ( is_singular( 'fedistream_artist' ) ) {
|
||||
$classes[] = 'fedistream-artist';
|
||||
$classes[] = 'fedistream-single';
|
||||
} elseif ( is_singular( 'fedistream_album' ) ) {
|
||||
$classes[] = 'fedistream-album';
|
||||
$classes[] = 'fedistream-single';
|
||||
} elseif ( is_singular( 'fedistream_track' ) ) {
|
||||
$classes[] = 'fedistream-track';
|
||||
$classes[] = 'fedistream-single';
|
||||
} elseif ( is_singular( 'fedistream_playlist' ) ) {
|
||||
$classes[] = 'fedistream-playlist';
|
||||
$classes[] = 'fedistream-single';
|
||||
} elseif ( is_post_type_archive( 'fedistream_artist' ) ) {
|
||||
$classes[] = 'fedistream-archive';
|
||||
$classes[] = 'fedistream-artists';
|
||||
} elseif ( is_post_type_archive( 'fedistream_album' ) ) {
|
||||
$classes[] = 'fedistream-archive';
|
||||
$classes[] = 'fedistream-albums';
|
||||
} elseif ( is_post_type_archive( 'fedistream_track' ) ) {
|
||||
$classes[] = 'fedistream-archive';
|
||||
$classes[] = 'fedistream-tracks';
|
||||
} elseif ( is_post_type_archive( 'fedistream_playlist' ) ) {
|
||||
$classes[] = 'fedistream-archive';
|
||||
$classes[] = 'fedistream-playlists';
|
||||
} elseif ( is_tax( 'fedistream_genre' ) || is_tax( 'fedistream_mood' ) ) {
|
||||
$classes[] = 'fedistream-archive';
|
||||
$classes[] = 'fedistream-taxonomy';
|
||||
}
|
||||
}
|
||||
|
||||
return $classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current page is a FediStream page.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_fedistream_page(): bool {
|
||||
return is_singular( array( 'fedistream_artist', 'fedistream_album', 'fedistream_track', 'fedistream_playlist' ) )
|
||||
|| is_post_type_archive( array( 'fedistream_artist', 'fedistream_album', 'fedistream_track', 'fedistream_playlist' ) )
|
||||
|| is_tax( array( 'fedistream_genre', 'fedistream_mood', 'fedistream_license' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get template context for current page.
|
||||
*
|
||||
* @return array Template context.
|
||||
*/
|
||||
public static function get_context(): array {
|
||||
$context = array(
|
||||
'site_name' => get_bloginfo( 'name' ),
|
||||
'site_url' => home_url(),
|
||||
'is_singular' => is_singular(),
|
||||
'is_archive' => is_archive(),
|
||||
'current_url' => get_permalink(),
|
||||
);
|
||||
|
||||
if ( is_singular() ) {
|
||||
global $post;
|
||||
$context['post'] = self::get_post_data( $post );
|
||||
}
|
||||
|
||||
if ( is_post_type_archive() || is_tax() ) {
|
||||
$context['posts'] = self::get_archive_posts();
|
||||
$context['pagination'] = self::get_pagination();
|
||||
$context['archive_title'] = self::get_archive_title();
|
||||
$context['archive_description'] = self::get_archive_description();
|
||||
}
|
||||
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get post data for template.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return array Post data.
|
||||
*/
|
||||
public static function get_post_data( \WP_Post $post ): array {
|
||||
$data = array(
|
||||
'id' => $post->ID,
|
||||
'title' => get_the_title( $post ),
|
||||
'content' => apply_filters( 'the_content', $post->post_content ),
|
||||
'excerpt' => get_the_excerpt( $post ),
|
||||
'permalink' => get_permalink( $post ),
|
||||
'thumbnail' => get_the_post_thumbnail_url( $post->ID, 'large' ),
|
||||
'date' => get_the_date( '', $post ),
|
||||
'author' => get_the_author_meta( 'display_name', $post->post_author ),
|
||||
);
|
||||
|
||||
// Add post type specific data.
|
||||
switch ( $post->post_type ) {
|
||||
case 'fedistream_artist':
|
||||
$data = array_merge( $data, self::get_artist_data( $post->ID ) );
|
||||
break;
|
||||
case 'fedistream_album':
|
||||
$data = array_merge( $data, self::get_album_data( $post->ID ) );
|
||||
break;
|
||||
case 'fedistream_track':
|
||||
$data = array_merge( $data, self::get_track_data( $post->ID ) );
|
||||
break;
|
||||
case 'fedistream_playlist':
|
||||
$data = array_merge( $data, self::get_playlist_data( $post->ID ) );
|
||||
break;
|
||||
}
|
||||
|
||||
// Add taxonomies.
|
||||
$data['genres'] = self::get_terms( $post->ID, 'fedistream_genre' );
|
||||
$data['moods'] = self::get_terms( $post->ID, 'fedistream_mood' );
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get artist-specific data.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return array Artist data.
|
||||
*/
|
||||
private static function get_artist_data( int $post_id ): array {
|
||||
$type = get_post_meta( $post_id, '_fedistream_artist_type', true ) ?: 'solo';
|
||||
$types = array(
|
||||
'solo' => __( 'Solo Artist', 'wp-fedistream' ),
|
||||
'band' => __( 'Band', 'wp-fedistream' ),
|
||||
'duo' => __( 'Duo', 'wp-fedistream' ),
|
||||
'collective' => __( 'Collective', 'wp-fedistream' ),
|
||||
);
|
||||
|
||||
$albums = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_album',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
'meta_key' => '_fedistream_album_artist',
|
||||
'meta_value' => $post_id,
|
||||
'orderby' => 'meta_value',
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => '_fedistream_album_release_date',
|
||||
'compare' => 'EXISTS',
|
||||
),
|
||||
),
|
||||
'order' => 'DESC',
|
||||
)
|
||||
);
|
||||
|
||||
return array(
|
||||
'artist_type' => $type,
|
||||
'artist_type_label' => $types[ $type ] ?? $types['solo'],
|
||||
'formed_date' => get_post_meta( $post_id, '_fedistream_artist_formed_date', true ),
|
||||
'location' => get_post_meta( $post_id, '_fedistream_artist_location', true ),
|
||||
'website' => get_post_meta( $post_id, '_fedistream_artist_website', true ),
|
||||
'social_links' => get_post_meta( $post_id, '_fedistream_artist_social_links', true ) ?: array(),
|
||||
'members' => get_post_meta( $post_id, '_fedistream_artist_members', true ) ?: array(),
|
||||
'albums' => array_map( array( __CLASS__, 'get_post_data' ), $albums ),
|
||||
'album_count' => count( $albums ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get album-specific data.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return array Album data.
|
||||
*/
|
||||
private static function get_album_data( int $post_id ): array {
|
||||
$type = get_post_meta( $post_id, '_fedistream_album_type', true ) ?: 'album';
|
||||
$types = array(
|
||||
'album' => __( 'Album', 'wp-fedistream' ),
|
||||
'ep' => __( 'EP', 'wp-fedistream' ),
|
||||
'single' => __( 'Single', 'wp-fedistream' ),
|
||||
'compilation' => __( 'Compilation', 'wp-fedistream' ),
|
||||
'live' => __( 'Live Album', 'wp-fedistream' ),
|
||||
'remix' => __( 'Remix Album', 'wp-fedistream' ),
|
||||
);
|
||||
$artist_id = get_post_meta( $post_id, '_fedistream_album_artist', true );
|
||||
|
||||
$tracks = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_track',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
'meta_key' => '_fedistream_track_album',
|
||||
'meta_value' => $post_id,
|
||||
'orderby' => 'meta_value_num',
|
||||
'meta_query' => array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'key' => '_fedistream_track_number',
|
||||
'compare' => 'EXISTS',
|
||||
),
|
||||
),
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
|
||||
return array(
|
||||
'album_type' => $type,
|
||||
'album_type_label' => $types[ $type ] ?? $types['album'],
|
||||
'release_date' => get_post_meta( $post_id, '_fedistream_album_release_date', true ),
|
||||
'release_year' => date( 'Y', strtotime( get_post_meta( $post_id, '_fedistream_album_release_date', true ) ?: 'now' ) ),
|
||||
'artist_id' => $artist_id,
|
||||
'artist_name' => $artist_id ? get_the_title( $artist_id ) : '',
|
||||
'artist_url' => $artist_id ? get_permalink( $artist_id ) : '',
|
||||
'upc' => get_post_meta( $post_id, '_fedistream_album_upc', true ),
|
||||
'catalog_number' => get_post_meta( $post_id, '_fedistream_album_catalog_number', true ),
|
||||
'total_tracks' => count( $tracks ),
|
||||
'total_duration' => (int) get_post_meta( $post_id, '_fedistream_album_total_duration', true ),
|
||||
'tracks' => array_map( array( __CLASS__, 'get_post_data' ), $tracks ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track-specific data.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return array Track data.
|
||||
*/
|
||||
private static function get_track_data( int $post_id ): array {
|
||||
$album_id = get_post_meta( $post_id, '_fedistream_track_album', true );
|
||||
$audio_file = get_post_meta( $post_id, '_fedistream_track_audio_file', true );
|
||||
$artists = get_post_meta( $post_id, '_fedistream_track_artists', true ) ?: array();
|
||||
$duration = (int) get_post_meta( $post_id, '_fedistream_track_duration', true );
|
||||
|
||||
$artist_data = array();
|
||||
foreach ( $artists as $artist_id ) {
|
||||
$artist = get_post( $artist_id );
|
||||
if ( $artist ) {
|
||||
$artist_data[] = array(
|
||||
'id' => $artist_id,
|
||||
'name' => $artist->post_title,
|
||||
'url' => get_permalink( $artist_id ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'track_number' => (int) get_post_meta( $post_id, '_fedistream_track_number', true ),
|
||||
'disc_number' => (int) get_post_meta( $post_id, '_fedistream_track_disc_number', true ) ?: 1,
|
||||
'duration' => $duration,
|
||||
'duration_formatted' => $duration ? sprintf( '%d:%02d', floor( $duration / 60 ), $duration % 60 ) : '',
|
||||
'audio_url' => $audio_file ? wp_get_attachment_url( $audio_file ) : '',
|
||||
'audio_format' => get_post_meta( $post_id, '_fedistream_track_audio_format', true ),
|
||||
'bpm' => (int) get_post_meta( $post_id, '_fedistream_track_bpm', true ),
|
||||
'key' => get_post_meta( $post_id, '_fedistream_track_key', true ),
|
||||
'explicit' => (bool) get_post_meta( $post_id, '_fedistream_track_explicit', true ),
|
||||
'isrc' => get_post_meta( $post_id, '_fedistream_track_isrc', true ),
|
||||
'album_id' => $album_id,
|
||||
'album_title' => $album_id ? get_the_title( $album_id ) : '',
|
||||
'album_url' => $album_id ? get_permalink( $album_id ) : '',
|
||||
'album_artwork' => $album_id ? get_the_post_thumbnail_url( $album_id, 'medium' ) : '',
|
||||
'artists' => $artist_data,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get playlist-specific data.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return array Playlist data.
|
||||
*/
|
||||
private static function get_playlist_data( int $post_id ): array {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_playlist_tracks';
|
||||
$duration = (int) get_post_meta( $post_id, '_fedistream_playlist_total_duration', true );
|
||||
|
||||
// Get tracks.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$track_ids = $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"SELECT track_id FROM $table WHERE playlist_id = %d ORDER BY position ASC",
|
||||
$post_id
|
||||
)
|
||||
);
|
||||
|
||||
$tracks = array();
|
||||
foreach ( $track_ids as $track_id ) {
|
||||
$track = get_post( $track_id );
|
||||
if ( $track && 'publish' === $track->post_status ) {
|
||||
$tracks[] = self::get_post_data( $track );
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'visibility' => get_post_meta( $post_id, '_fedistream_playlist_visibility', true ) ?: 'public',
|
||||
'collaborative' => (bool) get_post_meta( $post_id, '_fedistream_playlist_collaborative', true ),
|
||||
'federated' => (bool) get_post_meta( $post_id, '_fedistream_playlist_federated', true ),
|
||||
'track_count' => count( $tracks ),
|
||||
'total_duration' => $duration,
|
||||
'duration_formatted' => $duration >= 3600
|
||||
? sprintf( '%d:%02d:%02d', floor( $duration / 3600 ), floor( ( $duration % 3600 ) / 60 ), $duration % 60 )
|
||||
: sprintf( '%d:%02d', floor( $duration / 60 ), $duration % 60 ),
|
||||
'tracks' => $tracks,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get taxonomy terms for post.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $taxonomy Taxonomy name.
|
||||
* @return array Terms with name and URL.
|
||||
*/
|
||||
private static function get_terms( int $post_id, string $taxonomy ): array {
|
||||
$terms = get_the_terms( $post_id, $taxonomy );
|
||||
|
||||
if ( ! $terms || is_wp_error( $terms ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return array_map(
|
||||
function ( $term ) {
|
||||
return array(
|
||||
'id' => $term->term_id,
|
||||
'name' => $term->name,
|
||||
'slug' => $term->slug,
|
||||
'url' => get_term_link( $term ),
|
||||
);
|
||||
},
|
||||
$terms
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get archive posts.
|
||||
*
|
||||
* @return array Posts for archive.
|
||||
*/
|
||||
private static function get_archive_posts(): array {
|
||||
global $wp_query;
|
||||
|
||||
$posts = array();
|
||||
if ( $wp_query->have_posts() ) {
|
||||
while ( $wp_query->have_posts() ) {
|
||||
$wp_query->the_post();
|
||||
$posts[] = self::get_post_data( get_post() );
|
||||
}
|
||||
wp_reset_postdata();
|
||||
}
|
||||
|
||||
return $posts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pagination data.
|
||||
*
|
||||
* @return array Pagination data.
|
||||
*/
|
||||
private static function get_pagination(): array {
|
||||
global $wp_query;
|
||||
|
||||
$total_pages = $wp_query->max_num_pages;
|
||||
$current = max( 1, get_query_var( 'paged' ) );
|
||||
|
||||
return array(
|
||||
'total_pages' => $total_pages,
|
||||
'current_page' => $current,
|
||||
'has_prev' => $current > 1,
|
||||
'has_next' => $current < $total_pages,
|
||||
'prev_url' => $current > 1 ? get_pagenum_link( $current - 1 ) : '',
|
||||
'next_url' => $current < $total_pages ? get_pagenum_link( $current + 1 ) : '',
|
||||
'links' => paginate_links(
|
||||
array(
|
||||
'total' => $total_pages,
|
||||
'current' => $current,
|
||||
'type' => 'array',
|
||||
'prev_text' => __( '« Previous', 'wp-fedistream' ),
|
||||
'next_text' => __( 'Next »', 'wp-fedistream' ),
|
||||
)
|
||||
) ?: array(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get archive title.
|
||||
*
|
||||
* @return string Archive title.
|
||||
*/
|
||||
private static function get_archive_title(): string {
|
||||
if ( is_post_type_archive( 'fedistream_artist' ) ) {
|
||||
return __( 'Artists', 'wp-fedistream' );
|
||||
}
|
||||
if ( is_post_type_archive( 'fedistream_album' ) ) {
|
||||
return __( 'Albums', 'wp-fedistream' );
|
||||
}
|
||||
if ( is_post_type_archive( 'fedistream_track' ) ) {
|
||||
return __( 'Tracks', 'wp-fedistream' );
|
||||
}
|
||||
if ( is_post_type_archive( 'fedistream_playlist' ) ) {
|
||||
return __( 'Playlists', 'wp-fedistream' );
|
||||
}
|
||||
if ( is_tax() ) {
|
||||
return single_term_title( '', false );
|
||||
}
|
||||
return get_the_archive_title();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get archive description.
|
||||
*
|
||||
* @return string Archive description.
|
||||
*/
|
||||
private static function get_archive_description(): string {
|
||||
if ( is_tax() ) {
|
||||
return term_description();
|
||||
}
|
||||
return get_the_archive_description();
|
||||
}
|
||||
}
|
||||
38
includes/Frontend/Widgets.php
Normal file
38
includes/Frontend/Widgets.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
/**
|
||||
* Widgets handler.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\Frontend;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers and manages all plugin widgets.
|
||||
*/
|
||||
class Widgets {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'widgets_init', array( $this, 'register_widgets' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all widgets.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_widgets(): void {
|
||||
register_widget( Widgets\RecentReleasesWidget::class );
|
||||
register_widget( Widgets\PopularTracksWidget::class );
|
||||
register_widget( Widgets\FeaturedArtistWidget::class );
|
||||
register_widget( Widgets\NowPlayingWidget::class );
|
||||
}
|
||||
}
|
||||
162
includes/Frontend/Widgets/FeaturedArtistWidget.php
Normal file
162
includes/Frontend/Widgets/FeaturedArtistWidget.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
/**
|
||||
* Featured Artist Widget.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\Frontend\Widgets;
|
||||
|
||||
use WP_FediStream\Frontend\TemplateLoader;
|
||||
use WP_FediStream\Plugin;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a featured artist.
|
||||
*/
|
||||
class FeaturedArtistWidget extends \WP_Widget {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct(
|
||||
'fedistream_featured_artist',
|
||||
__( 'FediStream: Featured Artist', 'wp-fedistream' ),
|
||||
array(
|
||||
'description' => __( 'Display a featured artist.', 'wp-fedistream' ),
|
||||
'classname' => 'fedistream-widget fedistream-widget--featured-artist',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Front-end display.
|
||||
*
|
||||
* @param array $args Widget arguments.
|
||||
* @param array $instance Saved values from database.
|
||||
* @return void
|
||||
*/
|
||||
public function widget( $args, $instance ): void {
|
||||
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Featured Artist', 'wp-fedistream' );
|
||||
$title = apply_filters( 'widget_title', $title, $instance, $this->id_base );
|
||||
$artist_id = ! empty( $instance['artist_id'] ) ? absint( $instance['artist_id'] ) : 0;
|
||||
$random = ! empty( $instance['random'] ) && $instance['random'];
|
||||
|
||||
$post = null;
|
||||
|
||||
if ( $random ) {
|
||||
// Get a random artist.
|
||||
$posts = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_artist',
|
||||
'posts_per_page' => 1,
|
||||
'orderby' => 'rand',
|
||||
'post_status' => 'publish',
|
||||
)
|
||||
);
|
||||
if ( ! empty( $posts ) ) {
|
||||
$post = $posts[0];
|
||||
}
|
||||
} elseif ( $artist_id ) {
|
||||
$post = get_post( $artist_id );
|
||||
if ( $post && 'fedistream_artist' !== $post->post_type ) {
|
||||
$post = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $post ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$artist_data = TemplateLoader::get_artist_data( $post );
|
||||
|
||||
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
|
||||
if ( $title ) {
|
||||
echo $args['before_title'] . esc_html( $title ) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
try {
|
||||
$plugin = Plugin::get_instance();
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $plugin->render(
|
||||
'widgets/featured-artist',
|
||||
array(
|
||||
'post' => $artist_data,
|
||||
)
|
||||
);
|
||||
} catch ( \Exception $e ) {
|
||||
if ( WP_DEBUG ) {
|
||||
echo '<p class="fedistream-error">' . esc_html( $e->getMessage() ) . '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
/**
|
||||
* Back-end widget form.
|
||||
*
|
||||
* @param array $instance Previously saved values from database.
|
||||
* @return void
|
||||
*/
|
||||
public function form( $instance ): void {
|
||||
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Featured Artist', 'wp-fedistream' );
|
||||
$artist_id = ! empty( $instance['artist_id'] ) ? absint( $instance['artist_id'] ) : 0;
|
||||
$random = ! empty( $instance['random'] ) && $instance['random'];
|
||||
|
||||
// Get all artists for dropdown.
|
||||
$artists = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_artist',
|
||||
'posts_per_page' => -1,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
'post_status' => 'publish',
|
||||
)
|
||||
);
|
||||
?>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Title:', 'wp-fedistream' ); ?></label>
|
||||
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" id="<?php echo esc_attr( $this->get_field_id( 'random' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'random' ) ); ?>" value="1" <?php checked( $random ); ?>>
|
||||
<?php esc_html_e( 'Show random artist', 'wp-fedistream' ); ?>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'artist_id' ) ); ?>"><?php esc_html_e( 'Or select specific artist:', 'wp-fedistream' ); ?></label>
|
||||
<select class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'artist_id' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'artist_id' ) ); ?>">
|
||||
<option value=""><?php esc_html_e( '-- Select Artist --', 'wp-fedistream' ); ?></option>
|
||||
<?php foreach ( $artists as $artist ) : ?>
|
||||
<option value="<?php echo esc_attr( $artist->ID ); ?>" <?php selected( $artist_id, $artist->ID ); ?>>
|
||||
<?php echo esc_html( $artist->post_title ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize widget form values as they are saved.
|
||||
*
|
||||
* @param array $new_instance Values just sent to be saved.
|
||||
* @param array $old_instance Previously saved values from database.
|
||||
* @return array Updated safe values to be saved.
|
||||
*/
|
||||
public function update( $new_instance, $old_instance ): array {
|
||||
$instance = array();
|
||||
$instance['title'] = ! empty( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '';
|
||||
$instance['artist_id'] = ! empty( $new_instance['artist_id'] ) ? absint( $new_instance['artist_id'] ) : 0;
|
||||
$instance['random'] = ! empty( $new_instance['random'] );
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
111
includes/Frontend/Widgets/NowPlayingWidget.php
Normal file
111
includes/Frontend/Widgets/NowPlayingWidget.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
/**
|
||||
* Now Playing Widget.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\Frontend\Widgets;
|
||||
|
||||
use WP_FediStream\Plugin;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the currently playing track (updates via JavaScript).
|
||||
*/
|
||||
class NowPlayingWidget extends \WP_Widget {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct(
|
||||
'fedistream_now_playing',
|
||||
__( 'FediStream: Now Playing', 'wp-fedistream' ),
|
||||
array(
|
||||
'description' => __( 'Display the currently playing track.', 'wp-fedistream' ),
|
||||
'classname' => 'fedistream-widget fedistream-widget--now-playing',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Front-end display.
|
||||
*
|
||||
* @param array $args Widget arguments.
|
||||
* @param array $instance Saved values from database.
|
||||
* @return void
|
||||
*/
|
||||
public function widget( $args, $instance ): void {
|
||||
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Now Playing', 'wp-fedistream' );
|
||||
$title = apply_filters( 'widget_title', $title, $instance, $this->id_base );
|
||||
$show_player = ! empty( $instance['show_player'] );
|
||||
|
||||
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
|
||||
if ( $title ) {
|
||||
echo $args['before_title'] . esc_html( $title ) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
try {
|
||||
$plugin = Plugin::get_instance();
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $plugin->render(
|
||||
'widgets/now-playing',
|
||||
array(
|
||||
'show_player' => $show_player,
|
||||
)
|
||||
);
|
||||
} catch ( \Exception $e ) {
|
||||
if ( WP_DEBUG ) {
|
||||
echo '<p class="fedistream-error">' . esc_html( $e->getMessage() ) . '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
/**
|
||||
* Back-end widget form.
|
||||
*
|
||||
* @param array $instance Previously saved values from database.
|
||||
* @return void
|
||||
*/
|
||||
public function form( $instance ): void {
|
||||
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Now Playing', 'wp-fedistream' );
|
||||
$show_player = ! empty( $instance['show_player'] );
|
||||
?>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Title:', 'wp-fedistream' ); ?></label>
|
||||
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" id="<?php echo esc_attr( $this->get_field_id( 'show_player' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'show_player' ) ); ?>" value="1" <?php checked( $show_player ); ?>>
|
||||
<?php esc_html_e( 'Show player controls', 'wp-fedistream' ); ?>
|
||||
</label>
|
||||
</p>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'This widget shows information about the currently playing track and updates automatically via JavaScript.', 'wp-fedistream' ); ?>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize widget form values as they are saved.
|
||||
*
|
||||
* @param array $new_instance Values just sent to be saved.
|
||||
* @param array $old_instance Previously saved values from database.
|
||||
* @return array Updated safe values to be saved.
|
||||
*/
|
||||
public function update( $new_instance, $old_instance ): array {
|
||||
$instance = array();
|
||||
$instance['title'] = ! empty( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '';
|
||||
$instance['show_player'] = ! empty( $new_instance['show_player'] );
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
127
includes/Frontend/Widgets/PopularTracksWidget.php
Normal file
127
includes/Frontend/Widgets/PopularTracksWidget.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
/**
|
||||
* Popular Tracks Widget.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\Frontend\Widgets;
|
||||
|
||||
use WP_FediStream\Frontend\TemplateLoader;
|
||||
use WP_FediStream\Plugin;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays popular tracks based on play count.
|
||||
*/
|
||||
class PopularTracksWidget extends \WP_Widget {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct(
|
||||
'fedistream_popular_tracks',
|
||||
__( 'FediStream: Popular Tracks', 'wp-fedistream' ),
|
||||
array(
|
||||
'description' => __( 'Display popular tracks by play count.', 'wp-fedistream' ),
|
||||
'classname' => 'fedistream-widget fedistream-widget--popular-tracks',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Front-end display.
|
||||
*
|
||||
* @param array $args Widget arguments.
|
||||
* @param array $instance Saved values from database.
|
||||
* @return void
|
||||
*/
|
||||
public function widget( $args, $instance ): void {
|
||||
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Popular Tracks', 'wp-fedistream' );
|
||||
$title = apply_filters( 'widget_title', $title, $instance, $this->id_base );
|
||||
$count = ! empty( $instance['count'] ) ? absint( $instance['count'] ) : 5;
|
||||
|
||||
$query_args = array(
|
||||
'post_type' => 'fedistream_track',
|
||||
'posts_per_page' => $count,
|
||||
'orderby' => 'meta_value_num',
|
||||
'meta_key' => '_fedistream_play_count',
|
||||
'order' => 'DESC',
|
||||
'post_status' => 'publish',
|
||||
);
|
||||
|
||||
$query = new \WP_Query( $query_args );
|
||||
$posts = array();
|
||||
|
||||
if ( $query->have_posts() ) {
|
||||
while ( $query->have_posts() ) {
|
||||
$query->the_post();
|
||||
$posts[] = TemplateLoader::get_track_data( get_post() );
|
||||
}
|
||||
wp_reset_postdata();
|
||||
}
|
||||
|
||||
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
|
||||
if ( $title ) {
|
||||
echo $args['before_title'] . esc_html( $title ) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
try {
|
||||
$plugin = Plugin::get_instance();
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $plugin->render(
|
||||
'widgets/popular-tracks',
|
||||
array(
|
||||
'posts' => $posts,
|
||||
)
|
||||
);
|
||||
} catch ( \Exception $e ) {
|
||||
if ( WP_DEBUG ) {
|
||||
echo '<p class="fedistream-error">' . esc_html( $e->getMessage() ) . '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
/**
|
||||
* Back-end widget form.
|
||||
*
|
||||
* @param array $instance Previously saved values from database.
|
||||
* @return void
|
||||
*/
|
||||
public function form( $instance ): void {
|
||||
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Popular Tracks', 'wp-fedistream' );
|
||||
$count = ! empty( $instance['count'] ) ? absint( $instance['count'] ) : 5;
|
||||
?>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Title:', 'wp-fedistream' ); ?></label>
|
||||
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>"><?php esc_html_e( 'Number of tracks:', 'wp-fedistream' ); ?></label>
|
||||
<input class="tiny-text" id="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'count' ) ); ?>" type="number" min="1" max="20" value="<?php echo esc_attr( $count ); ?>">
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize widget form values as they are saved.
|
||||
*
|
||||
* @param array $new_instance Values just sent to be saved.
|
||||
* @param array $old_instance Previously saved values from database.
|
||||
* @return array Updated safe values to be saved.
|
||||
*/
|
||||
public function update( $new_instance, $old_instance ): array {
|
||||
$instance = array();
|
||||
$instance['title'] = ! empty( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '';
|
||||
$instance['count'] = ! empty( $new_instance['count'] ) ? absint( $new_instance['count'] ) : 5;
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
147
includes/Frontend/Widgets/RecentReleasesWidget.php
Normal file
147
includes/Frontend/Widgets/RecentReleasesWidget.php
Normal file
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
/**
|
||||
* Recent Releases Widget.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\Frontend\Widgets;
|
||||
|
||||
use WP_FediStream\Frontend\TemplateLoader;
|
||||
use WP_FediStream\Plugin;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays recent album releases.
|
||||
*/
|
||||
class RecentReleasesWidget extends \WP_Widget {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct(
|
||||
'fedistream_recent_releases',
|
||||
__( 'FediStream: Recent Releases', 'wp-fedistream' ),
|
||||
array(
|
||||
'description' => __( 'Display recent album releases.', 'wp-fedistream' ),
|
||||
'classname' => 'fedistream-widget fedistream-widget--recent-releases',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Front-end display.
|
||||
*
|
||||
* @param array $args Widget arguments.
|
||||
* @param array $instance Saved values from database.
|
||||
* @return void
|
||||
*/
|
||||
public function widget( $args, $instance ): void {
|
||||
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Recent Releases', 'wp-fedistream' );
|
||||
$title = apply_filters( 'widget_title', $title, $instance, $this->id_base );
|
||||
$count = ! empty( $instance['count'] ) ? absint( $instance['count'] ) : 5;
|
||||
$type = ! empty( $instance['type'] ) ? $instance['type'] : '';
|
||||
|
||||
$query_args = array(
|
||||
'post_type' => 'fedistream_album',
|
||||
'posts_per_page' => $count,
|
||||
'orderby' => 'meta_value',
|
||||
'meta_key' => '_fedistream_release_date',
|
||||
'order' => 'DESC',
|
||||
'post_status' => 'publish',
|
||||
);
|
||||
|
||||
if ( ! empty( $type ) ) {
|
||||
$query_args['meta_query'][] = array(
|
||||
'key' => '_fedistream_album_type',
|
||||
'value' => $type,
|
||||
);
|
||||
}
|
||||
|
||||
$query = new \WP_Query( $query_args );
|
||||
$posts = array();
|
||||
|
||||
if ( $query->have_posts() ) {
|
||||
while ( $query->have_posts() ) {
|
||||
$query->the_post();
|
||||
$posts[] = TemplateLoader::get_album_data( get_post() );
|
||||
}
|
||||
wp_reset_postdata();
|
||||
}
|
||||
|
||||
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
|
||||
if ( $title ) {
|
||||
echo $args['before_title'] . esc_html( $title ) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
try {
|
||||
$plugin = Plugin::get_instance();
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $plugin->render(
|
||||
'widgets/recent-releases',
|
||||
array(
|
||||
'posts' => $posts,
|
||||
)
|
||||
);
|
||||
} catch ( \Exception $e ) {
|
||||
if ( WP_DEBUG ) {
|
||||
echo '<p class="fedistream-error">' . esc_html( $e->getMessage() ) . '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
/**
|
||||
* Back-end widget form.
|
||||
*
|
||||
* @param array $instance Previously saved values from database.
|
||||
* @return void
|
||||
*/
|
||||
public function form( $instance ): void {
|
||||
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Recent Releases', 'wp-fedistream' );
|
||||
$count = ! empty( $instance['count'] ) ? absint( $instance['count'] ) : 5;
|
||||
$type = ! empty( $instance['type'] ) ? $instance['type'] : '';
|
||||
?>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Title:', 'wp-fedistream' ); ?></label>
|
||||
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>"><?php esc_html_e( 'Number of releases:', 'wp-fedistream' ); ?></label>
|
||||
<input class="tiny-text" id="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'count' ) ); ?>" type="number" min="1" max="20" value="<?php echo esc_attr( $count ); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'type' ) ); ?>"><?php esc_html_e( 'Release type:', 'wp-fedistream' ); ?></label>
|
||||
<select class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'type' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'type' ) ); ?>">
|
||||
<option value="" <?php selected( $type, '' ); ?>><?php esc_html_e( 'All types', 'wp-fedistream' ); ?></option>
|
||||
<option value="album" <?php selected( $type, 'album' ); ?>><?php esc_html_e( 'Album', 'wp-fedistream' ); ?></option>
|
||||
<option value="ep" <?php selected( $type, 'ep' ); ?>><?php esc_html_e( 'EP', 'wp-fedistream' ); ?></option>
|
||||
<option value="single" <?php selected( $type, 'single' ); ?>><?php esc_html_e( 'Single', 'wp-fedistream' ); ?></option>
|
||||
<option value="compilation" <?php selected( $type, 'compilation' ); ?>><?php esc_html_e( 'Compilation', 'wp-fedistream' ); ?></option>
|
||||
</select>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize widget form values as they are saved.
|
||||
*
|
||||
* @param array $new_instance Values just sent to be saved.
|
||||
* @param array $old_instance Previously saved values from database.
|
||||
* @return array Updated safe values to be saved.
|
||||
*/
|
||||
public function update( $new_instance, $old_instance ): array {
|
||||
$instance = array();
|
||||
$instance['title'] = ! empty( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '';
|
||||
$instance['count'] = ! empty( $new_instance['count'] ) ? absint( $new_instance['count'] ) : 5;
|
||||
$instance['type'] = ! empty( $new_instance['type'] ) ? sanitize_key( $new_instance['type'] ) : '';
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
78
includes/Frontend/template-wrapper.php
Normal file
78
includes/Frontend/template-wrapper.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
/**
|
||||
* Template wrapper for FediStream Twig templates.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
use WP_FediStream\Plugin;
|
||||
use WP_FediStream\Frontend\TemplateLoader;
|
||||
|
||||
// Get template context.
|
||||
$context = TemplateLoader::get_context();
|
||||
|
||||
// Determine template name.
|
||||
$template_name = '';
|
||||
|
||||
if ( is_singular( 'fedistream_artist' ) ) {
|
||||
$template_name = 'single/artist';
|
||||
} elseif ( is_singular( 'fedistream_album' ) ) {
|
||||
$template_name = 'single/album';
|
||||
} elseif ( is_singular( 'fedistream_track' ) ) {
|
||||
$template_name = 'single/track';
|
||||
} elseif ( is_singular( 'fedistream_playlist' ) ) {
|
||||
$template_name = 'single/playlist';
|
||||
} elseif ( is_post_type_archive( 'fedistream_artist' ) ) {
|
||||
$template_name = 'archive/artist';
|
||||
} elseif ( is_post_type_archive( 'fedistream_album' ) ) {
|
||||
$template_name = 'archive/album';
|
||||
} elseif ( is_post_type_archive( 'fedistream_track' ) ) {
|
||||
$template_name = 'archive/track';
|
||||
} elseif ( is_post_type_archive( 'fedistream_playlist' ) ) {
|
||||
$template_name = 'archive/playlist';
|
||||
} elseif ( is_tax( 'fedistream_genre' ) ) {
|
||||
$template_name = 'archive/taxonomy';
|
||||
$context['taxonomy_name'] = __( 'Genre', 'wp-fedistream' );
|
||||
} elseif ( is_tax( 'fedistream_mood' ) ) {
|
||||
$template_name = 'archive/taxonomy';
|
||||
$context['taxonomy_name'] = __( 'Mood', 'wp-fedistream' );
|
||||
}
|
||||
|
||||
// Get the plugin instance.
|
||||
$plugin = Plugin::get_instance();
|
||||
|
||||
get_header();
|
||||
?>
|
||||
|
||||
<main id="fedistream-content" class="fedistream-main">
|
||||
<?php
|
||||
if ( $template_name ) {
|
||||
try {
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $plugin->render( $template_name, $context );
|
||||
} catch ( \Exception $e ) {
|
||||
if ( WP_DEBUG ) {
|
||||
echo '<div class="fedistream-error">';
|
||||
echo '<p>' . esc_html__( 'Template Error:', 'wp-fedistream' ) . ' ' . esc_html( $e->getMessage() ) . '</p>';
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback to default content.
|
||||
if ( have_posts() ) {
|
||||
while ( have_posts() ) {
|
||||
the_post();
|
||||
the_content();
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
</main>
|
||||
|
||||
<?php
|
||||
get_footer();
|
||||
462
includes/Installer.php
Normal file
462
includes/Installer.php
Normal file
@@ -0,0 +1,462 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin installer class.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream;
|
||||
|
||||
use WP_FediStream\Roles\Capabilities;
|
||||
use WP_FediStream\Taxonomies\Genre;
|
||||
use WP_FediStream\Taxonomies\Mood;
|
||||
use WP_FediStream\Taxonomies\License;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles plugin activation, deactivation, and uninstallation.
|
||||
*/
|
||||
class Installer {
|
||||
|
||||
/**
|
||||
* Plugin activation.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function activate(): void {
|
||||
self::create_tables();
|
||||
self::create_directories();
|
||||
self::set_default_options();
|
||||
self::schedule_events();
|
||||
|
||||
// Install roles and capabilities.
|
||||
Capabilities::install();
|
||||
|
||||
// Store installed version.
|
||||
update_option( 'wp_fedistream_version', WP_FEDISTREAM_VERSION );
|
||||
|
||||
// Flush rewrite rules for custom post types.
|
||||
flush_rewrite_rules();
|
||||
|
||||
// Install default taxonomy terms (after rewrite rules flush).
|
||||
// Schedule for next page load since taxonomies need to be registered first.
|
||||
update_option( 'wp_fedistream_install_defaults', 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin deactivation.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function deactivate(): void {
|
||||
self::unschedule_events();
|
||||
|
||||
// Flush rewrite rules.
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin uninstallation.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function uninstall(): void {
|
||||
// Only run if uninstall is explicitly requested.
|
||||
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::delete_tables();
|
||||
self::delete_options();
|
||||
self::delete_user_meta();
|
||||
self::delete_transients();
|
||||
self::delete_posts();
|
||||
self::delete_terms();
|
||||
|
||||
// Remove roles and capabilities.
|
||||
Capabilities::uninstall();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all plugin posts.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function delete_posts(): void {
|
||||
$post_types = array(
|
||||
'fedistream_artist',
|
||||
'fedistream_album',
|
||||
'fedistream_track',
|
||||
'fedistream_playlist',
|
||||
);
|
||||
|
||||
foreach ( $post_types as $post_type ) {
|
||||
$posts = get_posts(
|
||||
array(
|
||||
'post_type' => $post_type,
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'any',
|
||||
'fields' => 'ids',
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $posts as $post_id ) {
|
||||
wp_delete_post( $post_id, true );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all plugin taxonomy terms.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function delete_terms(): void {
|
||||
$taxonomies = array(
|
||||
'fedistream_genre',
|
||||
'fedistream_mood',
|
||||
'fedistream_license',
|
||||
);
|
||||
|
||||
foreach ( $taxonomies as $taxonomy ) {
|
||||
$terms = get_terms(
|
||||
array(
|
||||
'taxonomy' => $taxonomy,
|
||||
'hide_empty' => false,
|
||||
'fields' => 'ids',
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! is_wp_error( $terms ) ) {
|
||||
foreach ( $terms as $term_id ) {
|
||||
wp_delete_term( $term_id, $taxonomy );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install default taxonomy terms.
|
||||
*
|
||||
* Called on first load after activation when taxonomies are registered.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function install_defaults(): void {
|
||||
if ( ! get_option( 'wp_fedistream_install_defaults' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Install default genres.
|
||||
Genre::install_defaults();
|
||||
|
||||
// Install default moods.
|
||||
Mood::install_defaults();
|
||||
|
||||
// Install default licenses.
|
||||
License::install_defaults();
|
||||
|
||||
// Clear the flag.
|
||||
delete_option( 'wp_fedistream_install_defaults' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create custom database tables.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function create_tables(): void {
|
||||
global $wpdb;
|
||||
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
// Track plays table.
|
||||
$table_plays = $wpdb->prefix . 'fedistream_plays';
|
||||
$sql_plays = "CREATE TABLE $table_plays (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
track_id bigint(20) unsigned NOT NULL,
|
||||
user_id bigint(20) unsigned DEFAULT NULL,
|
||||
remote_actor varchar(255) DEFAULT NULL,
|
||||
played_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
play_duration int(11) unsigned DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
KEY track_id (track_id),
|
||||
KEY user_id (user_id),
|
||||
KEY played_at (played_at)
|
||||
) $charset_collate;";
|
||||
|
||||
// Playlist tracks table (many-to-many relationship).
|
||||
$table_playlist_tracks = $wpdb->prefix . 'fedistream_playlist_tracks';
|
||||
$sql_playlist_tracks = "CREATE TABLE $table_playlist_tracks (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
playlist_id bigint(20) unsigned NOT NULL,
|
||||
track_id bigint(20) unsigned NOT NULL,
|
||||
position int(11) unsigned NOT NULL DEFAULT 0,
|
||||
added_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY playlist_track (playlist_id, track_id),
|
||||
KEY playlist_id (playlist_id),
|
||||
KEY track_id (track_id),
|
||||
KEY position (position)
|
||||
) $charset_collate;";
|
||||
|
||||
// ActivityPub followers table.
|
||||
$table_followers = $wpdb->prefix . 'fedistream_followers';
|
||||
$sql_followers = "CREATE TABLE $table_followers (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
artist_id bigint(20) unsigned NOT NULL,
|
||||
follower_uri varchar(2083) NOT NULL,
|
||||
follower_name varchar(255) DEFAULT NULL,
|
||||
follower_icon varchar(2083) DEFAULT NULL,
|
||||
inbox varchar(2083) DEFAULT NULL,
|
||||
shared_inbox varchar(2083) DEFAULT NULL,
|
||||
activity_data longtext DEFAULT NULL,
|
||||
followed_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY artist_follower (artist_id, follower_uri(191)),
|
||||
KEY artist_id (artist_id),
|
||||
KEY followed_at (followed_at)
|
||||
) $charset_collate;";
|
||||
|
||||
// WooCommerce purchases table.
|
||||
$table_purchases = $wpdb->prefix . 'fedistream_purchases';
|
||||
$sql_purchases = "CREATE TABLE $table_purchases (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
user_id bigint(20) unsigned NOT NULL,
|
||||
content_type varchar(50) NOT NULL,
|
||||
content_id bigint(20) unsigned NOT NULL,
|
||||
order_id bigint(20) unsigned NOT NULL,
|
||||
purchased_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY user_content (user_id, content_type, content_id),
|
||||
KEY user_id (user_id),
|
||||
KEY content_type (content_type),
|
||||
KEY content_id (content_id),
|
||||
KEY order_id (order_id)
|
||||
) $charset_collate;";
|
||||
|
||||
// User favorites table.
|
||||
$table_favorites = $wpdb->prefix . 'fedistream_favorites';
|
||||
$sql_favorites = "CREATE TABLE $table_favorites (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
user_id bigint(20) unsigned NOT NULL,
|
||||
content_type varchar(50) NOT NULL,
|
||||
content_id bigint(20) unsigned NOT NULL,
|
||||
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY user_content (user_id, content_type, content_id),
|
||||
KEY user_id (user_id),
|
||||
KEY content_type (content_type),
|
||||
KEY content_id (content_id),
|
||||
KEY created_at (created_at)
|
||||
) $charset_collate;";
|
||||
|
||||
// User follows table (local follows, not ActivityPub).
|
||||
$table_user_follows = $wpdb->prefix . 'fedistream_user_follows';
|
||||
$sql_user_follows = "CREATE TABLE $table_user_follows (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
user_id bigint(20) unsigned NOT NULL,
|
||||
artist_id bigint(20) unsigned NOT NULL,
|
||||
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY user_artist (user_id, artist_id),
|
||||
KEY user_id (user_id),
|
||||
KEY artist_id (artist_id),
|
||||
KEY created_at (created_at)
|
||||
) $charset_collate;";
|
||||
|
||||
// Listening history table.
|
||||
$table_history = $wpdb->prefix . 'fedistream_listening_history';
|
||||
$sql_history = "CREATE TABLE $table_history (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
user_id bigint(20) unsigned NOT NULL,
|
||||
track_id bigint(20) unsigned NOT NULL,
|
||||
played_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY user_id (user_id),
|
||||
KEY track_id (track_id),
|
||||
KEY played_at (played_at),
|
||||
KEY user_played (user_id, played_at)
|
||||
) $charset_collate;";
|
||||
|
||||
// Notifications table.
|
||||
$table_notifications = $wpdb->prefix . 'fedistream_notifications';
|
||||
$sql_notifications = "CREATE TABLE $table_notifications (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
user_id bigint(20) unsigned NOT NULL,
|
||||
type varchar(50) NOT NULL,
|
||||
title varchar(255) NOT NULL,
|
||||
message text NOT NULL,
|
||||
data longtext DEFAULT NULL,
|
||||
is_read tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||||
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
read_at datetime DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY user_id (user_id),
|
||||
KEY type (type),
|
||||
KEY is_read (is_read),
|
||||
KEY created_at (created_at),
|
||||
KEY user_unread (user_id, is_read)
|
||||
) $charset_collate;";
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
|
||||
dbDelta( $sql_plays );
|
||||
dbDelta( $sql_playlist_tracks );
|
||||
dbDelta( $sql_followers );
|
||||
dbDelta( $sql_purchases );
|
||||
dbDelta( $sql_favorites );
|
||||
dbDelta( $sql_user_follows );
|
||||
dbDelta( $sql_history );
|
||||
dbDelta( $sql_notifications );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create required directories.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function create_directories(): void {
|
||||
$directories = array(
|
||||
WP_FEDISTREAM_PATH . 'cache/twig',
|
||||
);
|
||||
|
||||
foreach ( $directories as $directory ) {
|
||||
if ( ! file_exists( $directory ) ) {
|
||||
wp_mkdir_p( $directory );
|
||||
}
|
||||
}
|
||||
|
||||
// Create .htaccess to protect cache directory.
|
||||
$htaccess = WP_FEDISTREAM_PATH . 'cache/.htaccess';
|
||||
if ( ! file_exists( $htaccess ) ) {
|
||||
file_put_contents( $htaccess, 'Deny from all' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default plugin options.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function set_default_options(): void {
|
||||
$defaults = array(
|
||||
'wp_fedistream_enable_activitypub' => 1,
|
||||
'wp_fedistream_enable_woocommerce' => 0,
|
||||
'wp_fedistream_audio_formats' => array( 'mp3', 'wav', 'flac', 'ogg' ),
|
||||
'wp_fedistream_max_upload_size' => 50, // MB
|
||||
'wp_fedistream_default_license' => 'all-rights-reserved',
|
||||
);
|
||||
|
||||
foreach ( $defaults as $option => $value ) {
|
||||
if ( false === get_option( $option ) ) {
|
||||
add_option( $option, $value );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule cron events.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function schedule_events(): void {
|
||||
if ( ! wp_next_scheduled( 'wp_fedistream_daily_cleanup' ) ) {
|
||||
wp_schedule_event( time(), 'daily', 'wp_fedistream_daily_cleanup' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unschedule cron events.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function unschedule_events(): void {
|
||||
$timestamp = wp_next_scheduled( 'wp_fedistream_daily_cleanup' );
|
||||
if ( $timestamp ) {
|
||||
wp_unschedule_event( $timestamp, 'wp_fedistream_daily_cleanup' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete custom database tables.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function delete_tables(): void {
|
||||
global $wpdb;
|
||||
|
||||
$tables = array(
|
||||
$wpdb->prefix . 'fedistream_plays',
|
||||
$wpdb->prefix . 'fedistream_playlist_tracks',
|
||||
$wpdb->prefix . 'fedistream_followers',
|
||||
$wpdb->prefix . 'fedistream_purchases',
|
||||
$wpdb->prefix . 'fedistream_reactions',
|
||||
$wpdb->prefix . 'fedistream_favorites',
|
||||
$wpdb->prefix . 'fedistream_user_follows',
|
||||
$wpdb->prefix . 'fedistream_listening_history',
|
||||
$wpdb->prefix . 'fedistream_notifications',
|
||||
);
|
||||
|
||||
foreach ( $tables as $table ) {
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$wpdb->query( "DROP TABLE IF EXISTS $table" );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete plugin options.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function delete_options(): void {
|
||||
global $wpdb;
|
||||
|
||||
// Delete all options starting with wp_fedistream_.
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM $wpdb->options WHERE option_name LIKE %s",
|
||||
'wp_fedistream_%'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete user meta.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function delete_user_meta(): void {
|
||||
global $wpdb;
|
||||
|
||||
// Delete all user meta starting with wp_fedistream_.
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM $wpdb->usermeta WHERE meta_key LIKE %s",
|
||||
'wp_fedistream_%'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete transients.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function delete_transients(): void {
|
||||
global $wpdb;
|
||||
|
||||
// Delete all transients starting with wp_fedistream_.
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM $wpdb->options WHERE option_name LIKE %s OR option_name LIKE %s",
|
||||
'_transient_wp_fedistream_%',
|
||||
'_transient_timeout_wp_fedistream_%'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
603
includes/Plugin.php
Normal file
603
includes/Plugin.php
Normal file
@@ -0,0 +1,603 @@
|
||||
<?php
|
||||
/**
|
||||
* Main plugin class.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream;
|
||||
|
||||
use WP_FediStream\ActivityPub\Integration as ActivityPubIntegration;
|
||||
use WP_FediStream\ActivityPub\RestApi as ActivityPubRestApi;
|
||||
use WP_FediStream\Admin\ListColumns;
|
||||
use WP_FediStream\Frontend\Ajax;
|
||||
use WP_FediStream\Frontend\Shortcodes;
|
||||
use WP_FediStream\Frontend\TemplateLoader;
|
||||
use WP_FediStream\Frontend\Widgets;
|
||||
use WP_FediStream\PostTypes\Artist;
|
||||
use WP_FediStream\WooCommerce\Integration as WooCommerceIntegration;
|
||||
use WP_FediStream\WooCommerce\DigitalDelivery;
|
||||
use WP_FediStream\WooCommerce\StreamingAccess;
|
||||
use WP_FediStream\PostTypes\Album;
|
||||
use WP_FediStream\PostTypes\Track;
|
||||
use WP_FediStream\PostTypes\Playlist;
|
||||
use WP_FediStream\Taxonomies\Genre;
|
||||
use WP_FediStream\Taxonomies\Mood;
|
||||
use WP_FediStream\Taxonomies\License;
|
||||
use WP_FediStream\User\Library as UserLibrary;
|
||||
use WP_FediStream\User\LibraryPage;
|
||||
use WP_FediStream\User\Notifications;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin singleton class.
|
||||
*
|
||||
* Initializes and manages all plugin components.
|
||||
*/
|
||||
final class Plugin {
|
||||
|
||||
/**
|
||||
* Singleton instance.
|
||||
*
|
||||
* @var Plugin|null
|
||||
*/
|
||||
private static ?Plugin $instance = null;
|
||||
|
||||
/**
|
||||
* Twig environment instance.
|
||||
*
|
||||
* @var \Twig\Environment|null
|
||||
*/
|
||||
private ?\Twig\Environment $twig = null;
|
||||
|
||||
/**
|
||||
* Post type instances.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $post_types = array();
|
||||
|
||||
/**
|
||||
* Taxonomy instances.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $taxonomies = array();
|
||||
|
||||
/**
|
||||
* Get singleton instance.
|
||||
*
|
||||
* @return Plugin
|
||||
*/
|
||||
public static function get_instance(): Plugin {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Private constructor to enforce singleton pattern.
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init_twig();
|
||||
$this->init_components();
|
||||
$this->init_hooks();
|
||||
$this->load_textdomain();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent cloning.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function __clone() {}
|
||||
|
||||
/**
|
||||
* Prevent unserialization.
|
||||
*
|
||||
* @throws \Exception Always throws to prevent unserialization.
|
||||
* @return void
|
||||
*/
|
||||
public function __wakeup(): void {
|
||||
throw new \Exception( 'Cannot unserialize singleton' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Twig template engine.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function init_twig(): void {
|
||||
$loader = new \Twig\Loader\FilesystemLoader( WP_FEDISTREAM_PATH . 'templates' );
|
||||
|
||||
$this->twig = new \Twig\Environment(
|
||||
$loader,
|
||||
array(
|
||||
'cache' => WP_FEDISTREAM_PATH . 'cache/twig',
|
||||
'auto_reload' => WP_DEBUG,
|
||||
)
|
||||
);
|
||||
|
||||
// Add WordPress escaping functions.
|
||||
$this->twig->addFunction( new \Twig\TwigFunction( 'esc_html', 'esc_html' ) );
|
||||
$this->twig->addFunction( new \Twig\TwigFunction( 'esc_attr', 'esc_attr' ) );
|
||||
$this->twig->addFunction( new \Twig\TwigFunction( 'esc_url', 'esc_url' ) );
|
||||
$this->twig->addFunction( new \Twig\TwigFunction( 'esc_js', 'esc_js' ) );
|
||||
$this->twig->addFunction( new \Twig\TwigFunction( 'wp_nonce_field', 'wp_nonce_field', array( 'is_safe' => array( 'html' ) ) ) );
|
||||
$this->twig->addFunction( new \Twig\TwigFunction( '__', '__' ) );
|
||||
$this->twig->addFunction( new \Twig\TwigFunction( '_e', '_e' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize plugin components.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function init_components(): void {
|
||||
// Initialize post types.
|
||||
$this->post_types['artist'] = new Artist();
|
||||
$this->post_types['album'] = new Album();
|
||||
$this->post_types['track'] = new Track();
|
||||
$this->post_types['playlist'] = new Playlist();
|
||||
|
||||
// Initialize taxonomies.
|
||||
$this->taxonomies['genre'] = new Genre();
|
||||
$this->taxonomies['mood'] = new Mood();
|
||||
$this->taxonomies['license'] = new License();
|
||||
|
||||
// Initialize admin components.
|
||||
if ( is_admin() ) {
|
||||
new ListColumns();
|
||||
}
|
||||
|
||||
// Initialize frontend components.
|
||||
if ( ! is_admin() ) {
|
||||
new TemplateLoader();
|
||||
new Shortcodes();
|
||||
}
|
||||
|
||||
// Initialize widgets (always needed for admin widget management).
|
||||
new Widgets();
|
||||
|
||||
// Initialize AJAX handlers.
|
||||
new Ajax();
|
||||
|
||||
// Initialize ActivityPub integration.
|
||||
if ( get_option( 'wp_fedistream_enable_activitypub', 1 ) ) {
|
||||
new ActivityPubIntegration();
|
||||
new ActivityPubRestApi();
|
||||
}
|
||||
|
||||
// Initialize WooCommerce integration.
|
||||
if ( get_option( 'wp_fedistream_enable_woocommerce', 0 ) && $this->is_woocommerce_active() ) {
|
||||
new WooCommerceIntegration();
|
||||
new DigitalDelivery();
|
||||
new StreamingAccess();
|
||||
}
|
||||
|
||||
// Initialize user library and notifications.
|
||||
new UserLibrary();
|
||||
new LibraryPage();
|
||||
new Notifications();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize WordPress hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
add_action( 'init', array( $this, 'maybe_install_defaults' ), 20 );
|
||||
add_action( 'admin_menu', array( $this, 'add_admin_menu' ) );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
|
||||
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_assets' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe install default taxonomy terms.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function maybe_install_defaults(): void {
|
||||
Installer::install_defaults();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load plugin textdomain.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function load_textdomain(): void {
|
||||
load_plugin_textdomain(
|
||||
'wp-fedistream',
|
||||
false,
|
||||
dirname( WP_FEDISTREAM_BASENAME ) . '/languages'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add admin menu.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_admin_menu(): void {
|
||||
// Main menu.
|
||||
add_menu_page(
|
||||
__( 'FediStream', 'wp-fedistream' ),
|
||||
__( 'FediStream', 'wp-fedistream' ),
|
||||
'edit_fedistream_tracks',
|
||||
'fedistream',
|
||||
array( $this, 'render_dashboard_page' ),
|
||||
'dashicons-format-audio',
|
||||
30
|
||||
);
|
||||
|
||||
// Dashboard submenu.
|
||||
add_submenu_page(
|
||||
'fedistream',
|
||||
__( 'Dashboard', 'wp-fedistream' ),
|
||||
__( 'Dashboard', 'wp-fedistream' ),
|
||||
'edit_fedistream_tracks',
|
||||
'fedistream',
|
||||
array( $this, 'render_dashboard_page' )
|
||||
);
|
||||
|
||||
// Artists submenu.
|
||||
add_submenu_page(
|
||||
'fedistream',
|
||||
__( 'Artists', 'wp-fedistream' ),
|
||||
__( 'Artists', 'wp-fedistream' ),
|
||||
'edit_fedistream_artists',
|
||||
'edit.php?post_type=fedistream_artist'
|
||||
);
|
||||
|
||||
// Albums submenu.
|
||||
add_submenu_page(
|
||||
'fedistream',
|
||||
__( 'Albums', 'wp-fedistream' ),
|
||||
__( 'Albums', 'wp-fedistream' ),
|
||||
'edit_fedistream_albums',
|
||||
'edit.php?post_type=fedistream_album'
|
||||
);
|
||||
|
||||
// Tracks submenu.
|
||||
add_submenu_page(
|
||||
'fedistream',
|
||||
__( 'Tracks', 'wp-fedistream' ),
|
||||
__( 'Tracks', 'wp-fedistream' ),
|
||||
'edit_fedistream_tracks',
|
||||
'edit.php?post_type=fedistream_track'
|
||||
);
|
||||
|
||||
// Playlists submenu.
|
||||
add_submenu_page(
|
||||
'fedistream',
|
||||
__( 'Playlists', 'wp-fedistream' ),
|
||||
__( 'Playlists', 'wp-fedistream' ),
|
||||
'edit_fedistream_playlists',
|
||||
'edit.php?post_type=fedistream_playlist'
|
||||
);
|
||||
|
||||
// Genres submenu.
|
||||
add_submenu_page(
|
||||
'fedistream',
|
||||
__( 'Genres', 'wp-fedistream' ),
|
||||
__( 'Genres', 'wp-fedistream' ),
|
||||
'manage_fedistream_genres',
|
||||
'edit-tags.php?taxonomy=fedistream_genre'
|
||||
);
|
||||
|
||||
// Settings submenu.
|
||||
add_submenu_page(
|
||||
'fedistream',
|
||||
__( 'Settings', 'wp-fedistream' ),
|
||||
__( 'Settings', 'wp-fedistream' ),
|
||||
'manage_fedistream_settings',
|
||||
'fedistream-settings',
|
||||
array( $this, 'render_settings_page' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render dashboard page.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render_dashboard_page(): void {
|
||||
// Get stats.
|
||||
$artist_count = wp_count_posts( 'fedistream_artist' )->publish ?? 0;
|
||||
$album_count = wp_count_posts( 'fedistream_album' )->publish ?? 0;
|
||||
$track_count = wp_count_posts( 'fedistream_track' )->publish ?? 0;
|
||||
$playlist_count = wp_count_posts( 'fedistream_playlist' )->publish ?? 0;
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php esc_html_e( 'FediStream Dashboard', 'wp-fedistream' ); ?></h1>
|
||||
|
||||
<div class="fedistream-dashboard">
|
||||
<div class="fedistream-stats" style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin: 20px 0;">
|
||||
<div class="fedistream-stat-box" style="background: #fff; padding: 20px; border: 1px solid #ccd0d4; border-radius: 4px;">
|
||||
<h3 style="margin: 0 0 10px;"><?php esc_html_e( 'Artists', 'wp-fedistream' ); ?></h3>
|
||||
<p style="font-size: 2em; margin: 0; color: #2271b1;"><?php echo esc_html( $artist_count ); ?></p>
|
||||
<a href="<?php echo esc_url( admin_url( 'edit.php?post_type=fedistream_artist' ) ); ?>"><?php esc_html_e( 'Manage Artists', 'wp-fedistream' ); ?></a>
|
||||
</div>
|
||||
|
||||
<div class="fedistream-stat-box" style="background: #fff; padding: 20px; border: 1px solid #ccd0d4; border-radius: 4px;">
|
||||
<h3 style="margin: 0 0 10px;"><?php esc_html_e( 'Albums', 'wp-fedistream' ); ?></h3>
|
||||
<p style="font-size: 2em; margin: 0; color: #2271b1;"><?php echo esc_html( $album_count ); ?></p>
|
||||
<a href="<?php echo esc_url( admin_url( 'edit.php?post_type=fedistream_album' ) ); ?>"><?php esc_html_e( 'Manage Albums', 'wp-fedistream' ); ?></a>
|
||||
</div>
|
||||
|
||||
<div class="fedistream-stat-box" style="background: #fff; padding: 20px; border: 1px solid #ccd0d4; border-radius: 4px;">
|
||||
<h3 style="margin: 0 0 10px;"><?php esc_html_e( 'Tracks', 'wp-fedistream' ); ?></h3>
|
||||
<p style="font-size: 2em; margin: 0; color: #2271b1;"><?php echo esc_html( $track_count ); ?></p>
|
||||
<a href="<?php echo esc_url( admin_url( 'edit.php?post_type=fedistream_track' ) ); ?>"><?php esc_html_e( 'Manage Tracks', 'wp-fedistream' ); ?></a>
|
||||
</div>
|
||||
|
||||
<div class="fedistream-stat-box" style="background: #fff; padding: 20px; border: 1px solid #ccd0d4; border-radius: 4px;">
|
||||
<h3 style="margin: 0 0 10px;"><?php esc_html_e( 'Playlists', 'wp-fedistream' ); ?></h3>
|
||||
<p style="font-size: 2em; margin: 0; color: #2271b1;"><?php echo esc_html( $playlist_count ); ?></p>
|
||||
<a href="<?php echo esc_url( admin_url( 'edit.php?post_type=fedistream_playlist' ) ); ?>"><?php esc_html_e( 'Manage Playlists', 'wp-fedistream' ); ?></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fedistream-quick-actions" style="background: #fff; padding: 20px; border: 1px solid #ccd0d4; border-radius: 4px; margin: 20px 0;">
|
||||
<h2><?php esc_html_e( 'Quick Actions', 'wp-fedistream' ); ?></h2>
|
||||
<p>
|
||||
<a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=fedistream_artist' ) ); ?>" class="button button-primary"><?php esc_html_e( 'Add Artist', 'wp-fedistream' ); ?></a>
|
||||
<a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=fedistream_album' ) ); ?>" class="button"><?php esc_html_e( 'Add Album', 'wp-fedistream' ); ?></a>
|
||||
<a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=fedistream_track' ) ); ?>" class="button"><?php esc_html_e( 'Add Track', 'wp-fedistream' ); ?></a>
|
||||
<a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=fedistream_playlist' ) ); ?>" class="button"><?php esc_html_e( 'Add Playlist', 'wp-fedistream' ); ?></a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="fedistream-info" style="background: #fff; padding: 20px; border: 1px solid #ccd0d4; border-radius: 4px;">
|
||||
<h2><?php esc_html_e( 'Getting Started', 'wp-fedistream' ); ?></h2>
|
||||
<ol>
|
||||
<li><?php esc_html_e( 'Add your artists or bands.', 'wp-fedistream' ); ?></li>
|
||||
<li><?php esc_html_e( 'Create albums and assign them to artists.', 'wp-fedistream' ); ?></li>
|
||||
<li><?php esc_html_e( 'Upload tracks and add them to albums.', 'wp-fedistream' ); ?></li>
|
||||
<li><?php esc_html_e( 'Create playlists to curate your music.', 'wp-fedistream' ); ?></li>
|
||||
<li><?php esc_html_e( 'Share your music via ActivityPub to the Fediverse!', 'wp-fedistream' ); ?></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render settings page.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render_settings_page(): void {
|
||||
// Check user capabilities.
|
||||
if ( ! current_user_can( 'manage_fedistream_settings' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save settings.
|
||||
if ( isset( $_POST['fedistream_settings_nonce'] ) && wp_verify_nonce( sanitize_key( $_POST['fedistream_settings_nonce'] ), 'fedistream_save_settings' ) ) {
|
||||
update_option( 'wp_fedistream_enable_activitypub', isset( $_POST['enable_activitypub'] ) ? 1 : 0 );
|
||||
update_option( 'wp_fedistream_enable_woocommerce', isset( $_POST['enable_woocommerce'] ) ? 1 : 0 );
|
||||
update_option( 'wp_fedistream_max_upload_size', absint( $_POST['max_upload_size'] ?? 50 ) );
|
||||
update_option( 'wp_fedistream_default_license', sanitize_text_field( wp_unslash( $_POST['default_license'] ?? 'all-rights-reserved' ) ) );
|
||||
|
||||
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Settings saved.', 'wp-fedistream' ) . '</p></div>';
|
||||
}
|
||||
|
||||
// Get current settings.
|
||||
$enable_activitypub = get_option( 'wp_fedistream_enable_activitypub', 1 );
|
||||
$enable_woocommerce = get_option( 'wp_fedistream_enable_woocommerce', 0 );
|
||||
$max_upload_size = get_option( 'wp_fedistream_max_upload_size', 50 );
|
||||
$default_license = get_option( 'wp_fedistream_default_license', 'all-rights-reserved' );
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php esc_html_e( 'FediStream Settings', 'wp-fedistream' ); ?></h1>
|
||||
|
||||
<form method="post" action="">
|
||||
<?php wp_nonce_field( 'fedistream_save_settings', 'fedistream_settings_nonce' ); ?>
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'ActivityPub Integration', 'wp-fedistream' ); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="enable_activitypub" value="1" <?php checked( $enable_activitypub, 1 ); ?>>
|
||||
<?php esc_html_e( 'Enable ActivityPub features', 'wp-fedistream' ); ?>
|
||||
</label>
|
||||
<p class="description"><?php esc_html_e( 'Publish releases to the Fediverse and allow followers.', 'wp-fedistream' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'WooCommerce Integration', 'wp-fedistream' ); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="enable_woocommerce" value="1" <?php checked( $enable_woocommerce, 1 ); ?> <?php disabled( ! $this->is_woocommerce_active() ); ?>>
|
||||
<?php esc_html_e( 'Enable WooCommerce features', 'wp-fedistream' ); ?>
|
||||
</label>
|
||||
<?php if ( ! $this->is_woocommerce_active() ) : ?>
|
||||
<p class="description" style="color: #d63638;"><?php esc_html_e( 'WooCommerce is not installed or active.', 'wp-fedistream' ); ?></p>
|
||||
<?php else : ?>
|
||||
<p class="description"><?php esc_html_e( 'Sell albums and tracks through WooCommerce.', 'wp-fedistream' ); ?></p>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="max_upload_size"><?php esc_html_e( 'Max Upload Size', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="max_upload_size" id="max_upload_size" value="<?php echo esc_attr( $max_upload_size ); ?>" min="1" max="500" class="small-text"> MB
|
||||
<p class="description"><?php esc_html_e( 'Maximum file size for audio uploads.', 'wp-fedistream' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="default_license"><?php esc_html_e( 'Default License', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select name="default_license" id="default_license">
|
||||
<option value="all-rights-reserved" <?php selected( $default_license, 'all-rights-reserved' ); ?>><?php esc_html_e( 'All Rights Reserved', 'wp-fedistream' ); ?></option>
|
||||
<option value="cc-by" <?php selected( $default_license, 'cc-by' ); ?>>CC BY</option>
|
||||
<option value="cc-by-sa" <?php selected( $default_license, 'cc-by-sa' ); ?>>CC BY-SA</option>
|
||||
<option value="cc-by-nc" <?php selected( $default_license, 'cc-by-nc' ); ?>>CC BY-NC</option>
|
||||
<option value="cc-by-nc-sa" <?php selected( $default_license, 'cc-by-nc-sa' ); ?>>CC BY-NC-SA</option>
|
||||
<option value="cc0" <?php selected( $default_license, 'cc0' ); ?>>CC0 (Public Domain)</option>
|
||||
</select>
|
||||
<p class="description"><?php esc_html_e( 'Default license for new uploads.', 'wp-fedistream' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<?php submit_button(); ?>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue admin assets.
|
||||
*
|
||||
* @param string $hook_suffix The current admin page.
|
||||
* @return void
|
||||
*/
|
||||
public function enqueue_admin_assets( string $hook_suffix ): void {
|
||||
// Only enqueue on FediStream pages.
|
||||
$screen = get_current_screen();
|
||||
if ( ! $screen ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fedistream_screens = array(
|
||||
'toplevel_page_fedistream',
|
||||
'fedistream_page_fedistream-settings',
|
||||
'fedistream_artist',
|
||||
'fedistream_album',
|
||||
'fedistream_track',
|
||||
'fedistream_playlist',
|
||||
'edit-fedistream_artist',
|
||||
'edit-fedistream_album',
|
||||
'edit-fedistream_track',
|
||||
'edit-fedistream_playlist',
|
||||
'edit-fedistream_genre',
|
||||
'edit-fedistream_mood',
|
||||
'edit-fedistream_license',
|
||||
);
|
||||
|
||||
if ( ! in_array( $screen->id, $fedistream_screens, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_style(
|
||||
'wp-fedistream-admin',
|
||||
WP_FEDISTREAM_URL . 'assets/css/admin.css',
|
||||
array(),
|
||||
WP_FEDISTREAM_VERSION
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'wp-fedistream-admin',
|
||||
WP_FEDISTREAM_URL . 'assets/js/admin.js',
|
||||
array( 'jquery', 'jquery-ui-sortable' ),
|
||||
WP_FEDISTREAM_VERSION,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue frontend assets.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function enqueue_frontend_assets(): void {
|
||||
// Always enqueue as shortcodes/widgets can be used anywhere.
|
||||
// Assets are lightweight and properly cached.
|
||||
wp_enqueue_style(
|
||||
'wp-fedistream',
|
||||
WP_FEDISTREAM_URL . 'assets/css/frontend.css',
|
||||
array(),
|
||||
WP_FEDISTREAM_VERSION
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'wp-fedistream',
|
||||
WP_FEDISTREAM_URL . 'assets/js/frontend.js',
|
||||
array(),
|
||||
WP_FEDISTREAM_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
wp_localize_script(
|
||||
'wp-fedistream',
|
||||
'wpFediStream',
|
||||
array(
|
||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||
'nonce' => wp_create_nonce( 'wp-fedistream-nonce' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Twig environment.
|
||||
*
|
||||
* @return \Twig\Environment
|
||||
*/
|
||||
public function get_twig(): \Twig\Environment {
|
||||
return $this->twig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a Twig template.
|
||||
*
|
||||
* @param string $template Template name (without .twig extension).
|
||||
* @param array $context Template context variables.
|
||||
* @return string Rendered template.
|
||||
*/
|
||||
public function render( string $template, array $context = array() ): string {
|
||||
return $this->twig->render( $template . '.twig', $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a post type instance.
|
||||
*
|
||||
* @param string $name Post type name.
|
||||
* @return object|null Post type instance or null.
|
||||
*/
|
||||
public function get_post_type( string $name ): ?object {
|
||||
return $this->post_types[ $name ] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a taxonomy instance.
|
||||
*
|
||||
* @param string $name Taxonomy name.
|
||||
* @return object|null Taxonomy instance or null.
|
||||
*/
|
||||
public function get_taxonomy( string $name ): ?object {
|
||||
return $this->taxonomies[ $name ] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if WooCommerce is active.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_woocommerce_active(): bool {
|
||||
return class_exists( 'WooCommerce' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if ActivityPub plugin is active.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_activitypub_active(): bool {
|
||||
return class_exists( 'Activitypub\Activitypub' );
|
||||
}
|
||||
}
|
||||
202
includes/PostTypes/AbstractPostType.php
Normal file
202
includes/PostTypes/AbstractPostType.php
Normal file
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
/**
|
||||
* Abstract base class for custom post types.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\PostTypes;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract post type class.
|
||||
*
|
||||
* Provides common functionality for all custom post types.
|
||||
*/
|
||||
abstract class AbstractPostType {
|
||||
|
||||
/**
|
||||
* Post type key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $post_type;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'init', array( $this, 'register' ) );
|
||||
add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ) );
|
||||
add_action( 'save_post_' . $this->post_type, array( $this, 'save_meta' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the post type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function register(): void;
|
||||
|
||||
/**
|
||||
* Add meta boxes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function add_meta_boxes(): void;
|
||||
|
||||
/**
|
||||
* Save post meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
abstract public function save_meta( int $post_id, \WP_Post $post ): void;
|
||||
|
||||
/**
|
||||
* Get the post type key.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_post_type(): string {
|
||||
return $this->post_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify nonce and user capabilities before saving.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $nonce_action Nonce action name.
|
||||
* @param string $nonce_name Nonce field name.
|
||||
* @return bool Whether save should proceed.
|
||||
*/
|
||||
protected function can_save( int $post_id, string $nonce_action, string $nonce_name ): bool {
|
||||
// Verify nonce.
|
||||
if ( ! isset( $_POST[ $nonce_name ] ) || ! wp_verify_nonce( sanitize_key( $_POST[ $nonce_name ] ), $nonce_action ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check autosave.
|
||||
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check permissions.
|
||||
if ( ! current_user_can( 'edit_post', $post_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and save a text meta field.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param string $post_key POST array key.
|
||||
* @return void
|
||||
*/
|
||||
protected function save_text_meta( int $post_id, string $meta_key, string $post_key ): void {
|
||||
if ( isset( $_POST[ $post_key ] ) ) {
|
||||
update_post_meta( $post_id, $meta_key, sanitize_text_field( wp_unslash( $_POST[ $post_key ] ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and save a textarea meta field.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param string $post_key POST array key.
|
||||
* @return void
|
||||
*/
|
||||
protected function save_textarea_meta( int $post_id, string $meta_key, string $post_key ): void {
|
||||
if ( isset( $_POST[ $post_key ] ) ) {
|
||||
update_post_meta( $post_id, $meta_key, sanitize_textarea_field( wp_unslash( $_POST[ $post_key ] ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and save an integer meta field.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param string $post_key POST array key.
|
||||
* @return void
|
||||
*/
|
||||
protected function save_int_meta( int $post_id, string $meta_key, string $post_key ): void {
|
||||
if ( isset( $_POST[ $post_key ] ) ) {
|
||||
update_post_meta( $post_id, $meta_key, absint( $_POST[ $post_key ] ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and save a URL meta field.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param string $post_key POST array key.
|
||||
* @return void
|
||||
*/
|
||||
protected function save_url_meta( int $post_id, string $meta_key, string $post_key ): void {
|
||||
if ( isset( $_POST[ $post_key ] ) ) {
|
||||
update_post_meta( $post_id, $meta_key, esc_url_raw( wp_unslash( $_POST[ $post_key ] ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and save a boolean meta field.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param string $post_key POST array key.
|
||||
* @return void
|
||||
*/
|
||||
protected function save_bool_meta( int $post_id, string $meta_key, string $post_key ): void {
|
||||
$value = isset( $_POST[ $post_key ] ) ? 1 : 0;
|
||||
update_post_meta( $post_id, $meta_key, $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and save an array meta field.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param string $post_key POST array key.
|
||||
* @return void
|
||||
*/
|
||||
protected function save_array_meta( int $post_id, string $meta_key, string $post_key ): void {
|
||||
if ( isset( $_POST[ $post_key ] ) && is_array( $_POST[ $post_key ] ) ) {
|
||||
$values = array_map( 'sanitize_text_field', wp_unslash( $_POST[ $post_key ] ) );
|
||||
update_post_meta( $post_id, $meta_key, $values );
|
||||
} else {
|
||||
delete_post_meta( $post_id, $meta_key );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and save a date meta field.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param string $post_key POST array key.
|
||||
* @return void
|
||||
*/
|
||||
protected function save_date_meta( int $post_id, string $meta_key, string $post_key ): void {
|
||||
if ( isset( $_POST[ $post_key ] ) && ! empty( $_POST[ $post_key ] ) ) {
|
||||
$date = sanitize_text_field( wp_unslash( $_POST[ $post_key ] ) );
|
||||
// Validate date format (YYYY-MM-DD).
|
||||
if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) {
|
||||
update_post_meta( $post_id, $meta_key, $date );
|
||||
}
|
||||
} else {
|
||||
delete_post_meta( $post_id, $meta_key );
|
||||
}
|
||||
}
|
||||
}
|
||||
340
includes/PostTypes/Album.php
Normal file
340
includes/PostTypes/Album.php
Normal file
@@ -0,0 +1,340 @@
|
||||
<?php
|
||||
/**
|
||||
* Album custom post type.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\PostTypes;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Album post type class.
|
||||
*
|
||||
* Handles registration and management of albums/releases.
|
||||
*/
|
||||
class Album extends AbstractPostType {
|
||||
|
||||
/**
|
||||
* Post type key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $post_type = 'fedistream_album';
|
||||
|
||||
/**
|
||||
* Meta key prefix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private const META_PREFIX = '_fedistream_album_';
|
||||
|
||||
/**
|
||||
* Register the post type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void {
|
||||
$labels = array(
|
||||
'name' => _x( 'Albums', 'Post type general name', 'wp-fedistream' ),
|
||||
'singular_name' => _x( 'Album', 'Post type singular name', 'wp-fedistream' ),
|
||||
'menu_name' => _x( 'Albums', 'Admin Menu text', 'wp-fedistream' ),
|
||||
'name_admin_bar' => _x( 'Album', 'Add New on Toolbar', 'wp-fedistream' ),
|
||||
'add_new' => __( 'Add New', 'wp-fedistream' ),
|
||||
'add_new_item' => __( 'Add New Album', 'wp-fedistream' ),
|
||||
'new_item' => __( 'New Album', 'wp-fedistream' ),
|
||||
'edit_item' => __( 'Edit Album', 'wp-fedistream' ),
|
||||
'view_item' => __( 'View Album', 'wp-fedistream' ),
|
||||
'all_items' => __( 'All Albums', 'wp-fedistream' ),
|
||||
'search_items' => __( 'Search Albums', 'wp-fedistream' ),
|
||||
'parent_item_colon' => __( 'Parent Albums:', 'wp-fedistream' ),
|
||||
'not_found' => __( 'No albums found.', 'wp-fedistream' ),
|
||||
'not_found_in_trash' => __( 'No albums found in Trash.', 'wp-fedistream' ),
|
||||
'featured_image' => _x( 'Album Artwork', 'Overrides the "Featured Image" phrase', 'wp-fedistream' ),
|
||||
'set_featured_image' => _x( 'Set album artwork', 'Overrides the "Set featured image" phrase', 'wp-fedistream' ),
|
||||
'remove_featured_image' => _x( 'Remove album artwork', 'Overrides the "Remove featured image" phrase', 'wp-fedistream' ),
|
||||
'use_featured_image' => _x( 'Use as album artwork', 'Overrides the "Use as featured image" phrase', 'wp-fedistream' ),
|
||||
'archives' => _x( 'Album archives', 'The post type archive label', 'wp-fedistream' ),
|
||||
'insert_into_item' => _x( 'Insert into album', 'Overrides the "Insert into post" phrase', 'wp-fedistream' ),
|
||||
'uploaded_to_this_item' => _x( 'Uploaded to this album', 'Overrides the "Uploaded to this post" phrase', 'wp-fedistream' ),
|
||||
'filter_items_list' => _x( 'Filter albums list', 'Screen reader text', 'wp-fedistream' ),
|
||||
'items_list_navigation' => _x( 'Albums list navigation', 'Screen reader text', 'wp-fedistream' ),
|
||||
'items_list' => _x( 'Albums list', 'Screen reader text', 'wp-fedistream' ),
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'labels' => $labels,
|
||||
'public' => true,
|
||||
'publicly_queryable' => true,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => false, // Will be added to custom menu.
|
||||
'query_var' => true,
|
||||
'rewrite' => array( 'slug' => 'albums' ),
|
||||
'capability_type' => array( 'fedistream_album', 'fedistream_albums' ),
|
||||
'map_meta_cap' => true,
|
||||
'has_archive' => true,
|
||||
'hierarchical' => false,
|
||||
'menu_position' => null,
|
||||
'menu_icon' => 'dashicons-album',
|
||||
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt', 'revisions' ),
|
||||
'show_in_rest' => true,
|
||||
'rest_base' => 'albums',
|
||||
);
|
||||
|
||||
register_post_type( $this->post_type, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add meta boxes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_meta_boxes(): void {
|
||||
add_meta_box(
|
||||
'fedistream_album_info',
|
||||
__( 'Album Information', 'wp-fedistream' ),
|
||||
array( $this, 'render_info_meta_box' ),
|
||||
$this->post_type,
|
||||
'normal',
|
||||
'high'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_album_artist',
|
||||
__( 'Artist', 'wp-fedistream' ),
|
||||
array( $this, 'render_artist_meta_box' ),
|
||||
$this->post_type,
|
||||
'side',
|
||||
'high'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_album_codes',
|
||||
__( 'Album Codes', 'wp-fedistream' ),
|
||||
array( $this, 'render_codes_meta_box' ),
|
||||
$this->post_type,
|
||||
'side',
|
||||
'default'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render album info meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_info_meta_box( \WP_Post $post ): void {
|
||||
wp_nonce_field( 'fedistream_album_save', 'fedistream_album_nonce' );
|
||||
|
||||
$album_type = get_post_meta( $post->ID, self::META_PREFIX . 'type', true );
|
||||
$release_date = get_post_meta( $post->ID, self::META_PREFIX . 'release_date', true );
|
||||
$total_tracks = get_post_meta( $post->ID, self::META_PREFIX . 'total_tracks', true );
|
||||
$total_duration = get_post_meta( $post->ID, self::META_PREFIX . 'total_duration', true );
|
||||
?>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_album_type"><?php esc_html_e( 'Release Type', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select name="fedistream_album_type" id="fedistream_album_type">
|
||||
<option value="album" <?php selected( $album_type, 'album' ); ?>><?php esc_html_e( 'Album', 'wp-fedistream' ); ?></option>
|
||||
<option value="ep" <?php selected( $album_type, 'ep' ); ?>><?php esc_html_e( 'EP', 'wp-fedistream' ); ?></option>
|
||||
<option value="single" <?php selected( $album_type, 'single' ); ?>><?php esc_html_e( 'Single', 'wp-fedistream' ); ?></option>
|
||||
<option value="compilation" <?php selected( $album_type, 'compilation' ); ?>><?php esc_html_e( 'Compilation', 'wp-fedistream' ); ?></option>
|
||||
<option value="live" <?php selected( $album_type, 'live' ); ?>><?php esc_html_e( 'Live Album', 'wp-fedistream' ); ?></option>
|
||||
<option value="remix" <?php selected( $album_type, 'remix' ); ?>><?php esc_html_e( 'Remix Album', 'wp-fedistream' ); ?></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_album_release_date"><?php esc_html_e( 'Release Date', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="date" name="fedistream_album_release_date" id="fedistream_album_release_date" value="<?php echo esc_attr( $release_date ); ?>" class="regular-text">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_album_total_tracks"><?php esc_html_e( 'Total Tracks', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="fedistream_album_total_tracks" id="fedistream_album_total_tracks" value="<?php echo esc_attr( $total_tracks ); ?>" min="1" max="999" class="small-text">
|
||||
<p class="description"><?php esc_html_e( 'Auto-calculated when tracks are added.', 'wp-fedistream' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_album_total_duration"><?php esc_html_e( 'Total Duration', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="fedistream_album_total_duration" id="fedistream_album_total_duration" value="<?php echo esc_attr( $total_duration ); ?>" min="0" class="small-text" readonly>
|
||||
<span class="description"><?php esc_html_e( 'seconds (auto-calculated)', 'wp-fedistream' ); ?></span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render artist selection meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_artist_meta_box( \WP_Post $post ): void {
|
||||
$selected_artist = get_post_meta( $post->ID, self::META_PREFIX . 'artist', true );
|
||||
|
||||
$artists = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_artist',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
?>
|
||||
<p>
|
||||
<label for="fedistream_album_artist"><?php esc_html_e( 'Primary Artist', 'wp-fedistream' ); ?></label>
|
||||
</p>
|
||||
<select name="fedistream_album_artist" id="fedistream_album_artist" class="widefat">
|
||||
<option value=""><?php esc_html_e( '— Select Artist —', 'wp-fedistream' ); ?></option>
|
||||
<?php foreach ( $artists as $artist ) : ?>
|
||||
<option value="<?php echo esc_attr( $artist->ID ); ?>" <?php selected( $selected_artist, $artist->ID ); ?>>
|
||||
<?php echo esc_html( $artist->post_title ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p class="description">
|
||||
<?php
|
||||
if ( empty( $artists ) ) {
|
||||
printf(
|
||||
/* translators: %s: URL to add new artist */
|
||||
esc_html__( 'No artists found. %s', 'wp-fedistream' ),
|
||||
'<a href="' . esc_url( admin_url( 'post-new.php?post_type=fedistream_artist' ) ) . '">' . esc_html__( 'Add an artist first.', 'wp-fedistream' ) . '</a>'
|
||||
);
|
||||
}
|
||||
?>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render album codes meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_codes_meta_box( \WP_Post $post ): void {
|
||||
$upc = get_post_meta( $post->ID, self::META_PREFIX . 'upc', true );
|
||||
$catalog_number = get_post_meta( $post->ID, self::META_PREFIX . 'catalog_number', true );
|
||||
?>
|
||||
<p>
|
||||
<label for="fedistream_album_upc"><?php esc_html_e( 'UPC/EAN', 'wp-fedistream' ); ?></label>
|
||||
<input type="text" name="fedistream_album_upc" id="fedistream_album_upc" value="<?php echo esc_attr( $upc ); ?>" class="widefat" pattern="[0-9]{12,13}" title="<?php esc_attr_e( '12-13 digit UPC/EAN code', 'wp-fedistream' ); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="fedistream_album_catalog_number"><?php esc_html_e( 'Catalog Number', 'wp-fedistream' ); ?></label>
|
||||
<input type="text" name="fedistream_album_catalog_number" id="fedistream_album_catalog_number" value="<?php echo esc_attr( $catalog_number ); ?>" class="widefat">
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save post meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function save_meta( int $post_id, \WP_Post $post ): void {
|
||||
if ( ! $this->can_save( $post_id, 'fedistream_album_save', 'fedistream_album_nonce' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save album type.
|
||||
if ( isset( $_POST['fedistream_album_type'] ) ) {
|
||||
$allowed_types = array( 'album', 'ep', 'single', 'compilation', 'live', 'remix' );
|
||||
$type = sanitize_text_field( wp_unslash( $_POST['fedistream_album_type'] ) );
|
||||
if ( in_array( $type, $allowed_types, true ) ) {
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'type', $type );
|
||||
}
|
||||
}
|
||||
|
||||
// Save other fields.
|
||||
$this->save_date_meta( $post_id, self::META_PREFIX . 'release_date', 'fedistream_album_release_date' );
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'artist', 'fedistream_album_artist' );
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'total_tracks', 'fedistream_album_total_tracks' );
|
||||
$this->save_text_meta( $post_id, self::META_PREFIX . 'upc', 'fedistream_album_upc' );
|
||||
$this->save_text_meta( $post_id, self::META_PREFIX . 'catalog_number', 'fedistream_album_catalog_number' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get album by ID with meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return array|null Album data or null.
|
||||
*/
|
||||
public static function get_album( int $post_id ): ?array {
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post || 'fedistream_album' !== $post->post_type ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$artist_id = get_post_meta( $post_id, self::META_PREFIX . 'artist', true );
|
||||
|
||||
return array(
|
||||
'id' => $post->ID,
|
||||
'title' => $post->post_title,
|
||||
'slug' => $post->post_name,
|
||||
'description' => $post->post_content,
|
||||
'excerpt' => $post->post_excerpt,
|
||||
'type' => get_post_meta( $post_id, self::META_PREFIX . 'type', true ) ?: 'album',
|
||||
'release_date' => get_post_meta( $post_id, self::META_PREFIX . 'release_date', true ),
|
||||
'artist_id' => $artist_id,
|
||||
'artist_name' => $artist_id ? get_the_title( $artist_id ) : '',
|
||||
'total_tracks' => (int) get_post_meta( $post_id, self::META_PREFIX . 'total_tracks', true ),
|
||||
'total_duration' => (int) get_post_meta( $post_id, self::META_PREFIX . 'total_duration', true ),
|
||||
'upc' => get_post_meta( $post_id, self::META_PREFIX . 'upc', true ),
|
||||
'catalog_number' => get_post_meta( $post_id, self::META_PREFIX . 'catalog_number', true ),
|
||||
'artwork' => get_the_post_thumbnail_url( $post_id, 'large' ),
|
||||
'url' => get_permalink( $post_id ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update album track count and duration.
|
||||
*
|
||||
* @param int $album_id Album post ID.
|
||||
* @return void
|
||||
*/
|
||||
public static function update_album_stats( int $album_id ): void {
|
||||
$tracks = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_track',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
'meta_key' => '_fedistream_track_album',
|
||||
'meta_value' => $album_id,
|
||||
)
|
||||
);
|
||||
|
||||
$total_tracks = count( $tracks );
|
||||
$total_duration = 0;
|
||||
|
||||
foreach ( $tracks as $track ) {
|
||||
$duration = (int) get_post_meta( $track->ID, '_fedistream_track_duration', true );
|
||||
$total_duration += $duration;
|
||||
}
|
||||
|
||||
update_post_meta( $album_id, self::META_PREFIX . 'total_tracks', $total_tracks );
|
||||
update_post_meta( $album_id, self::META_PREFIX . 'total_duration', $total_duration );
|
||||
}
|
||||
}
|
||||
331
includes/PostTypes/Artist.php
Normal file
331
includes/PostTypes/Artist.php
Normal file
@@ -0,0 +1,331 @@
|
||||
<?php
|
||||
/**
|
||||
* Artist custom post type.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\PostTypes;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Artist post type class.
|
||||
*
|
||||
* Handles registration and management of artist/band profiles.
|
||||
*/
|
||||
class Artist extends AbstractPostType {
|
||||
|
||||
/**
|
||||
* Post type key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $post_type = 'fedistream_artist';
|
||||
|
||||
/**
|
||||
* Meta key prefix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private const META_PREFIX = '_fedistream_artist_';
|
||||
|
||||
/**
|
||||
* Register the post type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void {
|
||||
$labels = array(
|
||||
'name' => _x( 'Artists', 'Post type general name', 'wp-fedistream' ),
|
||||
'singular_name' => _x( 'Artist', 'Post type singular name', 'wp-fedistream' ),
|
||||
'menu_name' => _x( 'Artists', 'Admin Menu text', 'wp-fedistream' ),
|
||||
'name_admin_bar' => _x( 'Artist', 'Add New on Toolbar', 'wp-fedistream' ),
|
||||
'add_new' => __( 'Add New', 'wp-fedistream' ),
|
||||
'add_new_item' => __( 'Add New Artist', 'wp-fedistream' ),
|
||||
'new_item' => __( 'New Artist', 'wp-fedistream' ),
|
||||
'edit_item' => __( 'Edit Artist', 'wp-fedistream' ),
|
||||
'view_item' => __( 'View Artist', 'wp-fedistream' ),
|
||||
'all_items' => __( 'All Artists', 'wp-fedistream' ),
|
||||
'search_items' => __( 'Search Artists', 'wp-fedistream' ),
|
||||
'parent_item_colon' => __( 'Parent Artists:', 'wp-fedistream' ),
|
||||
'not_found' => __( 'No artists found.', 'wp-fedistream' ),
|
||||
'not_found_in_trash' => __( 'No artists found in Trash.', 'wp-fedistream' ),
|
||||
'featured_image' => _x( 'Artist Photo', 'Overrides the "Featured Image" phrase', 'wp-fedistream' ),
|
||||
'set_featured_image' => _x( 'Set artist photo', 'Overrides the "Set featured image" phrase', 'wp-fedistream' ),
|
||||
'remove_featured_image' => _x( 'Remove artist photo', 'Overrides the "Remove featured image" phrase', 'wp-fedistream' ),
|
||||
'use_featured_image' => _x( 'Use as artist photo', 'Overrides the "Use as featured image" phrase', 'wp-fedistream' ),
|
||||
'archives' => _x( 'Artist archives', 'The post type archive label', 'wp-fedistream' ),
|
||||
'insert_into_item' => _x( 'Insert into artist', 'Overrides the "Insert into post" phrase', 'wp-fedistream' ),
|
||||
'uploaded_to_this_item' => _x( 'Uploaded to this artist', 'Overrides the "Uploaded to this post" phrase', 'wp-fedistream' ),
|
||||
'filter_items_list' => _x( 'Filter artists list', 'Screen reader text', 'wp-fedistream' ),
|
||||
'items_list_navigation' => _x( 'Artists list navigation', 'Screen reader text', 'wp-fedistream' ),
|
||||
'items_list' => _x( 'Artists list', 'Screen reader text', 'wp-fedistream' ),
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'labels' => $labels,
|
||||
'public' => true,
|
||||
'publicly_queryable' => true,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => false, // Will be added to custom menu.
|
||||
'query_var' => true,
|
||||
'rewrite' => array( 'slug' => 'artists' ),
|
||||
'capability_type' => array( 'fedistream_artist', 'fedistream_artists' ),
|
||||
'map_meta_cap' => true,
|
||||
'has_archive' => true,
|
||||
'hierarchical' => false,
|
||||
'menu_position' => null,
|
||||
'menu_icon' => 'dashicons-groups',
|
||||
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt', 'revisions' ),
|
||||
'show_in_rest' => true,
|
||||
'rest_base' => 'artists',
|
||||
);
|
||||
|
||||
register_post_type( $this->post_type, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add meta boxes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_meta_boxes(): void {
|
||||
add_meta_box(
|
||||
'fedistream_artist_info',
|
||||
__( 'Artist Information', 'wp-fedistream' ),
|
||||
array( $this, 'render_info_meta_box' ),
|
||||
$this->post_type,
|
||||
'normal',
|
||||
'high'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_artist_social',
|
||||
__( 'Social Links', 'wp-fedistream' ),
|
||||
array( $this, 'render_social_meta_box' ),
|
||||
$this->post_type,
|
||||
'normal',
|
||||
'default'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_artist_members',
|
||||
__( 'Band Members', 'wp-fedistream' ),
|
||||
array( $this, 'render_members_meta_box' ),
|
||||
$this->post_type,
|
||||
'side',
|
||||
'default'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render artist info meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_info_meta_box( \WP_Post $post ): void {
|
||||
wp_nonce_field( 'fedistream_artist_save', 'fedistream_artist_nonce' );
|
||||
|
||||
$artist_type = get_post_meta( $post->ID, self::META_PREFIX . 'type', true );
|
||||
$formed_date = get_post_meta( $post->ID, self::META_PREFIX . 'formed_date', true );
|
||||
$location = get_post_meta( $post->ID, self::META_PREFIX . 'location', true );
|
||||
$website = get_post_meta( $post->ID, self::META_PREFIX . 'website', true );
|
||||
?>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_artist_type"><?php esc_html_e( 'Artist Type', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select name="fedistream_artist_type" id="fedistream_artist_type">
|
||||
<option value="solo" <?php selected( $artist_type, 'solo' ); ?>><?php esc_html_e( 'Solo Artist', 'wp-fedistream' ); ?></option>
|
||||
<option value="band" <?php selected( $artist_type, 'band' ); ?>><?php esc_html_e( 'Band', 'wp-fedistream' ); ?></option>
|
||||
<option value="duo" <?php selected( $artist_type, 'duo' ); ?>><?php esc_html_e( 'Duo', 'wp-fedistream' ); ?></option>
|
||||
<option value="collective" <?php selected( $artist_type, 'collective' ); ?>><?php esc_html_e( 'Collective', 'wp-fedistream' ); ?></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_artist_formed_date"><?php esc_html_e( 'Formed Date', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="date" name="fedistream_artist_formed_date" id="fedistream_artist_formed_date" value="<?php echo esc_attr( $formed_date ); ?>" class="regular-text">
|
||||
<p class="description"><?php esc_html_e( 'When the artist/band was formed or started their career.', 'wp-fedistream' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_artist_location"><?php esc_html_e( 'Location', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" name="fedistream_artist_location" id="fedistream_artist_location" value="<?php echo esc_attr( $location ); ?>" class="regular-text">
|
||||
<p class="description"><?php esc_html_e( 'City, Country or region where the artist is based.', 'wp-fedistream' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_artist_website"><?php esc_html_e( 'Website', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="url" name="fedistream_artist_website" id="fedistream_artist_website" value="<?php echo esc_url( $website ); ?>" class="regular-text">
|
||||
<p class="description"><?php esc_html_e( 'Official website URL.', 'wp-fedistream' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render social links meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_social_meta_box( \WP_Post $post ): void {
|
||||
$social_links = get_post_meta( $post->ID, self::META_PREFIX . 'social_links', true );
|
||||
if ( ! is_array( $social_links ) ) {
|
||||
$social_links = array();
|
||||
}
|
||||
|
||||
$platforms = array(
|
||||
'mastodon' => __( 'Mastodon', 'wp-fedistream' ),
|
||||
'bandcamp' => __( 'Bandcamp', 'wp-fedistream' ),
|
||||
'soundcloud' => __( 'SoundCloud', 'wp-fedistream' ),
|
||||
'youtube' => __( 'YouTube', 'wp-fedistream' ),
|
||||
'instagram' => __( 'Instagram', 'wp-fedistream' ),
|
||||
'twitter' => __( 'Twitter/X', 'wp-fedistream' ),
|
||||
'facebook' => __( 'Facebook', 'wp-fedistream' ),
|
||||
'tiktok' => __( 'TikTok', 'wp-fedistream' ),
|
||||
'other' => __( 'Other', 'wp-fedistream' ),
|
||||
);
|
||||
?>
|
||||
<table class="form-table">
|
||||
<?php foreach ( $platforms as $key => $label ) : ?>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_social_<?php echo esc_attr( $key ); ?>"><?php echo esc_html( $label ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="url" name="fedistream_artist_social[<?php echo esc_attr( $key ); ?>]" id="fedistream_social_<?php echo esc_attr( $key ); ?>" value="<?php echo esc_url( $social_links[ $key ] ?? '' ); ?>" class="regular-text">
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render band members meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_members_meta_box( \WP_Post $post ): void {
|
||||
$members = get_post_meta( $post->ID, self::META_PREFIX . 'members', true );
|
||||
if ( ! is_array( $members ) ) {
|
||||
$members = array();
|
||||
}
|
||||
|
||||
$artist_type = get_post_meta( $post->ID, self::META_PREFIX . 'type', true );
|
||||
?>
|
||||
<div id="fedistream-members-wrapper" style="<?php echo 'solo' === $artist_type ? 'display:none;' : ''; ?>">
|
||||
<p class="description"><?php esc_html_e( 'Add band/group members (comma-separated names).', 'wp-fedistream' ); ?></p>
|
||||
<textarea name="fedistream_artist_members" id="fedistream_artist_members" rows="5" class="large-text"><?php echo esc_textarea( implode( "\n", $members ) ); ?></textarea>
|
||||
<p class="description"><?php esc_html_e( 'One member per line.', 'wp-fedistream' ); ?></p>
|
||||
</div>
|
||||
<script>
|
||||
jQuery(document).ready(function($) {
|
||||
$('#fedistream_artist_type').on('change', function() {
|
||||
if ($(this).val() === 'solo') {
|
||||
$('#fedistream-members-wrapper').hide();
|
||||
} else {
|
||||
$('#fedistream-members-wrapper').show();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save post meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function save_meta( int $post_id, \WP_Post $post ): void {
|
||||
if ( ! $this->can_save( $post_id, 'fedistream_artist_save', 'fedistream_artist_nonce' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save artist type.
|
||||
if ( isset( $_POST['fedistream_artist_type'] ) ) {
|
||||
$allowed_types = array( 'solo', 'band', 'duo', 'collective' );
|
||||
$type = sanitize_text_field( wp_unslash( $_POST['fedistream_artist_type'] ) );
|
||||
if ( in_array( $type, $allowed_types, true ) ) {
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'type', $type );
|
||||
}
|
||||
}
|
||||
|
||||
// Save other fields.
|
||||
$this->save_date_meta( $post_id, self::META_PREFIX . 'formed_date', 'fedistream_artist_formed_date' );
|
||||
$this->save_text_meta( $post_id, self::META_PREFIX . 'location', 'fedistream_artist_location' );
|
||||
$this->save_url_meta( $post_id, self::META_PREFIX . 'website', 'fedistream_artist_website' );
|
||||
|
||||
// Save social links.
|
||||
if ( isset( $_POST['fedistream_artist_social'] ) && is_array( $_POST['fedistream_artist_social'] ) ) {
|
||||
$social_links = array();
|
||||
foreach ( $_POST['fedistream_artist_social'] as $key => $url ) {
|
||||
$clean_key = sanitize_key( $key );
|
||||
$clean_url = esc_url_raw( wp_unslash( $url ) );
|
||||
if ( ! empty( $clean_url ) ) {
|
||||
$social_links[ $clean_key ] = $clean_url;
|
||||
}
|
||||
}
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'social_links', $social_links );
|
||||
}
|
||||
|
||||
// Save members.
|
||||
if ( isset( $_POST['fedistream_artist_members'] ) ) {
|
||||
$members_text = sanitize_textarea_field( wp_unslash( $_POST['fedistream_artist_members'] ) );
|
||||
$members = array_filter( array_map( 'trim', explode( "\n", $members_text ) ) );
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'members', $members );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get artist by ID with meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return array|null Artist data or null.
|
||||
*/
|
||||
public static function get_artist( int $post_id ): ?array {
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post || 'fedistream_artist' !== $post->post_type ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'id' => $post->ID,
|
||||
'name' => $post->post_title,
|
||||
'slug' => $post->post_name,
|
||||
'bio' => $post->post_content,
|
||||
'excerpt' => $post->post_excerpt,
|
||||
'type' => get_post_meta( $post_id, self::META_PREFIX . 'type', true ) ?: 'solo',
|
||||
'formed_date' => get_post_meta( $post_id, self::META_PREFIX . 'formed_date', true ),
|
||||
'location' => get_post_meta( $post_id, self::META_PREFIX . 'location', true ),
|
||||
'website' => get_post_meta( $post_id, self::META_PREFIX . 'website', true ),
|
||||
'social_links' => get_post_meta( $post_id, self::META_PREFIX . 'social_links', true ) ?: array(),
|
||||
'members' => get_post_meta( $post_id, self::META_PREFIX . 'members', true ) ?: array(),
|
||||
'photo' => get_the_post_thumbnail_url( $post_id, 'large' ),
|
||||
'url' => get_permalink( $post_id ),
|
||||
);
|
||||
}
|
||||
}
|
||||
458
includes/PostTypes/Playlist.php
Normal file
458
includes/PostTypes/Playlist.php
Normal file
@@ -0,0 +1,458 @@
|
||||
<?php
|
||||
/**
|
||||
* Playlist custom post type.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\PostTypes;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Playlist post type class.
|
||||
*
|
||||
* Handles registration and management of playlists.
|
||||
*/
|
||||
class Playlist extends AbstractPostType {
|
||||
|
||||
/**
|
||||
* Post type key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $post_type = 'fedistream_playlist';
|
||||
|
||||
/**
|
||||
* Meta key prefix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private const META_PREFIX = '_fedistream_playlist_';
|
||||
|
||||
/**
|
||||
* Register the post type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void {
|
||||
$labels = array(
|
||||
'name' => _x( 'Playlists', 'Post type general name', 'wp-fedistream' ),
|
||||
'singular_name' => _x( 'Playlist', 'Post type singular name', 'wp-fedistream' ),
|
||||
'menu_name' => _x( 'Playlists', 'Admin Menu text', 'wp-fedistream' ),
|
||||
'name_admin_bar' => _x( 'Playlist', 'Add New on Toolbar', 'wp-fedistream' ),
|
||||
'add_new' => __( 'Add New', 'wp-fedistream' ),
|
||||
'add_new_item' => __( 'Add New Playlist', 'wp-fedistream' ),
|
||||
'new_item' => __( 'New Playlist', 'wp-fedistream' ),
|
||||
'edit_item' => __( 'Edit Playlist', 'wp-fedistream' ),
|
||||
'view_item' => __( 'View Playlist', 'wp-fedistream' ),
|
||||
'all_items' => __( 'All Playlists', 'wp-fedistream' ),
|
||||
'search_items' => __( 'Search Playlists', 'wp-fedistream' ),
|
||||
'parent_item_colon' => __( 'Parent Playlists:', 'wp-fedistream' ),
|
||||
'not_found' => __( 'No playlists found.', 'wp-fedistream' ),
|
||||
'not_found_in_trash' => __( 'No playlists found in Trash.', 'wp-fedistream' ),
|
||||
'featured_image' => _x( 'Playlist Cover', 'Overrides the "Featured Image" phrase', 'wp-fedistream' ),
|
||||
'set_featured_image' => _x( 'Set playlist cover', 'Overrides the "Set featured image" phrase', 'wp-fedistream' ),
|
||||
'remove_featured_image' => _x( 'Remove playlist cover', 'Overrides the "Remove featured image" phrase', 'wp-fedistream' ),
|
||||
'use_featured_image' => _x( 'Use as playlist cover', 'Overrides the "Use as featured image" phrase', 'wp-fedistream' ),
|
||||
'archives' => _x( 'Playlist archives', 'The post type archive label', 'wp-fedistream' ),
|
||||
'insert_into_item' => _x( 'Insert into playlist', 'Overrides the "Insert into post" phrase', 'wp-fedistream' ),
|
||||
'uploaded_to_this_item' => _x( 'Uploaded to this playlist', 'Overrides the "Uploaded to this post" phrase', 'wp-fedistream' ),
|
||||
'filter_items_list' => _x( 'Filter playlists list', 'Screen reader text', 'wp-fedistream' ),
|
||||
'items_list_navigation' => _x( 'Playlists list navigation', 'Screen reader text', 'wp-fedistream' ),
|
||||
'items_list' => _x( 'Playlists list', 'Screen reader text', 'wp-fedistream' ),
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'labels' => $labels,
|
||||
'public' => true,
|
||||
'publicly_queryable' => true,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => false, // Will be added to custom menu.
|
||||
'query_var' => true,
|
||||
'rewrite' => array( 'slug' => 'playlists' ),
|
||||
'capability_type' => array( 'fedistream_playlist', 'fedistream_playlists' ),
|
||||
'map_meta_cap' => true,
|
||||
'has_archive' => true,
|
||||
'hierarchical' => false,
|
||||
'menu_position' => null,
|
||||
'menu_icon' => 'dashicons-playlist-audio',
|
||||
'supports' => array( 'title', 'editor', 'thumbnail', 'author', 'revisions' ),
|
||||
'show_in_rest' => true,
|
||||
'rest_base' => 'playlists',
|
||||
);
|
||||
|
||||
register_post_type( $this->post_type, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add meta boxes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_meta_boxes(): void {
|
||||
add_meta_box(
|
||||
'fedistream_playlist_tracks',
|
||||
__( 'Playlist Tracks', 'wp-fedistream' ),
|
||||
array( $this, 'render_tracks_meta_box' ),
|
||||
$this->post_type,
|
||||
'normal',
|
||||
'high'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_playlist_settings',
|
||||
__( 'Playlist Settings', 'wp-fedistream' ),
|
||||
array( $this, 'render_settings_meta_box' ),
|
||||
$this->post_type,
|
||||
'side',
|
||||
'default'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_playlist_stats',
|
||||
__( 'Playlist Stats', 'wp-fedistream' ),
|
||||
array( $this, 'render_stats_meta_box' ),
|
||||
$this->post_type,
|
||||
'side',
|
||||
'default'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render playlist tracks meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_tracks_meta_box( \WP_Post $post ): void {
|
||||
wp_nonce_field( 'fedistream_playlist_save', 'fedistream_playlist_nonce' );
|
||||
|
||||
global $wpdb;
|
||||
|
||||
// Get tracks in this playlist from the pivot table.
|
||||
$table = $wpdb->prefix . 'fedistream_playlist_tracks';
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$playlist_tracks = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT track_id, position FROM $table WHERE playlist_id = %d ORDER BY position ASC",
|
||||
$post->ID
|
||||
)
|
||||
);
|
||||
|
||||
$track_ids = wp_list_pluck( $playlist_tracks, 'track_id' );
|
||||
|
||||
// Get all available tracks.
|
||||
$available_tracks = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_track',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
?>
|
||||
<div class="fedistream-playlist-tracks">
|
||||
<h4><?php esc_html_e( 'Current Tracks', 'wp-fedistream' ); ?></h4>
|
||||
<ul id="fedistream-playlist-track-list" class="fedistream-sortable">
|
||||
<?php
|
||||
foreach ( $track_ids as $track_id ) :
|
||||
$track = get_post( $track_id );
|
||||
if ( ! $track ) {
|
||||
continue;
|
||||
}
|
||||
$artists = get_post_meta( $track_id, '_fedistream_track_artists', true ) ?: array();
|
||||
$duration = get_post_meta( $track_id, '_fedistream_track_duration', true );
|
||||
?>
|
||||
<li data-track-id="<?php echo esc_attr( $track_id ); ?>" style="padding: 8px; margin: 4px 0; background: #f9f9f9; border: 1px solid #ddd; cursor: move;">
|
||||
<input type="hidden" name="fedistream_playlist_tracks[]" value="<?php echo esc_attr( $track_id ); ?>">
|
||||
<span class="dashicons dashicons-menu" style="vertical-align: middle;"></span>
|
||||
<strong><?php echo esc_html( $track->post_title ); ?></strong>
|
||||
<?php if ( ! empty( $artists ) ) : ?>
|
||||
<span class="description">
|
||||
— <?php echo esc_html( implode( ', ', array_map( 'get_the_title', $artists ) ) ); ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<?php if ( $duration ) : ?>
|
||||
<span class="description">(<?php echo esc_html( gmdate( 'i:s', (int) $duration ) ); ?>)</span>
|
||||
<?php endif; ?>
|
||||
<button type="button" class="button-link fedistream-remove-track" style="color: #a00; float: right;">
|
||||
<?php esc_html_e( 'Remove', 'wp-fedistream' ); ?>
|
||||
</button>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
|
||||
<hr>
|
||||
|
||||
<h4><?php esc_html_e( 'Add Tracks', 'wp-fedistream' ); ?></h4>
|
||||
<p>
|
||||
<select id="fedistream-add-track-select" class="widefat">
|
||||
<option value=""><?php esc_html_e( '— Select a track to add —', 'wp-fedistream' ); ?></option>
|
||||
<?php foreach ( $available_tracks as $track ) : ?>
|
||||
<?php
|
||||
$artists = get_post_meta( $track->ID, '_fedistream_track_artists', true ) ?: array();
|
||||
$duration = get_post_meta( $track->ID, '_fedistream_track_duration', true );
|
||||
?>
|
||||
<option value="<?php echo esc_attr( $track->ID ); ?>" data-title="<?php echo esc_attr( $track->post_title ); ?>" data-artists="<?php echo esc_attr( implode( ', ', array_map( 'get_the_title', $artists ) ) ); ?>" data-duration="<?php echo esc_attr( $duration ? gmdate( 'i:s', (int) $duration ) : '' ); ?>">
|
||||
<?php echo esc_html( $track->post_title ); ?>
|
||||
<?php if ( ! empty( $artists ) ) : ?>
|
||||
— <?php echo esc_html( implode( ', ', array_map( 'get_the_title', $artists ) ) ); ?>
|
||||
<?php endif; ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
<button type="button" class="button" id="fedistream-add-track-btn"><?php esc_html_e( 'Add to Playlist', 'wp-fedistream' ); ?></button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.fedistream-sortable li { list-style: none; }
|
||||
.fedistream-sortable .ui-sortable-helper { background: #fff !important; box-shadow: 0 2px 4px rgba(0,0,0,0.2); }
|
||||
.fedistream-sortable .ui-sortable-placeholder { visibility: visible !important; background: #e0e0e0; border: 1px dashed #999; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
jQuery(document).ready(function($) {
|
||||
// Make list sortable.
|
||||
$('#fedistream-playlist-track-list').sortable({
|
||||
placeholder: 'ui-sortable-placeholder',
|
||||
axis: 'y'
|
||||
});
|
||||
|
||||
// Add track.
|
||||
$('#fedistream-add-track-btn').on('click', function() {
|
||||
var $select = $('#fedistream-add-track-select');
|
||||
var trackId = $select.val();
|
||||
if (!trackId) return;
|
||||
|
||||
var $option = $select.find('option:selected');
|
||||
var title = $option.data('title');
|
||||
var artists = $option.data('artists');
|
||||
var duration = $option.data('duration');
|
||||
|
||||
var html = '<li data-track-id="' + trackId + '" style="padding: 8px; margin: 4px 0; background: #f9f9f9; border: 1px solid #ddd; cursor: move;">' +
|
||||
'<input type="hidden" name="fedistream_playlist_tracks[]" value="' + trackId + '">' +
|
||||
'<span class="dashicons dashicons-menu" style="vertical-align: middle;"></span> ' +
|
||||
'<strong>' + title + '</strong>';
|
||||
|
||||
if (artists) {
|
||||
html += ' <span class="description">— ' + artists + '</span>';
|
||||
}
|
||||
if (duration) {
|
||||
html += ' <span class="description">(' + duration + ')</span>';
|
||||
}
|
||||
|
||||
html += '<button type="button" class="button-link fedistream-remove-track" style="color: #a00; float: right;"><?php echo esc_js( __( 'Remove', 'wp-fedistream' ) ); ?></button>' +
|
||||
'</li>';
|
||||
|
||||
$('#fedistream-playlist-track-list').append(html);
|
||||
$select.val('');
|
||||
});
|
||||
|
||||
// Remove track.
|
||||
$(document).on('click', '.fedistream-remove-track', function() {
|
||||
$(this).closest('li').remove();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render playlist settings meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_settings_meta_box( \WP_Post $post ): void {
|
||||
$visibility = get_post_meta( $post->ID, self::META_PREFIX . 'visibility', true ) ?: 'public';
|
||||
$collaborative = get_post_meta( $post->ID, self::META_PREFIX . 'collaborative', true );
|
||||
$federated = get_post_meta( $post->ID, self::META_PREFIX . 'federated', true );
|
||||
?>
|
||||
<p>
|
||||
<label for="fedistream_playlist_visibility"><?php esc_html_e( 'Visibility', 'wp-fedistream' ); ?></label>
|
||||
<select name="fedistream_playlist_visibility" id="fedistream_playlist_visibility" class="widefat">
|
||||
<option value="public" <?php selected( $visibility, 'public' ); ?>><?php esc_html_e( 'Public', 'wp-fedistream' ); ?></option>
|
||||
<option value="unlisted" <?php selected( $visibility, 'unlisted' ); ?>><?php esc_html_e( 'Unlisted (link only)', 'wp-fedistream' ); ?></option>
|
||||
<option value="private" <?php selected( $visibility, 'private' ); ?>><?php esc_html_e( 'Private', 'wp-fedistream' ); ?></option>
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" name="fedistream_playlist_collaborative" value="1" <?php checked( $collaborative, 1 ); ?>>
|
||||
<?php esc_html_e( 'Collaborative', 'wp-fedistream' ); ?>
|
||||
</label>
|
||||
<br>
|
||||
<span class="description"><?php esc_html_e( 'Allow others to add tracks.', 'wp-fedistream' ); ?></span>
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" name="fedistream_playlist_federated" value="1" <?php checked( $federated, 1 ); ?>>
|
||||
<?php esc_html_e( 'Federated', 'wp-fedistream' ); ?>
|
||||
</label>
|
||||
<br>
|
||||
<span class="description"><?php esc_html_e( 'Allow tracks from other FediStream instances.', 'wp-fedistream' ); ?></span>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render playlist stats meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_stats_meta_box( \WP_Post $post ): void {
|
||||
$track_count = get_post_meta( $post->ID, self::META_PREFIX . 'track_count', true ) ?: 0;
|
||||
$total_duration = get_post_meta( $post->ID, self::META_PREFIX . 'total_duration', true ) ?: 0;
|
||||
?>
|
||||
<p>
|
||||
<strong><?php esc_html_e( 'Tracks:', 'wp-fedistream' ); ?></strong>
|
||||
<?php echo esc_html( $track_count ); ?>
|
||||
</p>
|
||||
<p>
|
||||
<strong><?php esc_html_e( 'Total Duration:', 'wp-fedistream' ); ?></strong>
|
||||
<?php
|
||||
if ( $total_duration > 3600 ) {
|
||||
echo esc_html( gmdate( 'H:i:s', (int) $total_duration ) );
|
||||
} else {
|
||||
echo esc_html( gmdate( 'i:s', (int) $total_duration ) );
|
||||
}
|
||||
?>
|
||||
</p>
|
||||
<p class="description"><?php esc_html_e( 'Stats are automatically updated when saved.', 'wp-fedistream' ); ?></p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save post meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function save_meta( int $post_id, \WP_Post $post ): void {
|
||||
if ( ! $this->can_save( $post_id, 'fedistream_playlist_save', 'fedistream_playlist_nonce' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'fedistream_playlist_tracks';
|
||||
|
||||
// Save visibility.
|
||||
if ( isset( $_POST['fedistream_playlist_visibility'] ) ) {
|
||||
$allowed_visibility = array( 'public', 'unlisted', 'private' );
|
||||
$visibility = sanitize_text_field( wp_unslash( $_POST['fedistream_playlist_visibility'] ) );
|
||||
if ( in_array( $visibility, $allowed_visibility, true ) ) {
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'visibility', $visibility );
|
||||
}
|
||||
}
|
||||
|
||||
// Save settings.
|
||||
$this->save_bool_meta( $post_id, self::META_PREFIX . 'collaborative', 'fedistream_playlist_collaborative' );
|
||||
$this->save_bool_meta( $post_id, self::META_PREFIX . 'federated', 'fedistream_playlist_federated' );
|
||||
|
||||
// Save tracks.
|
||||
// First, delete existing tracks for this playlist.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$wpdb->delete( $table, array( 'playlist_id' => $post_id ), array( '%d' ) );
|
||||
|
||||
// Insert new tracks.
|
||||
if ( isset( $_POST['fedistream_playlist_tracks'] ) && is_array( $_POST['fedistream_playlist_tracks'] ) ) {
|
||||
$position = 0;
|
||||
$total_duration = 0;
|
||||
|
||||
foreach ( $_POST['fedistream_playlist_tracks'] as $track_id ) {
|
||||
$track_id = absint( $track_id );
|
||||
if ( $track_id > 0 ) {
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$wpdb->insert(
|
||||
$table,
|
||||
array(
|
||||
'playlist_id' => $post_id,
|
||||
'track_id' => $track_id,
|
||||
'position' => $position,
|
||||
),
|
||||
array( '%d', '%d', '%d' )
|
||||
);
|
||||
++$position;
|
||||
|
||||
// Sum duration.
|
||||
$duration = (int) get_post_meta( $track_id, '_fedistream_track_duration', true );
|
||||
$total_duration += $duration;
|
||||
}
|
||||
}
|
||||
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'track_count', $position );
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'total_duration', $total_duration );
|
||||
} else {
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'track_count', 0 );
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'total_duration', 0 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get playlist by ID with meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return array|null Playlist data or null.
|
||||
*/
|
||||
public static function get_playlist( int $post_id ): ?array {
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post || 'fedistream_playlist' !== $post->post_type ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'id' => $post->ID,
|
||||
'title' => $post->post_title,
|
||||
'slug' => $post->post_name,
|
||||
'description' => $post->post_content,
|
||||
'author_id' => $post->post_author,
|
||||
'author_name' => get_the_author_meta( 'display_name', $post->post_author ),
|
||||
'visibility' => get_post_meta( $post_id, self::META_PREFIX . 'visibility', true ) ?: 'public',
|
||||
'collaborative' => (bool) get_post_meta( $post_id, self::META_PREFIX . 'collaborative', true ),
|
||||
'federated' => (bool) get_post_meta( $post_id, self::META_PREFIX . 'federated', true ),
|
||||
'track_count' => (int) get_post_meta( $post_id, self::META_PREFIX . 'track_count', true ),
|
||||
'total_duration' => (int) get_post_meta( $post_id, self::META_PREFIX . 'total_duration', true ),
|
||||
'cover' => get_the_post_thumbnail_url( $post_id, 'large' ),
|
||||
'url' => get_permalink( $post_id ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tracks in a playlist.
|
||||
*
|
||||
* @param int $playlist_id Playlist post ID.
|
||||
* @return array Array of track data.
|
||||
*/
|
||||
public static function get_playlist_tracks( int $playlist_id ): array {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_playlist_tracks';
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$results = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT track_id FROM $table WHERE playlist_id = %d ORDER BY position ASC",
|
||||
$playlist_id
|
||||
)
|
||||
);
|
||||
|
||||
$tracks = array();
|
||||
foreach ( $results as $row ) {
|
||||
$track = Track::get_track( (int) $row->track_id );
|
||||
if ( $track ) {
|
||||
$tracks[] = $track;
|
||||
}
|
||||
}
|
||||
|
||||
return $tracks;
|
||||
}
|
||||
}
|
||||
615
includes/PostTypes/Track.php
Normal file
615
includes/PostTypes/Track.php
Normal file
@@ -0,0 +1,615 @@
|
||||
<?php
|
||||
/**
|
||||
* Track custom post type.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\PostTypes;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track post type class.
|
||||
*
|
||||
* Handles registration and management of individual tracks.
|
||||
*/
|
||||
class Track extends AbstractPostType {
|
||||
|
||||
/**
|
||||
* Post type key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $post_type = 'fedistream_track';
|
||||
|
||||
/**
|
||||
* Meta key prefix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private const META_PREFIX = '_fedistream_track_';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
add_action( 'save_post_fedistream_track', array( $this, 'update_album_on_save' ), 20, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the post type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void {
|
||||
$labels = array(
|
||||
'name' => _x( 'Tracks', 'Post type general name', 'wp-fedistream' ),
|
||||
'singular_name' => _x( 'Track', 'Post type singular name', 'wp-fedistream' ),
|
||||
'menu_name' => _x( 'Tracks', 'Admin Menu text', 'wp-fedistream' ),
|
||||
'name_admin_bar' => _x( 'Track', 'Add New on Toolbar', 'wp-fedistream' ),
|
||||
'add_new' => __( 'Add New', 'wp-fedistream' ),
|
||||
'add_new_item' => __( 'Add New Track', 'wp-fedistream' ),
|
||||
'new_item' => __( 'New Track', 'wp-fedistream' ),
|
||||
'edit_item' => __( 'Edit Track', 'wp-fedistream' ),
|
||||
'view_item' => __( 'View Track', 'wp-fedistream' ),
|
||||
'all_items' => __( 'All Tracks', 'wp-fedistream' ),
|
||||
'search_items' => __( 'Search Tracks', 'wp-fedistream' ),
|
||||
'parent_item_colon' => __( 'Parent Tracks:', 'wp-fedistream' ),
|
||||
'not_found' => __( 'No tracks found.', 'wp-fedistream' ),
|
||||
'not_found_in_trash' => __( 'No tracks found in Trash.', 'wp-fedistream' ),
|
||||
'featured_image' => _x( 'Track Artwork', 'Overrides the "Featured Image" phrase', 'wp-fedistream' ),
|
||||
'set_featured_image' => _x( 'Set track artwork', 'Overrides the "Set featured image" phrase', 'wp-fedistream' ),
|
||||
'remove_featured_image' => _x( 'Remove track artwork', 'Overrides the "Remove featured image" phrase', 'wp-fedistream' ),
|
||||
'use_featured_image' => _x( 'Use as track artwork', 'Overrides the "Use as featured image" phrase', 'wp-fedistream' ),
|
||||
'archives' => _x( 'Track archives', 'The post type archive label', 'wp-fedistream' ),
|
||||
'insert_into_item' => _x( 'Insert into track', 'Overrides the "Insert into post" phrase', 'wp-fedistream' ),
|
||||
'uploaded_to_this_item' => _x( 'Uploaded to this track', 'Overrides the "Uploaded to this post" phrase', 'wp-fedistream' ),
|
||||
'filter_items_list' => _x( 'Filter tracks list', 'Screen reader text', 'wp-fedistream' ),
|
||||
'items_list_navigation' => _x( 'Tracks list navigation', 'Screen reader text', 'wp-fedistream' ),
|
||||
'items_list' => _x( 'Tracks list', 'Screen reader text', 'wp-fedistream' ),
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'labels' => $labels,
|
||||
'public' => true,
|
||||
'publicly_queryable' => true,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => false, // Will be added to custom menu.
|
||||
'query_var' => true,
|
||||
'rewrite' => array( 'slug' => 'tracks' ),
|
||||
'capability_type' => array( 'fedistream_track', 'fedistream_tracks' ),
|
||||
'map_meta_cap' => true,
|
||||
'has_archive' => true,
|
||||
'hierarchical' => false,
|
||||
'menu_position' => null,
|
||||
'menu_icon' => 'dashicons-format-audio',
|
||||
'supports' => array( 'title', 'editor', 'thumbnail', 'revisions' ),
|
||||
'show_in_rest' => true,
|
||||
'rest_base' => 'tracks',
|
||||
);
|
||||
|
||||
register_post_type( $this->post_type, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add meta boxes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_meta_boxes(): void {
|
||||
add_meta_box(
|
||||
'fedistream_track_audio',
|
||||
__( 'Audio File', 'wp-fedistream' ),
|
||||
array( $this, 'render_audio_meta_box' ),
|
||||
$this->post_type,
|
||||
'normal',
|
||||
'high'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_track_info',
|
||||
__( 'Track Information', 'wp-fedistream' ),
|
||||
array( $this, 'render_info_meta_box' ),
|
||||
$this->post_type,
|
||||
'normal',
|
||||
'high'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_track_album',
|
||||
__( 'Album', 'wp-fedistream' ),
|
||||
array( $this, 'render_album_meta_box' ),
|
||||
$this->post_type,
|
||||
'side',
|
||||
'high'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_track_artists',
|
||||
__( 'Artists', 'wp-fedistream' ),
|
||||
array( $this, 'render_artists_meta_box' ),
|
||||
$this->post_type,
|
||||
'side',
|
||||
'default'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_track_codes',
|
||||
__( 'Track Codes', 'wp-fedistream' ),
|
||||
array( $this, 'render_codes_meta_box' ),
|
||||
$this->post_type,
|
||||
'side',
|
||||
'default'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render audio file meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_audio_meta_box( \WP_Post $post ): void {
|
||||
wp_nonce_field( 'fedistream_track_save', 'fedistream_track_nonce' );
|
||||
|
||||
$audio_file = get_post_meta( $post->ID, self::META_PREFIX . 'audio_file', true );
|
||||
$audio_format = get_post_meta( $post->ID, self::META_PREFIX . 'audio_format', true );
|
||||
$duration = get_post_meta( $post->ID, self::META_PREFIX . 'duration', true );
|
||||
|
||||
wp_enqueue_media();
|
||||
?>
|
||||
<div class="fedistream-audio-upload">
|
||||
<p>
|
||||
<label for="fedistream_track_audio_file"><?php esc_html_e( 'Audio File', 'wp-fedistream' ); ?></label>
|
||||
</p>
|
||||
<input type="hidden" name="fedistream_track_audio_file" id="fedistream_track_audio_file" value="<?php echo esc_attr( $audio_file ); ?>">
|
||||
<div id="fedistream-audio-preview">
|
||||
<?php if ( $audio_file ) : ?>
|
||||
<?php
|
||||
$audio_url = wp_get_attachment_url( $audio_file );
|
||||
if ( $audio_url ) :
|
||||
?>
|
||||
<audio controls style="width: 100%;">
|
||||
<source src="<?php echo esc_url( $audio_url ); ?>" type="audio/<?php echo esc_attr( $audio_format ?: 'mpeg' ); ?>">
|
||||
</audio>
|
||||
<p><strong><?php echo esc_html( basename( get_attached_file( $audio_file ) ) ); ?></strong></p>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<p>
|
||||
<button type="button" class="button" id="fedistream-upload-audio"><?php esc_html_e( 'Select Audio File', 'wp-fedistream' ); ?></button>
|
||||
<button type="button" class="button" id="fedistream-remove-audio" style="<?php echo $audio_file ? '' : 'display:none;'; ?>"><?php esc_html_e( 'Remove', 'wp-fedistream' ); ?></button>
|
||||
</p>
|
||||
<p class="description"><?php esc_html_e( 'Supported formats: MP3, WAV, FLAC, OGG', 'wp-fedistream' ); ?></p>
|
||||
</div>
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_track_audio_format"><?php esc_html_e( 'Audio Format', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select name="fedistream_track_audio_format" id="fedistream_track_audio_format">
|
||||
<option value=""><?php esc_html_e( '— Auto-detect —', 'wp-fedistream' ); ?></option>
|
||||
<option value="mp3" <?php selected( $audio_format, 'mp3' ); ?>>MP3</option>
|
||||
<option value="wav" <?php selected( $audio_format, 'wav' ); ?>>WAV</option>
|
||||
<option value="flac" <?php selected( $audio_format, 'flac' ); ?>>FLAC</option>
|
||||
<option value="ogg" <?php selected( $audio_format, 'ogg' ); ?>>OGG</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_track_duration"><?php esc_html_e( 'Duration (seconds)', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="fedistream_track_duration" id="fedistream_track_duration" value="<?php echo esc_attr( $duration ); ?>" min="0" class="small-text">
|
||||
<span id="fedistream-duration-display">
|
||||
<?php
|
||||
if ( $duration ) {
|
||||
printf(
|
||||
/* translators: %s: formatted duration */
|
||||
esc_html__( '(%s)', 'wp-fedistream' ),
|
||||
esc_html( gmdate( 'i:s', (int) $duration ) )
|
||||
);
|
||||
}
|
||||
?>
|
||||
</span>
|
||||
<p class="description"><?php esc_html_e( 'Auto-detected from audio file if available.', 'wp-fedistream' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<script>
|
||||
jQuery(document).ready(function($) {
|
||||
var mediaUploader;
|
||||
|
||||
$('#fedistream-upload-audio').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (mediaUploader) {
|
||||
mediaUploader.open();
|
||||
return;
|
||||
}
|
||||
|
||||
mediaUploader = wp.media({
|
||||
title: '<?php echo esc_js( __( 'Select Audio File', 'wp-fedistream' ) ); ?>',
|
||||
button: { text: '<?php echo esc_js( __( 'Use this file', 'wp-fedistream' ) ); ?>' },
|
||||
library: { type: 'audio' },
|
||||
multiple: false
|
||||
});
|
||||
|
||||
mediaUploader.on('select', function() {
|
||||
var attachment = mediaUploader.state().get('selection').first().toJSON();
|
||||
$('#fedistream_track_audio_file').val(attachment.id);
|
||||
$('#fedistream-audio-preview').html(
|
||||
'<audio controls style="width: 100%;"><source src="' + attachment.url + '" type="' + attachment.mime + '"></audio>' +
|
||||
'<p><strong>' + attachment.filename + '</strong></p>'
|
||||
);
|
||||
$('#fedistream-remove-audio').show();
|
||||
|
||||
// Auto-detect format.
|
||||
var ext = attachment.filename.split('.').pop().toLowerCase();
|
||||
if (['mp3', 'wav', 'flac', 'ogg'].indexOf(ext) !== -1) {
|
||||
$('#fedistream_track_audio_format').val(ext);
|
||||
}
|
||||
});
|
||||
|
||||
mediaUploader.open();
|
||||
});
|
||||
|
||||
$('#fedistream-remove-audio').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
$('#fedistream_track_audio_file').val('');
|
||||
$('#fedistream-audio-preview').html('');
|
||||
$(this).hide();
|
||||
});
|
||||
|
||||
// Duration display.
|
||||
$('#fedistream_track_duration').on('change', function() {
|
||||
var seconds = parseInt($(this).val(), 10);
|
||||
if (seconds > 0) {
|
||||
var mins = Math.floor(seconds / 60);
|
||||
var secs = seconds % 60;
|
||||
$('#fedistream-duration-display').text('(' + mins + ':' + (secs < 10 ? '0' : '') + secs + ')');
|
||||
} else {
|
||||
$('#fedistream-duration-display').text('');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render track info meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_info_meta_box( \WP_Post $post ): void {
|
||||
$track_number = get_post_meta( $post->ID, self::META_PREFIX . 'number', true );
|
||||
$disc_number = get_post_meta( $post->ID, self::META_PREFIX . 'disc_number', true );
|
||||
$bpm = get_post_meta( $post->ID, self::META_PREFIX . 'bpm', true );
|
||||
$key = get_post_meta( $post->ID, self::META_PREFIX . 'key', true );
|
||||
$explicit = get_post_meta( $post->ID, self::META_PREFIX . 'explicit', true );
|
||||
$preview_start = get_post_meta( $post->ID, self::META_PREFIX . 'preview_start', true );
|
||||
$preview_duration = get_post_meta( $post->ID, self::META_PREFIX . 'preview_duration', true );
|
||||
|
||||
$musical_keys = array(
|
||||
'' => __( '— Select Key —', 'wp-fedistream' ),
|
||||
'C' => 'C Major',
|
||||
'Cm' => 'C Minor',
|
||||
'C#' => 'C# Major',
|
||||
'C#m' => 'C# Minor',
|
||||
'D' => 'D Major',
|
||||
'Dm' => 'D Minor',
|
||||
'D#' => 'D# Major',
|
||||
'D#m' => 'D# Minor',
|
||||
'E' => 'E Major',
|
||||
'Em' => 'E Minor',
|
||||
'F' => 'F Major',
|
||||
'Fm' => 'F Minor',
|
||||
'F#' => 'F# Major',
|
||||
'F#m' => 'F# Minor',
|
||||
'G' => 'G Major',
|
||||
'Gm' => 'G Minor',
|
||||
'G#' => 'G# Major',
|
||||
'G#m' => 'G# Minor',
|
||||
'A' => 'A Major',
|
||||
'Am' => 'A Minor',
|
||||
'A#' => 'A# Major',
|
||||
'A#m' => 'A# Minor',
|
||||
'B' => 'B Major',
|
||||
'Bm' => 'B Minor',
|
||||
);
|
||||
?>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_track_number"><?php esc_html_e( 'Track Number', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="fedistream_track_number" id="fedistream_track_number" value="<?php echo esc_attr( $track_number ); ?>" min="1" max="999" class="small-text">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_track_disc_number"><?php esc_html_e( 'Disc Number', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="fedistream_track_disc_number" id="fedistream_track_disc_number" value="<?php echo esc_attr( $disc_number ?: 1 ); ?>" min="1" max="99" class="small-text">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_track_bpm"><?php esc_html_e( 'BPM', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="fedistream_track_bpm" id="fedistream_track_bpm" value="<?php echo esc_attr( $bpm ); ?>" min="20" max="300" class="small-text">
|
||||
<p class="description"><?php esc_html_e( 'Beats per minute (tempo).', 'wp-fedistream' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_track_key"><?php esc_html_e( 'Musical Key', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select name="fedistream_track_key" id="fedistream_track_key">
|
||||
<?php foreach ( $musical_keys as $value => $label ) : ?>
|
||||
<option value="<?php echo esc_attr( $value ); ?>" <?php selected( $key, $value ); ?>><?php echo esc_html( $label ); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<?php esc_html_e( 'Explicit Content', 'wp-fedistream' ); ?>
|
||||
</th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="fedistream_track_explicit" value="1" <?php checked( $explicit, 1 ); ?>>
|
||||
<?php esc_html_e( 'This track contains explicit content', 'wp-fedistream' ); ?>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_track_preview_start"><?php esc_html_e( 'Preview Settings', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<label>
|
||||
<?php esc_html_e( 'Start at:', 'wp-fedistream' ); ?>
|
||||
<input type="number" name="fedistream_track_preview_start" id="fedistream_track_preview_start" value="<?php echo esc_attr( $preview_start ?: 0 ); ?>" min="0" class="small-text">
|
||||
<?php esc_html_e( 'seconds', 'wp-fedistream' ); ?>
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
<?php esc_html_e( 'Duration:', 'wp-fedistream' ); ?>
|
||||
<input type="number" name="fedistream_track_preview_duration" id="fedistream_track_preview_duration" value="<?php echo esc_attr( $preview_duration ?: 30 ); ?>" min="10" max="60" class="small-text">
|
||||
<?php esc_html_e( 'seconds', 'wp-fedistream' ); ?>
|
||||
</label>
|
||||
<p class="description"><?php esc_html_e( 'Preview clip for non-authenticated users or before purchase.', 'wp-fedistream' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render album selection meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_album_meta_box( \WP_Post $post ): void {
|
||||
$selected_album = get_post_meta( $post->ID, self::META_PREFIX . 'album', true );
|
||||
|
||||
$albums = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_album',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => array( 'publish', 'draft' ),
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
?>
|
||||
<p>
|
||||
<select name="fedistream_track_album" id="fedistream_track_album" class="widefat">
|
||||
<option value=""><?php esc_html_e( '— No Album (Single) —', 'wp-fedistream' ); ?></option>
|
||||
<?php foreach ( $albums as $album ) : ?>
|
||||
<?php
|
||||
$artist_id = get_post_meta( $album->ID, '_fedistream_album_artist', true );
|
||||
$artist_name = $artist_id ? get_the_title( $artist_id ) : '';
|
||||
?>
|
||||
<option value="<?php echo esc_attr( $album->ID ); ?>" <?php selected( $selected_album, $album->ID ); ?>>
|
||||
<?php echo esc_html( $album->post_title ); ?>
|
||||
<?php if ( $artist_name ) : ?>
|
||||
(<?php echo esc_html( $artist_name ); ?>)
|
||||
<?php endif; ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render artists meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_artists_meta_box( \WP_Post $post ): void {
|
||||
$selected_artists = get_post_meta( $post->ID, self::META_PREFIX . 'artists', true );
|
||||
if ( ! is_array( $selected_artists ) ) {
|
||||
$selected_artists = array();
|
||||
}
|
||||
|
||||
$artists = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_artist',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
?>
|
||||
<p class="description"><?php esc_html_e( 'Select all artists featured on this track.', 'wp-fedistream' ); ?></p>
|
||||
<div style="max-height: 200px; overflow-y: auto; border: 1px solid #ddd; padding: 5px;">
|
||||
<?php foreach ( $artists as $artist ) : ?>
|
||||
<label style="display: block; padding: 2px 0;">
|
||||
<input type="checkbox" name="fedistream_track_artists[]" value="<?php echo esc_attr( $artist->ID ); ?>" <?php checked( in_array( $artist->ID, $selected_artists, true ) ); ?>>
|
||||
<?php echo esc_html( $artist->post_title ); ?>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php if ( empty( $artists ) ) : ?>
|
||||
<p>
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: URL to add new artist */
|
||||
esc_html__( 'No artists found. %s', 'wp-fedistream' ),
|
||||
'<a href="' . esc_url( admin_url( 'post-new.php?post_type=fedistream_artist' ) ) . '">' . esc_html__( 'Add an artist first.', 'wp-fedistream' ) . '</a>'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render track codes meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_codes_meta_box( \WP_Post $post ): void {
|
||||
$isrc = get_post_meta( $post->ID, self::META_PREFIX . 'isrc', true );
|
||||
?>
|
||||
<p>
|
||||
<label for="fedistream_track_isrc"><?php esc_html_e( 'ISRC', 'wp-fedistream' ); ?></label>
|
||||
<input type="text" name="fedistream_track_isrc" id="fedistream_track_isrc" value="<?php echo esc_attr( $isrc ); ?>" class="widefat" pattern="[A-Z]{2}[A-Z0-9]{3}[0-9]{7}" title="<?php esc_attr_e( 'ISRC format: CC-XXX-YY-NNNNN', 'wp-fedistream' ); ?>">
|
||||
<span class="description"><?php esc_html_e( 'International Standard Recording Code', 'wp-fedistream' ); ?></span>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save post meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function save_meta( int $post_id, \WP_Post $post ): void {
|
||||
if ( ! $this->can_save( $post_id, 'fedistream_track_save', 'fedistream_track_nonce' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save audio fields.
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'audio_file', 'fedistream_track_audio_file' );
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'duration', 'fedistream_track_duration' );
|
||||
|
||||
// Save audio format.
|
||||
if ( isset( $_POST['fedistream_track_audio_format'] ) ) {
|
||||
$allowed_formats = array( '', 'mp3', 'wav', 'flac', 'ogg' );
|
||||
$format = sanitize_text_field( wp_unslash( $_POST['fedistream_track_audio_format'] ) );
|
||||
if ( in_array( $format, $allowed_formats, true ) ) {
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'audio_format', $format );
|
||||
}
|
||||
}
|
||||
|
||||
// Save track info.
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'number', 'fedistream_track_number' );
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'disc_number', 'fedistream_track_disc_number' );
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'bpm', 'fedistream_track_bpm' );
|
||||
$this->save_text_meta( $post_id, self::META_PREFIX . 'key', 'fedistream_track_key' );
|
||||
$this->save_bool_meta( $post_id, self::META_PREFIX . 'explicit', 'fedistream_track_explicit' );
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'preview_start', 'fedistream_track_preview_start' );
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'preview_duration', 'fedistream_track_preview_duration' );
|
||||
|
||||
// Save album.
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'album', 'fedistream_track_album' );
|
||||
|
||||
// Save artists.
|
||||
if ( isset( $_POST['fedistream_track_artists'] ) && is_array( $_POST['fedistream_track_artists'] ) ) {
|
||||
$artists = array_map( 'absint', $_POST['fedistream_track_artists'] );
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'artists', $artists );
|
||||
} else {
|
||||
delete_post_meta( $post_id, self::META_PREFIX . 'artists' );
|
||||
}
|
||||
|
||||
// Save ISRC.
|
||||
$this->save_text_meta( $post_id, self::META_PREFIX . 'isrc', 'fedistream_track_isrc' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update album stats when track is saved.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function update_album_on_save( int $post_id, \WP_Post $post ): void {
|
||||
$album_id = get_post_meta( $post_id, self::META_PREFIX . 'album', true );
|
||||
if ( $album_id ) {
|
||||
Album::update_album_stats( (int) $album_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track by ID with meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return array|null Track data or null.
|
||||
*/
|
||||
public static function get_track( int $post_id ): ?array {
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post || 'fedistream_track' !== $post->post_type ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$album_id = get_post_meta( $post_id, self::META_PREFIX . 'album', true );
|
||||
$audio_file = get_post_meta( $post_id, self::META_PREFIX . 'audio_file', true );
|
||||
$artists = get_post_meta( $post_id, self::META_PREFIX . 'artists', true ) ?: array();
|
||||
|
||||
// Get artist names.
|
||||
$artist_names = array();
|
||||
foreach ( $artists as $artist_id ) {
|
||||
$artist_names[] = get_the_title( $artist_id );
|
||||
}
|
||||
|
||||
return array(
|
||||
'id' => $post->ID,
|
||||
'title' => $post->post_title,
|
||||
'slug' => $post->post_name,
|
||||
'lyrics' => $post->post_content,
|
||||
'album_id' => $album_id ? (int) $album_id : null,
|
||||
'album_title' => $album_id ? get_the_title( $album_id ) : null,
|
||||
'artists' => $artists,
|
||||
'artist_names' => $artist_names,
|
||||
'track_number' => (int) get_post_meta( $post_id, self::META_PREFIX . 'number', true ),
|
||||
'disc_number' => (int) get_post_meta( $post_id, self::META_PREFIX . 'disc_number', true ) ?: 1,
|
||||
'duration' => (int) get_post_meta( $post_id, self::META_PREFIX . 'duration', true ),
|
||||
'audio_file' => $audio_file ? (int) $audio_file : null,
|
||||
'audio_url' => $audio_file ? wp_get_attachment_url( $audio_file ) : null,
|
||||
'audio_format' => get_post_meta( $post_id, self::META_PREFIX . 'audio_format', true ),
|
||||
'bpm' => (int) get_post_meta( $post_id, self::META_PREFIX . 'bpm', true ),
|
||||
'key' => get_post_meta( $post_id, self::META_PREFIX . 'key', true ),
|
||||
'explicit' => (bool) get_post_meta( $post_id, self::META_PREFIX . 'explicit', true ),
|
||||
'isrc' => get_post_meta( $post_id, self::META_PREFIX . 'isrc', true ),
|
||||
'preview_start' => (int) get_post_meta( $post_id, self::META_PREFIX . 'preview_start', true ),
|
||||
'preview_duration' => (int) get_post_meta( $post_id, self::META_PREFIX . 'preview_duration', true ) ?: 30,
|
||||
'artwork' => get_the_post_thumbnail_url( $post_id, 'large' ),
|
||||
'url' => get_permalink( $post_id ),
|
||||
);
|
||||
}
|
||||
}
|
||||
1
includes/PostTypes/index.php
Normal file
1
includes/PostTypes/index.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php // Silence is golden.
|
||||
308
includes/Roles/Capabilities.php
Normal file
308
includes/Roles/Capabilities.php
Normal file
@@ -0,0 +1,308 @@
|
||||
<?php
|
||||
/**
|
||||
* User roles and capabilities.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\Roles;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Capabilities class.
|
||||
*
|
||||
* Handles custom user roles and capabilities.
|
||||
*/
|
||||
class Capabilities {
|
||||
|
||||
/**
|
||||
* Post type capabilities.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private const POST_TYPE_CAPS = array(
|
||||
'fedistream_artist' => array(
|
||||
'edit_post' => 'edit_fedistream_artist',
|
||||
'read_post' => 'read_fedistream_artist',
|
||||
'delete_post' => 'delete_fedistream_artist',
|
||||
'edit_posts' => 'edit_fedistream_artists',
|
||||
'edit_others_posts' => 'edit_others_fedistream_artists',
|
||||
'publish_posts' => 'publish_fedistream_artists',
|
||||
'read_private_posts' => 'read_private_fedistream_artists',
|
||||
),
|
||||
'fedistream_album' => array(
|
||||
'edit_post' => 'edit_fedistream_album',
|
||||
'read_post' => 'read_fedistream_album',
|
||||
'delete_post' => 'delete_fedistream_album',
|
||||
'edit_posts' => 'edit_fedistream_albums',
|
||||
'edit_others_posts' => 'edit_others_fedistream_albums',
|
||||
'publish_posts' => 'publish_fedistream_albums',
|
||||
'read_private_posts' => 'read_private_fedistream_albums',
|
||||
),
|
||||
'fedistream_track' => array(
|
||||
'edit_post' => 'edit_fedistream_track',
|
||||
'read_post' => 'read_fedistream_track',
|
||||
'delete_post' => 'delete_fedistream_track',
|
||||
'edit_posts' => 'edit_fedistream_tracks',
|
||||
'edit_others_posts' => 'edit_others_fedistream_tracks',
|
||||
'publish_posts' => 'publish_fedistream_tracks',
|
||||
'read_private_posts' => 'read_private_fedistream_tracks',
|
||||
),
|
||||
'fedistream_playlist' => array(
|
||||
'edit_post' => 'edit_fedistream_playlist',
|
||||
'read_post' => 'read_fedistream_playlist',
|
||||
'delete_post' => 'delete_fedistream_playlist',
|
||||
'edit_posts' => 'edit_fedistream_playlists',
|
||||
'edit_others_posts' => 'edit_others_fedistream_playlists',
|
||||
'publish_posts' => 'publish_fedistream_playlists',
|
||||
'read_private_posts' => 'read_private_fedistream_playlists',
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Taxonomy capabilities.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private const TAXONOMY_CAPS = array(
|
||||
'manage_fedistream_genres',
|
||||
'manage_fedistream_moods',
|
||||
'manage_fedistream_licenses',
|
||||
);
|
||||
|
||||
/**
|
||||
* Custom capabilities.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private const CUSTOM_CAPS = array(
|
||||
'view_fedistream_stats',
|
||||
'manage_fedistream_settings',
|
||||
);
|
||||
|
||||
/**
|
||||
* Get all custom capabilities.
|
||||
*
|
||||
* @return array Array of all custom capabilities.
|
||||
*/
|
||||
public static function get_all_capabilities(): array {
|
||||
$caps = array();
|
||||
|
||||
// Post type capabilities.
|
||||
foreach ( self::POST_TYPE_CAPS as $post_type_caps ) {
|
||||
$caps = array_merge( $caps, array_values( $post_type_caps ) );
|
||||
}
|
||||
|
||||
// Delete capabilities.
|
||||
$caps[] = 'delete_fedistream_artists';
|
||||
$caps[] = 'delete_others_fedistream_artists';
|
||||
$caps[] = 'delete_published_fedistream_artists';
|
||||
$caps[] = 'delete_private_fedistream_artists';
|
||||
$caps[] = 'delete_fedistream_albums';
|
||||
$caps[] = 'delete_others_fedistream_albums';
|
||||
$caps[] = 'delete_published_fedistream_albums';
|
||||
$caps[] = 'delete_private_fedistream_albums';
|
||||
$caps[] = 'delete_fedistream_tracks';
|
||||
$caps[] = 'delete_others_fedistream_tracks';
|
||||
$caps[] = 'delete_published_fedistream_tracks';
|
||||
$caps[] = 'delete_private_fedistream_tracks';
|
||||
$caps[] = 'delete_fedistream_playlists';
|
||||
$caps[] = 'delete_others_fedistream_playlists';
|
||||
$caps[] = 'delete_published_fedistream_playlists';
|
||||
$caps[] = 'delete_private_fedistream_playlists';
|
||||
|
||||
// Taxonomy capabilities.
|
||||
$caps = array_merge( $caps, self::TAXONOMY_CAPS );
|
||||
|
||||
// Custom capabilities.
|
||||
$caps = array_merge( $caps, self::CUSTOM_CAPS );
|
||||
|
||||
return array_unique( $caps );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Artist role capabilities.
|
||||
*
|
||||
* @return array Array of capabilities for the Artist role.
|
||||
*/
|
||||
public static function get_artist_capabilities(): array {
|
||||
return array(
|
||||
// Core WordPress.
|
||||
'read' => true,
|
||||
'upload_files' => true,
|
||||
|
||||
// Own artists.
|
||||
'edit_fedistream_artists' => true,
|
||||
'edit_published_fedistream_artists' => true,
|
||||
'publish_fedistream_artists' => true,
|
||||
'delete_fedistream_artists' => true,
|
||||
'delete_published_fedistream_artists' => true,
|
||||
|
||||
// Own albums.
|
||||
'edit_fedistream_albums' => true,
|
||||
'edit_published_fedistream_albums' => true,
|
||||
'publish_fedistream_albums' => true,
|
||||
'delete_fedistream_albums' => true,
|
||||
'delete_published_fedistream_albums' => true,
|
||||
|
||||
// Own tracks.
|
||||
'edit_fedistream_tracks' => true,
|
||||
'edit_published_fedistream_tracks' => true,
|
||||
'publish_fedistream_tracks' => true,
|
||||
'delete_fedistream_tracks' => true,
|
||||
'delete_published_fedistream_tracks' => true,
|
||||
|
||||
// Own playlists.
|
||||
'edit_fedistream_playlists' => true,
|
||||
'edit_published_fedistream_playlists' => true,
|
||||
'publish_fedistream_playlists' => true,
|
||||
'delete_fedistream_playlists' => true,
|
||||
'delete_published_fedistream_playlists' => true,
|
||||
|
||||
// View stats.
|
||||
'view_fedistream_stats' => true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Label role capabilities.
|
||||
*
|
||||
* @return array Array of capabilities for the Label role.
|
||||
*/
|
||||
public static function get_label_capabilities(): array {
|
||||
$caps = self::get_artist_capabilities();
|
||||
|
||||
// Add label-specific capabilities.
|
||||
$label_caps = array(
|
||||
// Manage others' content.
|
||||
'edit_others_fedistream_artists' => true,
|
||||
'edit_others_fedistream_albums' => true,
|
||||
'edit_others_fedistream_tracks' => true,
|
||||
'edit_others_fedistream_playlists' => true,
|
||||
'delete_others_fedistream_artists' => true,
|
||||
'delete_others_fedistream_albums' => true,
|
||||
'delete_others_fedistream_tracks' => true,
|
||||
'delete_others_fedistream_playlists' => true,
|
||||
'read_private_fedistream_artists' => true,
|
||||
'read_private_fedistream_albums' => true,
|
||||
'read_private_fedistream_tracks' => true,
|
||||
'read_private_fedistream_playlists' => true,
|
||||
'delete_private_fedistream_artists' => true,
|
||||
'delete_private_fedistream_albums' => true,
|
||||
'delete_private_fedistream_tracks' => true,
|
||||
'delete_private_fedistream_playlists' => true,
|
||||
|
||||
// Manage taxonomies.
|
||||
'manage_fedistream_genres' => true,
|
||||
'manage_fedistream_moods' => true,
|
||||
'manage_fedistream_licenses' => true,
|
||||
|
||||
// View stats.
|
||||
'view_fedistream_stats' => true,
|
||||
);
|
||||
|
||||
return array_merge( $caps, $label_caps );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom roles.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function add_roles(): void {
|
||||
// Remove existing roles first to ensure clean slate.
|
||||
remove_role( 'fedistream_artist' );
|
||||
remove_role( 'fedistream_label' );
|
||||
|
||||
// Add Artist role.
|
||||
add_role(
|
||||
'fedistream_artist',
|
||||
__( 'FediStream Artist', 'wp-fedistream' ),
|
||||
self::get_artist_capabilities()
|
||||
);
|
||||
|
||||
// Add Label role.
|
||||
add_role(
|
||||
'fedistream_label',
|
||||
__( 'FediStream Label', 'wp-fedistream' ),
|
||||
self::get_label_capabilities()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove custom roles.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function remove_roles(): void {
|
||||
remove_role( 'fedistream_artist' );
|
||||
remove_role( 'fedistream_label' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add capabilities to administrator role.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function add_admin_capabilities(): void {
|
||||
$admin = get_role( 'administrator' );
|
||||
|
||||
if ( ! $admin ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add all custom capabilities.
|
||||
$all_caps = self::get_all_capabilities();
|
||||
foreach ( $all_caps as $cap ) {
|
||||
$admin->add_cap( $cap );
|
||||
}
|
||||
|
||||
// Add management capability.
|
||||
$admin->add_cap( 'manage_fedistream_settings' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove capabilities from administrator role.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function remove_admin_capabilities(): void {
|
||||
$admin = get_role( 'administrator' );
|
||||
|
||||
if ( ! $admin ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove all custom capabilities.
|
||||
$all_caps = self::get_all_capabilities();
|
||||
foreach ( $all_caps as $cap ) {
|
||||
$admin->remove_cap( $cap );
|
||||
}
|
||||
|
||||
$admin->remove_cap( 'manage_fedistream_settings' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Install roles and capabilities.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function install(): void {
|
||||
self::add_roles();
|
||||
self::add_admin_capabilities();
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall roles and capabilities.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function uninstall(): void {
|
||||
self::remove_roles();
|
||||
self::remove_admin_capabilities();
|
||||
}
|
||||
}
|
||||
1
includes/Roles/index.php
Normal file
1
includes/Roles/index.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php // Silence is golden.
|
||||
67
includes/Taxonomies/AbstractTaxonomy.php
Normal file
67
includes/Taxonomies/AbstractTaxonomy.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
/**
|
||||
* Abstract base class for custom taxonomies.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\Taxonomies;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract taxonomy class.
|
||||
*
|
||||
* Provides common functionality for all custom taxonomies.
|
||||
*/
|
||||
abstract class AbstractTaxonomy {
|
||||
|
||||
/**
|
||||
* Taxonomy key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $taxonomy;
|
||||
|
||||
/**
|
||||
* Post types this taxonomy applies to.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $post_types = array();
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'init', array( $this, 'register' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the taxonomy.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function register(): void;
|
||||
|
||||
/**
|
||||
* Get the taxonomy key.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_taxonomy(): string {
|
||||
return $this->taxonomy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the post types this taxonomy is registered for.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_post_types(): array {
|
||||
return $this->post_types;
|
||||
}
|
||||
}
|
||||
154
includes/Taxonomies/Genre.php
Normal file
154
includes/Taxonomies/Genre.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
/**
|
||||
* Genre custom taxonomy.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\Taxonomies;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Genre taxonomy class.
|
||||
*
|
||||
* Hierarchical taxonomy for music genres.
|
||||
*/
|
||||
class Genre extends AbstractTaxonomy {
|
||||
|
||||
/**
|
||||
* Taxonomy key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $taxonomy = 'fedistream_genre';
|
||||
|
||||
/**
|
||||
* Post types this taxonomy applies to.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $post_types = array(
|
||||
'fedistream_artist',
|
||||
'fedistream_album',
|
||||
'fedistream_track',
|
||||
);
|
||||
|
||||
/**
|
||||
* Register the taxonomy.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void {
|
||||
$labels = array(
|
||||
'name' => _x( 'Genres', 'taxonomy general name', 'wp-fedistream' ),
|
||||
'singular_name' => _x( 'Genre', 'taxonomy singular name', 'wp-fedistream' ),
|
||||
'search_items' => __( 'Search Genres', 'wp-fedistream' ),
|
||||
'popular_items' => __( 'Popular Genres', 'wp-fedistream' ),
|
||||
'all_items' => __( 'All Genres', 'wp-fedistream' ),
|
||||
'parent_item' => __( 'Parent Genre', 'wp-fedistream' ),
|
||||
'parent_item_colon' => __( 'Parent Genre:', 'wp-fedistream' ),
|
||||
'edit_item' => __( 'Edit Genre', 'wp-fedistream' ),
|
||||
'view_item' => __( 'View Genre', 'wp-fedistream' ),
|
||||
'update_item' => __( 'Update Genre', 'wp-fedistream' ),
|
||||
'add_new_item' => __( 'Add New Genre', 'wp-fedistream' ),
|
||||
'new_item_name' => __( 'New Genre Name', 'wp-fedistream' ),
|
||||
'separate_items_with_commas' => __( 'Separate genres with commas', 'wp-fedistream' ),
|
||||
'add_or_remove_items' => __( 'Add or remove genres', 'wp-fedistream' ),
|
||||
'choose_from_most_used' => __( 'Choose from the most used genres', 'wp-fedistream' ),
|
||||
'not_found' => __( 'No genres found.', 'wp-fedistream' ),
|
||||
'no_terms' => __( 'No genres', 'wp-fedistream' ),
|
||||
'menu_name' => __( 'Genres', 'wp-fedistream' ),
|
||||
'items_list_navigation' => __( 'Genres list navigation', 'wp-fedistream' ),
|
||||
'items_list' => __( 'Genres list', 'wp-fedistream' ),
|
||||
'back_to_items' => __( '← Back to Genres', 'wp-fedistream' ),
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'labels' => $labels,
|
||||
'hierarchical' => true, // Like categories.
|
||||
'public' => true,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => false, // Will be added to custom menu.
|
||||
'show_admin_column' => true,
|
||||
'show_in_nav_menus' => true,
|
||||
'show_tagcloud' => true,
|
||||
'show_in_rest' => true,
|
||||
'rest_base' => 'genres',
|
||||
'query_var' => true,
|
||||
'rewrite' => array( 'slug' => 'genre' ),
|
||||
'capabilities' => array(
|
||||
'manage_terms' => 'manage_fedistream_genres',
|
||||
'edit_terms' => 'manage_fedistream_genres',
|
||||
'delete_terms' => 'manage_fedistream_genres',
|
||||
'assign_terms' => 'edit_fedistream_tracks',
|
||||
),
|
||||
);
|
||||
|
||||
register_taxonomy( $this->taxonomy, $this->post_types, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default genres.
|
||||
*
|
||||
* @return array Array of genres with children.
|
||||
*/
|
||||
public static function get_default_genres(): array {
|
||||
return array(
|
||||
'Rock' => array( 'Alternative Rock', 'Hard Rock', 'Indie Rock', 'Progressive Rock', 'Punk Rock' ),
|
||||
'Pop' => array( 'Dance Pop', 'Electropop', 'Indie Pop', 'Synth Pop' ),
|
||||
'Electronic' => array( 'Ambient', 'Drum and Bass', 'Dubstep', 'House', 'Techno', 'Trance' ),
|
||||
'Hip Hop' => array( 'Rap', 'Trap', 'Lo-Fi Hip Hop' ),
|
||||
'Jazz' => array( 'Bebop', 'Cool Jazz', 'Free Jazz', 'Fusion' ),
|
||||
'Classical' => array( 'Baroque', 'Chamber Music', 'Opera', 'Orchestral', 'Romantic' ),
|
||||
'R&B' => array( 'Contemporary R&B', 'Neo Soul', 'Soul' ),
|
||||
'Country' => array( 'Americana', 'Bluegrass', 'Country Rock' ),
|
||||
'Metal' => array( 'Black Metal', 'Death Metal', 'Heavy Metal', 'Thrash Metal' ),
|
||||
'Folk' => array( 'Acoustic', 'Folk Rock', 'Traditional Folk' ),
|
||||
'Blues' => array( 'Chicago Blues', 'Delta Blues', 'Electric Blues' ),
|
||||
'Reggae' => array( 'Dancehall', 'Dub', 'Roots Reggae', 'Ska' ),
|
||||
'Latin' => array( 'Bossa Nova', 'Salsa', 'Reggaeton', 'Tango' ),
|
||||
'World' => array( 'African', 'Asian', 'Celtic', 'Middle Eastern' ),
|
||||
'Soundtrack' => array( 'Film Score', 'Video Game', 'Musical Theatre' ),
|
||||
'Other' => array(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Install default genres.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function install_defaults(): void {
|
||||
$genres = self::get_default_genres();
|
||||
|
||||
foreach ( $genres as $parent => $children ) {
|
||||
// Check if parent exists.
|
||||
$parent_term = term_exists( $parent, 'fedistream_genre' );
|
||||
|
||||
if ( ! $parent_term ) {
|
||||
$parent_term = wp_insert_term( $parent, 'fedistream_genre' );
|
||||
}
|
||||
|
||||
if ( is_wp_error( $parent_term ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parent_id = is_array( $parent_term ) ? $parent_term['term_id'] : $parent_term;
|
||||
|
||||
// Insert children.
|
||||
foreach ( $children as $child ) {
|
||||
if ( ! term_exists( $child, 'fedistream_genre' ) ) {
|
||||
wp_insert_term(
|
||||
$child,
|
||||
'fedistream_genre',
|
||||
array( 'parent' => (int) $parent_id )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
164
includes/Taxonomies/License.php
Normal file
164
includes/Taxonomies/License.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
/**
|
||||
* License custom taxonomy.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\Taxonomies;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* License taxonomy class.
|
||||
*
|
||||
* Hierarchical taxonomy for content licenses.
|
||||
*/
|
||||
class License extends AbstractTaxonomy {
|
||||
|
||||
/**
|
||||
* Taxonomy key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $taxonomy = 'fedistream_license';
|
||||
|
||||
/**
|
||||
* Post types this taxonomy applies to.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $post_types = array(
|
||||
'fedistream_album',
|
||||
'fedistream_track',
|
||||
);
|
||||
|
||||
/**
|
||||
* Register the taxonomy.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void {
|
||||
$labels = array(
|
||||
'name' => _x( 'Licenses', 'taxonomy general name', 'wp-fedistream' ),
|
||||
'singular_name' => _x( 'License', 'taxonomy singular name', 'wp-fedistream' ),
|
||||
'search_items' => __( 'Search Licenses', 'wp-fedistream' ),
|
||||
'popular_items' => __( 'Popular Licenses', 'wp-fedistream' ),
|
||||
'all_items' => __( 'All Licenses', 'wp-fedistream' ),
|
||||
'parent_item' => __( 'Parent License', 'wp-fedistream' ),
|
||||
'parent_item_colon' => __( 'Parent License:', 'wp-fedistream' ),
|
||||
'edit_item' => __( 'Edit License', 'wp-fedistream' ),
|
||||
'view_item' => __( 'View License', 'wp-fedistream' ),
|
||||
'update_item' => __( 'Update License', 'wp-fedistream' ),
|
||||
'add_new_item' => __( 'Add New License', 'wp-fedistream' ),
|
||||
'new_item_name' => __( 'New License Name', 'wp-fedistream' ),
|
||||
'separate_items_with_commas' => __( 'Separate licenses with commas', 'wp-fedistream' ),
|
||||
'add_or_remove_items' => __( 'Add or remove licenses', 'wp-fedistream' ),
|
||||
'choose_from_most_used' => __( 'Choose from the most used licenses', 'wp-fedistream' ),
|
||||
'not_found' => __( 'No licenses found.', 'wp-fedistream' ),
|
||||
'no_terms' => __( 'No licenses', 'wp-fedistream' ),
|
||||
'menu_name' => __( 'Licenses', 'wp-fedistream' ),
|
||||
'items_list_navigation' => __( 'Licenses list navigation', 'wp-fedistream' ),
|
||||
'items_list' => __( 'Licenses list', 'wp-fedistream' ),
|
||||
'back_to_items' => __( '← Back to Licenses', 'wp-fedistream' ),
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'labels' => $labels,
|
||||
'hierarchical' => true, // Like categories.
|
||||
'public' => true,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => false, // Will be added to custom menu.
|
||||
'show_admin_column' => true,
|
||||
'show_in_nav_menus' => false,
|
||||
'show_tagcloud' => false,
|
||||
'show_in_rest' => true,
|
||||
'rest_base' => 'licenses',
|
||||
'query_var' => true,
|
||||
'rewrite' => array( 'slug' => 'license' ),
|
||||
'capabilities' => array(
|
||||
'manage_terms' => 'manage_fedistream_licenses',
|
||||
'edit_terms' => 'manage_fedistream_licenses',
|
||||
'delete_terms' => 'manage_fedistream_licenses',
|
||||
'assign_terms' => 'edit_fedistream_tracks',
|
||||
),
|
||||
);
|
||||
|
||||
register_taxonomy( $this->taxonomy, $this->post_types, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default licenses.
|
||||
*
|
||||
* @return array Array of licenses with descriptions.
|
||||
*/
|
||||
public static function get_default_licenses(): array {
|
||||
return array(
|
||||
'All Rights Reserved' => array(
|
||||
'description' => __( 'Standard copyright. All rights reserved by the creator.', 'wp-fedistream' ),
|
||||
'children' => array(),
|
||||
),
|
||||
'Creative Commons' => array(
|
||||
'description' => __( 'Creative Commons licenses for sharing and reuse.', 'wp-fedistream' ),
|
||||
'children' => array(
|
||||
'CC0' => __( 'Public Domain Dedication - No rights reserved', 'wp-fedistream' ),
|
||||
'CC BY' => __( 'Attribution - Credit must be given', 'wp-fedistream' ),
|
||||
'CC BY-SA' => __( 'Attribution-ShareAlike - Credit and share under same terms', 'wp-fedistream' ),
|
||||
'CC BY-ND' => __( 'Attribution-NoDerivs - Credit, no modifications', 'wp-fedistream' ),
|
||||
'CC BY-NC' => __( 'Attribution-NonCommercial - Credit, non-commercial only', 'wp-fedistream' ),
|
||||
'CC BY-NC-SA' => __( 'Attribution-NonCommercial-ShareAlike', 'wp-fedistream' ),
|
||||
'CC BY-NC-ND' => __( 'Attribution-NonCommercial-NoDerivs', 'wp-fedistream' ),
|
||||
),
|
||||
),
|
||||
'Public Domain' => array(
|
||||
'description' => __( 'Works in the public domain with no copyright restrictions.', 'wp-fedistream' ),
|
||||
'children' => array(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Install default licenses.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function install_defaults(): void {
|
||||
$licenses = self::get_default_licenses();
|
||||
|
||||
foreach ( $licenses as $name => $data ) {
|
||||
// Check if parent exists.
|
||||
$parent_term = term_exists( $name, 'fedistream_license' );
|
||||
|
||||
if ( ! $parent_term ) {
|
||||
$parent_term = wp_insert_term(
|
||||
$name,
|
||||
'fedistream_license',
|
||||
array( 'description' => $data['description'] )
|
||||
);
|
||||
}
|
||||
|
||||
if ( is_wp_error( $parent_term ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parent_id = is_array( $parent_term ) ? $parent_term['term_id'] : $parent_term;
|
||||
|
||||
// Insert children.
|
||||
foreach ( $data['children'] as $child_name => $child_desc ) {
|
||||
if ( ! term_exists( $child_name, 'fedistream_license' ) ) {
|
||||
wp_insert_term(
|
||||
$child_name,
|
||||
'fedistream_license',
|
||||
array(
|
||||
'parent' => (int) $parent_id,
|
||||
'description' => $child_desc,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
135
includes/Taxonomies/Mood.php
Normal file
135
includes/Taxonomies/Mood.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
/**
|
||||
* Mood custom taxonomy.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\Taxonomies;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mood taxonomy class.
|
||||
*
|
||||
* Non-hierarchical taxonomy for track/playlist moods.
|
||||
*/
|
||||
class Mood extends AbstractTaxonomy {
|
||||
|
||||
/**
|
||||
* Taxonomy key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $taxonomy = 'fedistream_mood';
|
||||
|
||||
/**
|
||||
* Post types this taxonomy applies to.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $post_types = array(
|
||||
'fedistream_track',
|
||||
'fedistream_playlist',
|
||||
);
|
||||
|
||||
/**
|
||||
* Register the taxonomy.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void {
|
||||
$labels = array(
|
||||
'name' => _x( 'Moods', 'taxonomy general name', 'wp-fedistream' ),
|
||||
'singular_name' => _x( 'Mood', 'taxonomy singular name', 'wp-fedistream' ),
|
||||
'search_items' => __( 'Search Moods', 'wp-fedistream' ),
|
||||
'popular_items' => __( 'Popular Moods', 'wp-fedistream' ),
|
||||
'all_items' => __( 'All Moods', 'wp-fedistream' ),
|
||||
'edit_item' => __( 'Edit Mood', 'wp-fedistream' ),
|
||||
'view_item' => __( 'View Mood', 'wp-fedistream' ),
|
||||
'update_item' => __( 'Update Mood', 'wp-fedistream' ),
|
||||
'add_new_item' => __( 'Add New Mood', 'wp-fedistream' ),
|
||||
'new_item_name' => __( 'New Mood Name', 'wp-fedistream' ),
|
||||
'separate_items_with_commas' => __( 'Separate moods with commas', 'wp-fedistream' ),
|
||||
'add_or_remove_items' => __( 'Add or remove moods', 'wp-fedistream' ),
|
||||
'choose_from_most_used' => __( 'Choose from the most used moods', 'wp-fedistream' ),
|
||||
'not_found' => __( 'No moods found.', 'wp-fedistream' ),
|
||||
'no_terms' => __( 'No moods', 'wp-fedistream' ),
|
||||
'menu_name' => __( 'Moods', 'wp-fedistream' ),
|
||||
'items_list_navigation' => __( 'Moods list navigation', 'wp-fedistream' ),
|
||||
'items_list' => __( 'Moods list', 'wp-fedistream' ),
|
||||
'back_to_items' => __( '← Back to Moods', 'wp-fedistream' ),
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'labels' => $labels,
|
||||
'hierarchical' => false, // Like tags.
|
||||
'public' => true,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => false, // Will be added to custom menu.
|
||||
'show_admin_column' => true,
|
||||
'show_in_nav_menus' => true,
|
||||
'show_tagcloud' => true,
|
||||
'show_in_rest' => true,
|
||||
'rest_base' => 'moods',
|
||||
'query_var' => true,
|
||||
'rewrite' => array( 'slug' => 'mood' ),
|
||||
'capabilities' => array(
|
||||
'manage_terms' => 'manage_fedistream_moods',
|
||||
'edit_terms' => 'manage_fedistream_moods',
|
||||
'delete_terms' => 'manage_fedistream_moods',
|
||||
'assign_terms' => 'edit_fedistream_tracks',
|
||||
),
|
||||
);
|
||||
|
||||
register_taxonomy( $this->taxonomy, $this->post_types, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default moods.
|
||||
*
|
||||
* @return array Array of moods.
|
||||
*/
|
||||
public static function get_default_moods(): array {
|
||||
return array(
|
||||
__( 'Energetic', 'wp-fedistream' ),
|
||||
__( 'Calm', 'wp-fedistream' ),
|
||||
__( 'Uplifting', 'wp-fedistream' ),
|
||||
__( 'Melancholic', 'wp-fedistream' ),
|
||||
__( 'Aggressive', 'wp-fedistream' ),
|
||||
__( 'Romantic', 'wp-fedistream' ),
|
||||
__( 'Happy', 'wp-fedistream' ),
|
||||
__( 'Sad', 'wp-fedistream' ),
|
||||
__( 'Relaxing', 'wp-fedistream' ),
|
||||
__( 'Intense', 'wp-fedistream' ),
|
||||
__( 'Dreamy', 'wp-fedistream' ),
|
||||
__( 'Dark', 'wp-fedistream' ),
|
||||
__( 'Groovy', 'wp-fedistream' ),
|
||||
__( 'Epic', 'wp-fedistream' ),
|
||||
__( 'Peaceful', 'wp-fedistream' ),
|
||||
__( 'Motivational', 'wp-fedistream' ),
|
||||
__( 'Nostalgic', 'wp-fedistream' ),
|
||||
__( 'Playful', 'wp-fedistream' ),
|
||||
__( 'Sensual', 'wp-fedistream' ),
|
||||
__( 'Suspenseful', 'wp-fedistream' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Install default moods.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function install_defaults(): void {
|
||||
$moods = self::get_default_moods();
|
||||
|
||||
foreach ( $moods as $mood ) {
|
||||
if ( ! term_exists( $mood, 'fedistream_mood' ) ) {
|
||||
wp_insert_term( $mood, 'fedistream_mood' );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
includes/Taxonomies/index.php
Normal file
1
includes/Taxonomies/index.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php // Silence is golden.
|
||||
794
includes/User/Library.php
Normal file
794
includes/User/Library.php
Normal file
@@ -0,0 +1,794 @@
|
||||
<?php
|
||||
/**
|
||||
* User Library class.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\User;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles user library features (favorites, follows, history).
|
||||
*/
|
||||
class Library {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'wp_ajax_fedistream_toggle_favorite', array( $this, 'ajax_toggle_favorite' ) );
|
||||
add_action( 'wp_ajax_fedistream_toggle_follow', array( $this, 'ajax_toggle_follow' ) );
|
||||
add_action( 'wp_ajax_fedistream_get_library', array( $this, 'ajax_get_library' ) );
|
||||
add_action( 'wp_ajax_fedistream_get_followed_artists', array( $this, 'ajax_get_followed_artists' ) );
|
||||
add_action( 'wp_ajax_fedistream_get_history', array( $this, 'ajax_get_history' ) );
|
||||
add_action( 'wp_ajax_fedistream_clear_history', array( $this, 'ajax_clear_history' ) );
|
||||
|
||||
// Add library buttons to content.
|
||||
add_filter( 'fedistream_track_actions', array( $this, 'add_favorite_button' ), 10, 2 );
|
||||
add_filter( 'fedistream_album_actions', array( $this, 'add_favorite_button' ), 10, 2 );
|
||||
add_filter( 'fedistream_artist_actions', array( $this, 'add_follow_button' ), 10, 2 );
|
||||
|
||||
// Record play history.
|
||||
add_action( 'fedistream_track_played', array( $this, 'record_play_history' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle favorite via AJAX.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ajax_toggle_favorite(): 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' ) ) );
|
||||
}
|
||||
|
||||
$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(
|
||||
'<button class="fedistream-favorite-btn%s" data-content-type="%s" data-content-id="%d" title="%s">
|
||||
<span class="dashicons dashicons-heart"></span>
|
||||
</button>',
|
||||
$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(
|
||||
'<button class="fedistream-follow-btn%s" data-artist-id="%d">
|
||||
<span class="dashicons dashicons-%s"></span>
|
||||
<span class="button-text">%s</span>
|
||||
</button>',
|
||||
$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;
|
||||
}
|
||||
}
|
||||
324
includes/User/LibraryPage.php
Normal file
324
includes/User/LibraryPage.php
Normal file
@@ -0,0 +1,324 @@
|
||||
<?php
|
||||
/**
|
||||
* User Library Page class.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\User;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the My Library page display.
|
||||
*/
|
||||
class LibraryPage {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_shortcode( 'fedistream_library', array( $this, 'render_library_shortcode' ) );
|
||||
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue library scripts and styles.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function enqueue_scripts(): void {
|
||||
if ( ! is_user_logged_in() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_script(
|
||||
'fedistream-library',
|
||||
WP_FEDISTREAM_URL . 'assets/js/library.js',
|
||||
array( 'jquery' ),
|
||||
WP_FEDISTREAM_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
wp_localize_script(
|
||||
'fedistream-library',
|
||||
'fedistreamLibrary',
|
||||
array(
|
||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||
'nonce' => wp_create_nonce( 'fedistream_library' ),
|
||||
'i18n' => array(
|
||||
'loading' => __( 'Loading...', 'wp-fedistream' ),
|
||||
'noFavorites' => __( 'No favorites yet.', 'wp-fedistream' ),
|
||||
'noArtists' => __( 'Not following any artists yet.', 'wp-fedistream' ),
|
||||
'noHistory' => __( 'No listening history.', 'wp-fedistream' ),
|
||||
'confirmClear' => __( 'Are you sure you want to clear your listening history?', 'wp-fedistream' ),
|
||||
'historyCleared' => __( 'History cleared.', 'wp-fedistream' ),
|
||||
'error' => __( 'An error occurred. Please try again.', 'wp-fedistream' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the library shortcode.
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
* @return string
|
||||
*/
|
||||
public function render_library_shortcode( array $atts = array() ): string {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'tab' => 'favorites',
|
||||
),
|
||||
$atts,
|
||||
'fedistream_library'
|
||||
);
|
||||
|
||||
if ( ! is_user_logged_in() ) {
|
||||
return $this->render_login_required();
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
// Get counts for tabs.
|
||||
$favorite_count = Library::get_favorite_count( $user_id );
|
||||
$following_count = Library::get_following_count( $user_id );
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="fedistream-library" data-initial-tab="<?php echo esc_attr( $atts['tab'] ); ?>">
|
||||
<nav class="fedistream-library-nav">
|
||||
<button class="tab-btn active" data-tab="favorites">
|
||||
<?php esc_html_e( 'Favorites', 'wp-fedistream' ); ?>
|
||||
<span class="count"><?php echo esc_html( $favorite_count ); ?></span>
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="artists">
|
||||
<?php esc_html_e( 'Artists', 'wp-fedistream' ); ?>
|
||||
<span class="count"><?php echo esc_html( $following_count ); ?></span>
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="history">
|
||||
<?php esc_html_e( 'History', 'wp-fedistream' ); ?>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="fedistream-library-filters" data-tab="favorites">
|
||||
<select class="filter-type">
|
||||
<option value="all"><?php esc_html_e( 'All', 'wp-fedistream' ); ?></option>
|
||||
<option value="track"><?php esc_html_e( 'Tracks', 'wp-fedistream' ); ?></option>
|
||||
<option value="album"><?php esc_html_e( 'Albums', 'wp-fedistream' ); ?></option>
|
||||
<option value="playlist"><?php esc_html_e( 'Playlists', 'wp-fedistream' ); ?></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="fedistream-library-content">
|
||||
<div class="tab-content active" id="tab-favorites">
|
||||
<div class="library-grid favorites-grid">
|
||||
<!-- Loaded via AJAX -->
|
||||
</div>
|
||||
<div class="library-pagination" data-tab="favorites"></div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="tab-artists">
|
||||
<div class="library-grid artists-grid">
|
||||
<!-- Loaded via AJAX -->
|
||||
</div>
|
||||
<div class="library-pagination" data-tab="artists"></div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="tab-history">
|
||||
<div class="history-actions">
|
||||
<button class="btn-clear-history">
|
||||
<?php esc_html_e( 'Clear History', 'wp-fedistream' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
<div class="library-list history-list">
|
||||
<!-- Loaded via AJAX -->
|
||||
</div>
|
||||
<div class="library-pagination" data-tab="history"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fedistream-library-loading">
|
||||
<span class="spinner"></span>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render login required message.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function render_login_required(): string {
|
||||
ob_start();
|
||||
?>
|
||||
<div class="fedistream-login-required">
|
||||
<p><?php esc_html_e( 'Please log in to view your library.', 'wp-fedistream' ); ?></p>
|
||||
<a href="<?php echo esc_url( wp_login_url( get_permalink() ) ); ?>" class="btn btn-primary">
|
||||
<?php esc_html_e( 'Log In', 'wp-fedistream' ); ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a favorite item.
|
||||
*
|
||||
* @param array $item Item data.
|
||||
* @return string
|
||||
*/
|
||||
public static function render_favorite_item( array $item ): string {
|
||||
ob_start();
|
||||
?>
|
||||
<div class="library-item favorite-item" data-type="<?php echo esc_attr( $item['type'] ); ?>" data-id="<?php echo esc_attr( $item['id'] ); ?>">
|
||||
<div class="item-thumbnail">
|
||||
<?php if ( ! empty( $item['thumbnail'] ) ) : ?>
|
||||
<img src="<?php echo esc_url( $item['thumbnail'] ); ?>" alt="<?php echo esc_attr( $item['title'] ); ?>">
|
||||
<?php else : ?>
|
||||
<div class="placeholder-thumbnail">
|
||||
<span class="dashicons dashicons-<?php echo 'track' === $item['type'] ? 'format-audio' : ( 'album' === $item['type'] ? 'album' : 'playlist-audio' ); ?>"></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ( 'track' === $item['type'] ) : ?>
|
||||
<button class="play-btn" data-track-id="<?php echo esc_attr( $item['id'] ); ?>">
|
||||
<span class="dashicons dashicons-controls-play"></span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<h4 class="item-title">
|
||||
<a href="<?php echo esc_url( $item['permalink'] ); ?>"><?php echo esc_html( $item['title'] ); ?></a>
|
||||
</h4>
|
||||
<?php if ( ! empty( $item['artist'] ) ) : ?>
|
||||
<p class="item-artist"><?php echo esc_html( $item['artist'] ); ?></p>
|
||||
<?php endif; ?>
|
||||
<?php if ( 'track' === $item['type'] && ! empty( $item['duration'] ) ) : ?>
|
||||
<p class="item-duration"><?php echo esc_html( self::format_duration( $item['duration'] ) ); ?></p>
|
||||
<?php elseif ( 'album' === $item['type'] && ! empty( $item['track_count'] ) ) : ?>
|
||||
<p class="item-tracks">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %d: number of tracks */
|
||||
esc_html( _n( '%d track', '%d tracks', (int) $item['track_count'], 'wp-fedistream' ) ),
|
||||
(int) $item['track_count']
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button class="unfavorite-btn" data-content-type="<?php echo esc_attr( $item['type'] ); ?>" data-content-id="<?php echo esc_attr( $item['id'] ); ?>" title="<?php esc_attr_e( 'Remove from library', 'wp-fedistream' ); ?>">
|
||||
<span class="dashicons dashicons-heart"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an artist item.
|
||||
*
|
||||
* @param array $artist Artist data.
|
||||
* @return string
|
||||
*/
|
||||
public static function render_artist_item( array $artist ): string {
|
||||
ob_start();
|
||||
?>
|
||||
<div class="library-item artist-item" data-id="<?php echo esc_attr( $artist['id'] ); ?>">
|
||||
<div class="item-thumbnail artist-avatar">
|
||||
<?php if ( ! empty( $artist['thumbnail'] ) ) : ?>
|
||||
<img src="<?php echo esc_url( $artist['thumbnail'] ); ?>" alt="<?php echo esc_attr( $artist['name'] ); ?>">
|
||||
<?php else : ?>
|
||||
<div class="placeholder-thumbnail">
|
||||
<span class="dashicons dashicons-<?php echo 'band' === $artist['type'] ? 'groups' : 'admin-users'; ?>"></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<h4 class="item-title">
|
||||
<a href="<?php echo esc_url( $artist['permalink'] ); ?>"><?php echo esc_html( $artist['name'] ); ?></a>
|
||||
</h4>
|
||||
<p class="item-type">
|
||||
<?php echo 'band' === $artist['type'] ? esc_html__( 'Band', 'wp-fedistream' ) : esc_html__( 'Artist', 'wp-fedistream' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button class="unfollow-btn" data-artist-id="<?php echo esc_attr( $artist['id'] ); ?>" title="<?php esc_attr_e( 'Unfollow', 'wp-fedistream' ); ?>">
|
||||
<span class="dashicons dashicons-minus"></span>
|
||||
<span class="button-text"><?php esc_html_e( 'Unfollow', 'wp-fedistream' ); ?></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a history item.
|
||||
*
|
||||
* @param array $track Track data.
|
||||
* @return string
|
||||
*/
|
||||
public static function render_history_item( array $track ): string {
|
||||
ob_start();
|
||||
?>
|
||||
<div class="library-item history-item" data-id="<?php echo esc_attr( $track['id'] ); ?>">
|
||||
<div class="item-thumbnail">
|
||||
<?php if ( ! empty( $track['thumbnail'] ) ) : ?>
|
||||
<img src="<?php echo esc_url( $track['thumbnail'] ); ?>" alt="<?php echo esc_attr( $track['title'] ); ?>">
|
||||
<?php else : ?>
|
||||
<div class="placeholder-thumbnail">
|
||||
<span class="dashicons dashicons-format-audio"></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<button class="play-btn" data-track-id="<?php echo esc_attr( $track['id'] ); ?>">
|
||||
<span class="dashicons dashicons-controls-play"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<h4 class="item-title">
|
||||
<a href="<?php echo esc_url( $track['permalink'] ); ?>"><?php echo esc_html( $track['title'] ); ?></a>
|
||||
</h4>
|
||||
<?php if ( ! empty( $track['artist'] ) ) : ?>
|
||||
<p class="item-artist"><?php echo esc_html( $track['artist'] ); ?></p>
|
||||
<?php endif; ?>
|
||||
<p class="item-played">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: relative time */
|
||||
esc_html__( 'Played %s', 'wp-fedistream' ),
|
||||
esc_html( human_time_diff( strtotime( $track['played_at'] ), current_time( 'timestamp' ) ) . ' ' . __( 'ago', 'wp-fedistream' ) )
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
<div class="item-meta">
|
||||
<?php if ( ! empty( $track['duration'] ) ) : ?>
|
||||
<span class="item-duration"><?php echo esc_html( self::format_duration( $track['duration'] ) ); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration in seconds to MM:SS.
|
||||
*
|
||||
* @param int $seconds Duration in seconds.
|
||||
* @return string
|
||||
*/
|
||||
private static function format_duration( int $seconds ): string {
|
||||
$minutes = floor( $seconds / 60 );
|
||||
$secs = $seconds % 60;
|
||||
|
||||
return sprintf( '%d:%02d', $minutes, $secs );
|
||||
}
|
||||
}
|
||||
828
includes/User/Notifications.php
Normal file
828
includes/User/Notifications.php
Normal file
@@ -0,0 +1,828 @@
|
||||
<?php
|
||||
/**
|
||||
* User Notifications class.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\User;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles user notifications (in-app and email).
|
||||
*/
|
||||
class Notifications {
|
||||
|
||||
/**
|
||||
* Notification types.
|
||||
*/
|
||||
const TYPE_NEW_RELEASE = 'new_release';
|
||||
const TYPE_NEW_FOLLOWER = 'new_follower';
|
||||
const TYPE_FEDIVERSE_LIKE = 'fediverse_like';
|
||||
const TYPE_FEDIVERSE_BOOST = 'fediverse_boost';
|
||||
const TYPE_PLAYLIST_ADDED = 'playlist_added';
|
||||
const TYPE_PURCHASE = 'purchase';
|
||||
const TYPE_SYSTEM = 'system';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
// AJAX handlers.
|
||||
add_action( 'wp_ajax_fedistream_get_notifications', array( $this, 'ajax_get_notifications' ) );
|
||||
add_action( 'wp_ajax_fedistream_mark_notification_read', array( $this, 'ajax_mark_read' ) );
|
||||
add_action( 'wp_ajax_fedistream_mark_all_notifications_read', array( $this, 'ajax_mark_all_read' ) );
|
||||
add_action( 'wp_ajax_fedistream_delete_notification', array( $this, 'ajax_delete' ) );
|
||||
|
||||
// Notification triggers.
|
||||
add_action( 'fedistream_album_published', array( $this, 'notify_new_release' ), 10, 1 );
|
||||
add_action( 'fedistream_track_published', array( $this, 'notify_new_track' ), 10, 1 );
|
||||
add_action( 'fedistream_artist_followed', array( $this, 'notify_artist_followed' ), 10, 2 );
|
||||
add_action( 'fedistream_activitypub_like_received', array( $this, 'notify_fediverse_like' ), 10, 2 );
|
||||
add_action( 'fedistream_activitypub_announce_received', array( $this, 'notify_fediverse_boost' ), 10, 2 );
|
||||
|
||||
// Email notifications.
|
||||
add_action( 'fedistream_notification_created', array( $this, 'maybe_send_email' ), 10, 2 );
|
||||
|
||||
// Admin bar notification count.
|
||||
add_action( 'admin_bar_menu', array( $this, 'add_notification_indicator' ), 100 );
|
||||
|
||||
// Enqueue scripts.
|
||||
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue notification scripts.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function enqueue_scripts(): void {
|
||||
if ( ! is_user_logged_in() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_script(
|
||||
'fedistream-notifications',
|
||||
WP_FEDISTREAM_URL . 'assets/js/notifications.js',
|
||||
array( 'jquery' ),
|
||||
WP_FEDISTREAM_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
wp_localize_script(
|
||||
'fedistream-notifications',
|
||||
'fedistreamNotifications',
|
||||
array(
|
||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||
'nonce' => wp_create_nonce( 'fedistream_notifications' ),
|
||||
'pollInterval' => 60000, // 1 minute.
|
||||
'i18n' => array(
|
||||
'noNotifications' => __( 'No notifications', 'wp-fedistream' ),
|
||||
'markAllRead' => __( 'Mark all as read', 'wp-fedistream' ),
|
||||
'viewAll' => __( 'View all notifications', 'wp-fedistream' ),
|
||||
'justNow' => __( 'Just now', 'wp-fedistream' ),
|
||||
'error' => __( 'An error occurred.', 'wp-fedistream' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a notification.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @param string $type Notification type.
|
||||
* @param string $title Notification title.
|
||||
* @param string $message Notification message.
|
||||
* @param array $data Additional data.
|
||||
* @return int|false Notification ID or false on failure.
|
||||
*/
|
||||
public static function create( int $user_id, string $type, string $title, string $message, array $data = array() ) {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_notifications';
|
||||
|
||||
$result = $wpdb->insert(
|
||||
$table,
|
||||
array(
|
||||
'user_id' => $user_id,
|
||||
'type' => $type,
|
||||
'title' => $title,
|
||||
'message' => $message,
|
||||
'data' => wp_json_encode( $data ),
|
||||
'is_read' => 0,
|
||||
'created_at' => current_time( 'mysql' ),
|
||||
),
|
||||
array( '%d', '%s', '%s', '%s', '%s', '%d', '%s' )
|
||||
);
|
||||
|
||||
if ( $result ) {
|
||||
$notification_id = $wpdb->insert_id;
|
||||
|
||||
/**
|
||||
* Fires when a notification is created.
|
||||
*
|
||||
* @param int $notification_id Notification ID.
|
||||
* @param array $notification Notification data.
|
||||
*/
|
||||
do_action(
|
||||
'fedistream_notification_created',
|
||||
$notification_id,
|
||||
array(
|
||||
'user_id' => $user_id,
|
||||
'type' => $type,
|
||||
'title' => $title,
|
||||
'message' => $message,
|
||||
'data' => $data,
|
||||
)
|
||||
);
|
||||
|
||||
return $notification_id;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get notifications for a user.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @param bool $unread_only Only get unread notifications.
|
||||
* @param int $limit Number of notifications to retrieve.
|
||||
* @param int $offset Offset for pagination.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_for_user( int $user_id, bool $unread_only = false, int $limit = 20, int $offset = 0 ): array {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_notifications';
|
||||
$where = 'WHERE user_id = %d';
|
||||
$params = array( $user_id );
|
||||
|
||||
if ( $unread_only ) {
|
||||
$where .= ' AND is_read = 0';
|
||||
}
|
||||
|
||||
$params[] = $limit;
|
||||
$params[] = $offset;
|
||||
|
||||
$notifications = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$table} {$where} ORDER BY created_at DESC LIMIT %d OFFSET %d",
|
||||
...$params
|
||||
)
|
||||
);
|
||||
|
||||
$result = array();
|
||||
|
||||
foreach ( $notifications as $notification ) {
|
||||
$result[] = array(
|
||||
'id' => (int) $notification->id,
|
||||
'type' => $notification->type,
|
||||
'title' => $notification->title,
|
||||
'message' => $notification->message,
|
||||
'data' => json_decode( $notification->data, true ) ?: array(),
|
||||
'is_read' => (bool) $notification->is_read,
|
||||
'created_at' => $notification->created_at,
|
||||
'read_at' => $notification->read_at,
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unread count for a user.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @return int
|
||||
*/
|
||||
public static function get_unread_count( int $user_id ): int {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_notifications';
|
||||
|
||||
$count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE user_id = %d AND is_read = 0",
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
|
||||
return (int) $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a notification as read.
|
||||
*
|
||||
* @param int $notification_id Notification ID.
|
||||
* @param int $user_id User ID (for verification).
|
||||
* @return bool
|
||||
*/
|
||||
public static function mark_read( int $notification_id, int $user_id ): bool {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_notifications';
|
||||
|
||||
$result = $wpdb->update(
|
||||
$table,
|
||||
array(
|
||||
'is_read' => 1,
|
||||
'read_at' => current_time( 'mysql' ),
|
||||
),
|
||||
array(
|
||||
'id' => $notification_id,
|
||||
'user_id' => $user_id,
|
||||
),
|
||||
array( '%d', '%s' ),
|
||||
array( '%d', '%d' )
|
||||
);
|
||||
|
||||
return false !== $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all notifications as read for a user.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @return bool
|
||||
*/
|
||||
public static function mark_all_read( int $user_id ): bool {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_notifications';
|
||||
|
||||
$result = $wpdb->update(
|
||||
$table,
|
||||
array(
|
||||
'is_read' => 1,
|
||||
'read_at' => current_time( 'mysql' ),
|
||||
),
|
||||
array(
|
||||
'user_id' => $user_id,
|
||||
'is_read' => 0,
|
||||
),
|
||||
array( '%d', '%s' ),
|
||||
array( '%d', '%d' )
|
||||
);
|
||||
|
||||
return false !== $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a notification.
|
||||
*
|
||||
* @param int $notification_id Notification ID.
|
||||
* @param int $user_id User ID (for verification).
|
||||
* @return bool
|
||||
*/
|
||||
public static function delete( int $notification_id, int $user_id ): bool {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_notifications';
|
||||
|
||||
$result = $wpdb->delete(
|
||||
$table,
|
||||
array(
|
||||
'id' => $notification_id,
|
||||
'user_id' => $user_id,
|
||||
),
|
||||
array( '%d', '%d' )
|
||||
);
|
||||
|
||||
return (bool) $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Get notifications.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ajax_get_notifications(): void {
|
||||
check_ajax_referer( 'fedistream_notifications', 'nonce' );
|
||||
|
||||
if ( ! is_user_logged_in() ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Not logged in.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$unread_only = isset( $_POST['unread_only'] ) && $_POST['unread_only'];
|
||||
$limit = isset( $_POST['limit'] ) ? absint( $_POST['limit'] ) : 20;
|
||||
$offset = isset( $_POST['offset'] ) ? absint( $_POST['offset'] ) : 0;
|
||||
|
||||
$notifications = self::get_for_user( $user_id, $unread_only, $limit, $offset );
|
||||
$unread_count = self::get_unread_count( $user_id );
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'notifications' => $notifications,
|
||||
'unread_count' => $unread_count,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Mark notification as read.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ajax_mark_read(): void {
|
||||
check_ajax_referer( 'fedistream_notifications', 'nonce' );
|
||||
|
||||
if ( ! is_user_logged_in() ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Not logged in.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
$notification_id = isset( $_POST['notification_id'] ) ? absint( $_POST['notification_id'] ) : 0;
|
||||
|
||||
if ( ! $notification_id ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Invalid notification.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$result = self::mark_read( $notification_id, $user_id );
|
||||
|
||||
if ( $result ) {
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'unread_count' => self::get_unread_count( $user_id ),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
wp_send_json_error( array( 'message' => __( 'Failed to update notification.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Mark all notifications as read.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ajax_mark_all_read(): void {
|
||||
check_ajax_referer( 'fedistream_notifications', 'nonce' );
|
||||
|
||||
if ( ! is_user_logged_in() ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Not logged in.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$result = self::mark_all_read( $user_id );
|
||||
|
||||
if ( $result ) {
|
||||
wp_send_json_success( array( 'unread_count' => 0 ) );
|
||||
} else {
|
||||
wp_send_json_error( array( 'message' => __( 'Failed to update notifications.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Delete notification.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ajax_delete(): void {
|
||||
check_ajax_referer( 'fedistream_notifications', 'nonce' );
|
||||
|
||||
if ( ! is_user_logged_in() ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Not logged in.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
$notification_id = isset( $_POST['notification_id'] ) ? absint( $_POST['notification_id'] ) : 0;
|
||||
|
||||
if ( ! $notification_id ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Invalid notification.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$result = self::delete( $notification_id, $user_id );
|
||||
|
||||
if ( $result ) {
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'unread_count' => self::get_unread_count( $user_id ),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
wp_send_json_error( array( 'message' => __( 'Failed to delete notification.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify followers of a new album release.
|
||||
*
|
||||
* @param int $album_id Album post ID.
|
||||
* @return void
|
||||
*/
|
||||
public function notify_new_release( int $album_id ): void {
|
||||
$album = get_post( $album_id );
|
||||
if ( ! $album ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$artist_id = get_post_meta( $album_id, '_fedistream_album_artist', true );
|
||||
if ( ! $artist_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$artist = get_post( $artist_id );
|
||||
if ( ! $artist ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all local followers of this artist.
|
||||
$followers = $this->get_artist_local_followers( $artist_id );
|
||||
|
||||
foreach ( $followers as $user_id ) {
|
||||
self::create(
|
||||
$user_id,
|
||||
self::TYPE_NEW_RELEASE,
|
||||
sprintf(
|
||||
/* translators: %s: artist name */
|
||||
__( 'New release from %s', 'wp-fedistream' ),
|
||||
$artist->post_title
|
||||
),
|
||||
sprintf(
|
||||
/* translators: 1: artist name, 2: album title */
|
||||
__( '%1$s released a new album: %2$s', 'wp-fedistream' ),
|
||||
$artist->post_title,
|
||||
$album->post_title
|
||||
),
|
||||
array(
|
||||
'album_id' => $album_id,
|
||||
'artist_id' => $artist_id,
|
||||
'album_url' => get_permalink( $album ),
|
||||
'artist_url' => get_permalink( $artist ),
|
||||
'thumbnail' => get_the_post_thumbnail_url( $album, 'thumbnail' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify followers of a new track.
|
||||
*
|
||||
* @param int $track_id Track post ID.
|
||||
* @return void
|
||||
*/
|
||||
public function notify_new_track( int $track_id ): void {
|
||||
$track = get_post( $track_id );
|
||||
if ( ! $track ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get artist from track or album.
|
||||
$artist_ids = get_post_meta( $track_id, '_fedistream_artist_ids', true );
|
||||
if ( empty( $artist_ids ) ) {
|
||||
$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;
|
||||
$artist_ids = $artist_id ? array( $artist_id ) : array();
|
||||
}
|
||||
|
||||
if ( empty( $artist_ids ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$artist_id = $artist_ids[0];
|
||||
$artist = get_post( $artist_id );
|
||||
if ( ! $artist ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only notify for single releases (tracks without album).
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
if ( $album_id ) {
|
||||
return; // Album release handles notification.
|
||||
}
|
||||
|
||||
$followers = $this->get_artist_local_followers( $artist_id );
|
||||
|
||||
foreach ( $followers as $user_id ) {
|
||||
self::create(
|
||||
$user_id,
|
||||
self::TYPE_NEW_RELEASE,
|
||||
sprintf(
|
||||
/* translators: %s: artist name */
|
||||
__( 'New track from %s', 'wp-fedistream' ),
|
||||
$artist->post_title
|
||||
),
|
||||
sprintf(
|
||||
/* translators: 1: artist name, 2: track title */
|
||||
__( '%1$s released a new track: %2$s', 'wp-fedistream' ),
|
||||
$artist->post_title,
|
||||
$track->post_title
|
||||
),
|
||||
array(
|
||||
'track_id' => $track_id,
|
||||
'artist_id' => $artist_id,
|
||||
'track_url' => get_permalink( $track ),
|
||||
'artist_url' => get_permalink( $artist ),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify artist when they get a new local follower.
|
||||
*
|
||||
* @param int $user_id User ID who followed.
|
||||
* @param int $artist_id Artist post ID.
|
||||
* @return void
|
||||
*/
|
||||
public function notify_artist_followed( int $user_id, int $artist_id ): void {
|
||||
$artist = get_post( $artist_id );
|
||||
if ( ! $artist ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the artist's WordPress user ID.
|
||||
$artist_user_id = get_post_meta( $artist_id, '_fedistream_user_id', true );
|
||||
if ( ! $artist_user_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$follower = get_user_by( 'id', $user_id );
|
||||
if ( ! $follower ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::create(
|
||||
$artist_user_id,
|
||||
self::TYPE_NEW_FOLLOWER,
|
||||
__( 'New follower', 'wp-fedistream' ),
|
||||
sprintf(
|
||||
/* translators: %s: follower display name */
|
||||
__( '%s started following you', 'wp-fedistream' ),
|
||||
$follower->display_name
|
||||
),
|
||||
array(
|
||||
'follower_id' => $user_id,
|
||||
'follower_name' => $follower->display_name,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify of a Fediverse like.
|
||||
*
|
||||
* @param int $content_id Content post ID.
|
||||
* @param array $actor Actor data.
|
||||
* @return void
|
||||
*/
|
||||
public function notify_fediverse_like( int $content_id, array $actor ): void {
|
||||
$post = get_post( $content_id );
|
||||
if ( ! $post ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$artist_user_id = $this->get_content_owner_user_id( $content_id );
|
||||
if ( ! $artist_user_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$actor_name = $actor['name'] ?? $actor['preferredUsername'] ?? __( 'Someone', 'wp-fedistream' );
|
||||
|
||||
self::create(
|
||||
$artist_user_id,
|
||||
self::TYPE_FEDIVERSE_LIKE,
|
||||
__( 'New like from Fediverse', 'wp-fedistream' ),
|
||||
sprintf(
|
||||
/* translators: 1: actor name, 2: content title */
|
||||
__( '%1$s liked your %2$s', 'wp-fedistream' ),
|
||||
$actor_name,
|
||||
$post->post_title
|
||||
),
|
||||
array(
|
||||
'content_id' => $content_id,
|
||||
'content_type' => $post->post_type,
|
||||
'actor_uri' => $actor['id'] ?? '',
|
||||
'actor_name' => $actor_name,
|
||||
'actor_icon' => $actor['icon']['url'] ?? '',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify of a Fediverse boost/announce.
|
||||
*
|
||||
* @param int $content_id Content post ID.
|
||||
* @param array $actor Actor data.
|
||||
* @return void
|
||||
*/
|
||||
public function notify_fediverse_boost( int $content_id, array $actor ): void {
|
||||
$post = get_post( $content_id );
|
||||
if ( ! $post ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$artist_user_id = $this->get_content_owner_user_id( $content_id );
|
||||
if ( ! $artist_user_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$actor_name = $actor['name'] ?? $actor['preferredUsername'] ?? __( 'Someone', 'wp-fedistream' );
|
||||
|
||||
self::create(
|
||||
$artist_user_id,
|
||||
self::TYPE_FEDIVERSE_BOOST,
|
||||
__( 'New boost from Fediverse', 'wp-fedistream' ),
|
||||
sprintf(
|
||||
/* translators: 1: actor name, 2: content title */
|
||||
__( '%1$s boosted your %2$s', 'wp-fedistream' ),
|
||||
$actor_name,
|
||||
$post->post_title
|
||||
),
|
||||
array(
|
||||
'content_id' => $content_id,
|
||||
'content_type' => $post->post_type,
|
||||
'actor_uri' => $actor['id'] ?? '',
|
||||
'actor_name' => $actor_name,
|
||||
'actor_icon' => $actor['icon']['url'] ?? '',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe send email notification.
|
||||
*
|
||||
* @param int $notification_id Notification ID.
|
||||
* @param array $notification Notification data.
|
||||
* @return void
|
||||
*/
|
||||
public function maybe_send_email( int $notification_id, array $notification ): void {
|
||||
$user_id = $notification['user_id'];
|
||||
$type = $notification['type'];
|
||||
|
||||
// Check user preference for email notifications.
|
||||
$email_enabled = get_user_meta( $user_id, 'fedistream_email_notifications', true );
|
||||
if ( '0' === $email_enabled ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check specific notification type preference.
|
||||
$type_enabled = get_user_meta( $user_id, 'fedistream_email_' . $type, true );
|
||||
if ( '0' === $type_enabled ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = get_user_by( 'id', $user_id );
|
||||
if ( ! $user || ! $user->user_email ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$subject = sprintf(
|
||||
/* translators: 1: site name, 2: notification title */
|
||||
__( '[%1$s] %2$s', 'wp-fedistream' ),
|
||||
get_bloginfo( 'name' ),
|
||||
$notification['title']
|
||||
);
|
||||
|
||||
$message = $this->build_email_message( $notification );
|
||||
|
||||
$headers = array(
|
||||
'Content-Type: text/html; charset=UTF-8',
|
||||
);
|
||||
|
||||
wp_mail( $user->user_email, $subject, $message, $headers );
|
||||
}
|
||||
|
||||
/**
|
||||
* Build email message HTML.
|
||||
*
|
||||
* @param array $notification Notification data.
|
||||
* @return string
|
||||
*/
|
||||
private function build_email_message( array $notification ): string {
|
||||
$site_name = get_bloginfo( 'name' );
|
||||
$site_url = home_url();
|
||||
|
||||
$html = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>';
|
||||
$html .= '<div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">';
|
||||
$html .= '<h2 style="color: #333;">' . esc_html( $notification['title'] ) . '</h2>';
|
||||
$html .= '<p style="color: #666; font-size: 16px;">' . esc_html( $notification['message'] ) . '</p>';
|
||||
|
||||
// Add action link if available.
|
||||
$data = $notification['data'];
|
||||
$link = '';
|
||||
|
||||
if ( ! empty( $data['album_url'] ) ) {
|
||||
$link = $data['album_url'];
|
||||
} elseif ( ! empty( $data['track_url'] ) ) {
|
||||
$link = $data['track_url'];
|
||||
} elseif ( ! empty( $data['artist_url'] ) ) {
|
||||
$link = $data['artist_url'];
|
||||
}
|
||||
|
||||
if ( $link ) {
|
||||
$html .= '<p><a href="' . esc_url( $link ) . '" style="display: inline-block; padding: 10px 20px; background: #0073aa; color: #fff; text-decoration: none; border-radius: 4px;">' . esc_html__( 'View Details', 'wp-fedistream' ) . '</a></p>';
|
||||
}
|
||||
|
||||
$html .= '<hr style="margin: 30px 0; border: none; border-top: 1px solid #eee;">';
|
||||
$html .= '<p style="color: #999; font-size: 12px;">' . sprintf(
|
||||
/* translators: %s: site name */
|
||||
esc_html__( 'This email was sent by %s.', 'wp-fedistream' ),
|
||||
'<a href="' . esc_url( $site_url ) . '">' . esc_html( $site_name ) . '</a>'
|
||||
) . '</p>';
|
||||
$html .= '</div></body></html>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add notification indicator to admin bar.
|
||||
*
|
||||
* @param \WP_Admin_Bar $admin_bar Admin bar instance.
|
||||
* @return void
|
||||
*/
|
||||
public function add_notification_indicator( \WP_Admin_Bar $admin_bar ): void {
|
||||
if ( ! is_user_logged_in() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$unread_count = self::get_unread_count( $user_id );
|
||||
|
||||
$title = '<span class="ab-icon dashicons dashicons-bell"></span>';
|
||||
if ( $unread_count > 0 ) {
|
||||
$title .= '<span class="fedistream-notification-count">' . esc_html( $unread_count ) . '</span>';
|
||||
}
|
||||
|
||||
$admin_bar->add_node(
|
||||
array(
|
||||
'id' => 'fedistream-notifications',
|
||||
'title' => $title,
|
||||
'href' => '#',
|
||||
'meta' => array(
|
||||
'class' => 'fedistream-notifications-menu',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get local followers for an artist.
|
||||
*
|
||||
* @param int $artist_id Artist post ID.
|
||||
* @return array User IDs.
|
||||
*/
|
||||
private function get_artist_local_followers( int $artist_id ): array {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_user_follows';
|
||||
|
||||
$user_ids = $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"SELECT user_id FROM {$table} WHERE artist_id = %d",
|
||||
$artist_id
|
||||
)
|
||||
);
|
||||
|
||||
return array_map( 'intval', $user_ids );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the WordPress user ID who owns a piece of content.
|
||||
*
|
||||
* @param int $content_id Content post ID.
|
||||
* @return int|null User ID or null.
|
||||
*/
|
||||
private function get_content_owner_user_id( int $content_id ): ?int {
|
||||
$post = get_post( $content_id );
|
||||
if ( ! $post ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For tracks, get artist.
|
||||
if ( 'fedistream_track' === $post->post_type ) {
|
||||
$artist_ids = get_post_meta( $content_id, '_fedistream_artist_ids', true );
|
||||
if ( ! empty( $artist_ids ) ) {
|
||||
$artist_id = $artist_ids[0];
|
||||
return (int) get_post_meta( $artist_id, '_fedistream_user_id', true ) ?: null;
|
||||
}
|
||||
|
||||
$album_id = get_post_meta( $content_id, '_fedistream_album_id', true );
|
||||
$artist_id = $album_id ? get_post_meta( $album_id, '_fedistream_album_artist', true ) : 0;
|
||||
if ( $artist_id ) {
|
||||
return (int) get_post_meta( $artist_id, '_fedistream_user_id', true ) ?: null;
|
||||
}
|
||||
}
|
||||
|
||||
// For albums, get artist.
|
||||
if ( 'fedistream_album' === $post->post_type ) {
|
||||
$artist_id = get_post_meta( $content_id, '_fedistream_album_artist', true );
|
||||
if ( $artist_id ) {
|
||||
return (int) get_post_meta( $artist_id, '_fedistream_user_id', true ) ?: null;
|
||||
}
|
||||
}
|
||||
|
||||
// For artists.
|
||||
if ( 'fedistream_artist' === $post->post_type ) {
|
||||
return (int) get_post_meta( $content_id, '_fedistream_user_id', true ) ?: null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
499
includes/WooCommerce/AlbumProduct.php
Normal file
499
includes/WooCommerce/AlbumProduct.php
Normal file
@@ -0,0 +1,499 @@
|
||||
<?php
|
||||
/**
|
||||
* Album Product Type for WooCommerce.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\WooCommerce;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* FediStream Album product type.
|
||||
*
|
||||
* Digital product representing a FediStream album.
|
||||
*/
|
||||
class AlbumProduct extends \WC_Product {
|
||||
|
||||
/**
|
||||
* Product type.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $product_type = 'fedistream_album';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param int|\WC_Product|object $product Product ID or object.
|
||||
*/
|
||||
public function __construct( $product = 0 ) {
|
||||
parent::__construct( $product );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_type(): string {
|
||||
return 'fedistream_album';
|
||||
}
|
||||
|
||||
/**
|
||||
* Albums are virtual products.
|
||||
*
|
||||
* @param string $context View or edit context.
|
||||
* @return bool
|
||||
*/
|
||||
public function get_virtual( $context = 'view' ): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Albums are downloadable products.
|
||||
*
|
||||
* @param string $context View or edit context.
|
||||
* @return bool
|
||||
*/
|
||||
public function get_downloadable( $context = 'view' ): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the linked album ID.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_linked_album_id(): int {
|
||||
return (int) $this->get_meta( '_fedistream_linked_album', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the linked album post.
|
||||
*
|
||||
* @return \WP_Post|null
|
||||
*/
|
||||
public function get_linked_album(): ?\WP_Post {
|
||||
$album_id = $this->get_linked_album_id();
|
||||
|
||||
if ( ! $album_id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$album = get_post( $album_id );
|
||||
|
||||
if ( ! $album || 'fedistream_album' !== $album->post_type ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $album;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pricing type.
|
||||
*
|
||||
* @return string fixed, pwyw, or nyp
|
||||
*/
|
||||
public function get_pricing_type(): string {
|
||||
return $this->get_meta( '_fedistream_pricing_type', true ) ?: 'fixed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minimum price for PWYW.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function get_min_price(): float {
|
||||
return (float) $this->get_meta( '_fedistream_min_price', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get suggested price for PWYW.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function get_suggested_price(): float {
|
||||
return (float) $this->get_meta( '_fedistream_suggested_price', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if streaming is included.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function includes_streaming(): bool {
|
||||
return 'yes' === $this->get_meta( '_fedistream_include_streaming', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available download formats.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_available_formats(): array {
|
||||
$formats = $this->get_meta( '_fedistream_available_formats', true );
|
||||
|
||||
return is_array( $formats ) ? $formats : array( 'mp3' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tracks in this album.
|
||||
*
|
||||
* @return array Array of WP_Post objects.
|
||||
*/
|
||||
public function get_tracks(): array {
|
||||
$album_id = $this->get_linked_album_id();
|
||||
|
||||
if ( ! $album_id ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$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,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
return $tracks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track count.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_track_count(): int {
|
||||
$album_id = $this->get_linked_album_id();
|
||||
|
||||
if ( ! $album_id ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$count = get_post_meta( $album_id, '_fedistream_total_tracks', true );
|
||||
|
||||
if ( $count ) {
|
||||
return (int) $count;
|
||||
}
|
||||
|
||||
return count( $this->get_tracks() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total duration in seconds.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_total_duration(): int {
|
||||
$album_id = $this->get_linked_album_id();
|
||||
|
||||
if ( ! $album_id ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$duration = get_post_meta( $album_id, '_fedistream_total_duration', true );
|
||||
|
||||
if ( $duration ) {
|
||||
return (int) $duration;
|
||||
}
|
||||
|
||||
// Calculate from tracks.
|
||||
$tracks = $this->get_tracks();
|
||||
$duration = 0;
|
||||
|
||||
foreach ( $tracks as $track ) {
|
||||
$duration += (int) get_post_meta( $track->ID, '_fedistream_duration', true );
|
||||
}
|
||||
|
||||
return $duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatted duration.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_formatted_duration(): string {
|
||||
$seconds = $this->get_total_duration();
|
||||
|
||||
if ( ! $seconds ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$hours = floor( $seconds / 3600 );
|
||||
$mins = floor( ( $seconds % 3600 ) / 60 );
|
||||
$secs = $seconds % 60;
|
||||
|
||||
if ( $hours > 0 ) {
|
||||
return sprintf( '%d:%02d:%02d', $hours, $mins, $secs );
|
||||
}
|
||||
|
||||
return sprintf( '%d:%02d', $mins, $secs );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get artist name(s).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_artist_name(): string {
|
||||
$album_id = $this->get_linked_album_id();
|
||||
|
||||
if ( ! $album_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$artist_id = get_post_meta( $album_id, '_fedistream_album_artist', true );
|
||||
|
||||
if ( ! $artist_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$artist = get_post( $artist_id );
|
||||
|
||||
return $artist ? $artist->post_title : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get album artwork URL.
|
||||
*
|
||||
* @param string $size Image size.
|
||||
* @return string
|
||||
*/
|
||||
public function get_album_artwork( string $size = 'medium' ): string {
|
||||
$album_id = $this->get_linked_album_id();
|
||||
|
||||
if ( ! $album_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$thumbnail_id = get_post_thumbnail_id( $album_id );
|
||||
|
||||
if ( ! $thumbnail_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$image = wp_get_attachment_image_url( $thumbnail_id, $size );
|
||||
|
||||
return $image ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get release date.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_release_date(): string {
|
||||
$album_id = $this->get_linked_album_id();
|
||||
|
||||
if ( ! $album_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return get_post_meta( $album_id, '_fedistream_release_date', true ) ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get album type (album, ep, single, compilation).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_album_type(): string {
|
||||
$album_id = $this->get_linked_album_id();
|
||||
|
||||
if ( ! $album_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return get_post_meta( $album_id, '_fedistream_album_type', true ) ?: 'album';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get downloads for this product.
|
||||
*
|
||||
* Generates downloadable files based on available formats.
|
||||
*
|
||||
* @param string $context View or edit context.
|
||||
* @return array
|
||||
*/
|
||||
public function get_downloads( $context = 'view' ): array {
|
||||
$downloads = parent::get_downloads( $context );
|
||||
|
||||
// If no manual downloads set, generate from linked album.
|
||||
if ( empty( $downloads ) && $this->get_linked_album_id() ) {
|
||||
$downloads = $this->generate_album_downloads();
|
||||
}
|
||||
|
||||
return $downloads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate download files from linked album.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function generate_album_downloads(): array {
|
||||
$downloads = array();
|
||||
$tracks = $this->get_tracks();
|
||||
$formats = $this->get_available_formats();
|
||||
$album = $this->get_linked_album();
|
||||
|
||||
if ( empty( $tracks ) || ! $album ) {
|
||||
return $downloads;
|
||||
}
|
||||
|
||||
// For each format, create a download entry.
|
||||
foreach ( $formats as $format ) {
|
||||
$format_label = strtoupper( $format );
|
||||
|
||||
// Create album ZIP download entry.
|
||||
$download_id = 'album-' . $album->ID . '-' . $format;
|
||||
|
||||
$downloads[ $download_id ] = array(
|
||||
'id' => $download_id,
|
||||
'name' => sprintf(
|
||||
/* translators: 1: Album name, 2: Format name */
|
||||
__( '%1$s (%2$s)', 'wp-fedistream' ),
|
||||
$album->post_title,
|
||||
$format_label
|
||||
),
|
||||
'file' => add_query_arg(
|
||||
array(
|
||||
'fedistream_download' => 'album',
|
||||
'album_id' => $album->ID,
|
||||
'format' => $format,
|
||||
),
|
||||
home_url( '/' )
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return $downloads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if purchasable.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_purchasable(): bool {
|
||||
// Must have a linked album.
|
||||
if ( ! $this->get_linked_album_id() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check price for fixed pricing.
|
||||
if ( 'fixed' === $this->get_pricing_type() ) {
|
||||
return $this->get_price() !== '' && $this->get_price() >= 0;
|
||||
}
|
||||
|
||||
// PWYW and NYP are always purchasable.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get price HTML.
|
||||
*
|
||||
* @param string $price Price HTML.
|
||||
* @return string
|
||||
*/
|
||||
public function get_price_html( $price = '' ): string {
|
||||
$pricing_type = $this->get_pricing_type();
|
||||
|
||||
if ( 'nyp' === $pricing_type ) {
|
||||
return '<span class="fedistream-nyp-price">' . esc_html__( 'Name Your Price', 'wp-fedistream' ) . '</span>';
|
||||
}
|
||||
|
||||
if ( 'pwyw' === $pricing_type ) {
|
||||
$min_price = $this->get_min_price();
|
||||
$suggested = $this->get_suggested_price();
|
||||
|
||||
$html = '<span class="fedistream-pwyw-price">';
|
||||
|
||||
if ( $min_price > 0 ) {
|
||||
$html .= sprintf(
|
||||
/* translators: %s: Minimum price */
|
||||
esc_html__( 'From %s', 'wp-fedistream' ),
|
||||
wc_price( $min_price )
|
||||
);
|
||||
} else {
|
||||
$html .= esc_html__( 'Pay What You Want', 'wp-fedistream' );
|
||||
}
|
||||
|
||||
if ( $suggested > 0 ) {
|
||||
$html .= ' <span class="fedistream-suggested">';
|
||||
$html .= sprintf(
|
||||
/* translators: %s: Suggested price */
|
||||
esc_html__( '(Suggested: %s)', 'wp-fedistream' ),
|
||||
wc_price( $suggested )
|
||||
);
|
||||
$html .= '</span>';
|
||||
}
|
||||
|
||||
$html .= '</span>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
return parent::get_price_html( $price );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add to cart validation for PWYW products.
|
||||
*
|
||||
* @param bool $passed Validation passed.
|
||||
* @param int $product_id Product ID.
|
||||
* @param int $quantity Quantity.
|
||||
* @return bool
|
||||
*/
|
||||
public static function validate_add_to_cart( bool $passed, int $product_id, int $quantity ): bool {
|
||||
$product = wc_get_product( $product_id );
|
||||
|
||||
if ( ! $product || 'fedistream_album' !== $product->get_type() ) {
|
||||
return $passed;
|
||||
}
|
||||
|
||||
$pricing_type = $product->get_pricing_type();
|
||||
|
||||
if ( 'pwyw' === $pricing_type || 'nyp' === $pricing_type ) {
|
||||
// Check if custom price is set.
|
||||
$custom_price = isset( $_POST['fedistream_custom_price'] ) ? // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
wc_format_decimal( sanitize_text_field( wp_unslash( $_POST['fedistream_custom_price'] ) ) ) : // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
0;
|
||||
|
||||
$min_price = $product->get_min_price();
|
||||
|
||||
if ( 'pwyw' === $pricing_type && $custom_price < $min_price ) {
|
||||
wc_add_notice(
|
||||
sprintf(
|
||||
/* translators: %s: Minimum price */
|
||||
__( 'Please enter at least %s', 'wp-fedistream' ),
|
||||
wc_price( $min_price )
|
||||
),
|
||||
'error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Store custom price in session for cart.
|
||||
WC()->session->set( 'fedistream_custom_price_' . $product_id, $custom_price );
|
||||
}
|
||||
|
||||
return $passed;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
738
includes/WooCommerce/Integration.php
Normal file
738
includes/WooCommerce/Integration.php
Normal file
@@ -0,0 +1,738 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Integration.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\WooCommerce;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main WooCommerce integration class.
|
||||
*/
|
||||
class Integration {
|
||||
|
||||
/**
|
||||
* Whether WooCommerce is active.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private bool $woocommerce_active = false;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'plugins_loaded', array( $this, 'check_woocommerce' ), 5 );
|
||||
add_action( 'plugins_loaded', array( $this, 'init' ), 20 );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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',
|
||||
)
|
||||
);
|
||||
|
||||
?>
|
||||
<div id="fedistream_product_data" class="panel woocommerce_options_panel">
|
||||
<div class="options_group show_if_fedistream_album">
|
||||
<p class="form-field">
|
||||
<label for="_fedistream_linked_album"><?php esc_html_e( 'Linked Album', 'wp-fedistream' ); ?></label>
|
||||
<select id="_fedistream_linked_album" name="_fedistream_linked_album" class="wc-enhanced-select" style="width: 50%;">
|
||||
<option value=""><?php esc_html_e( 'Select an album...', 'wp-fedistream' ); ?></option>
|
||||
<?php foreach ( $albums as $album ) : ?>
|
||||
<option value="<?php echo esc_attr( $album->ID ); ?>" <?php selected( $linked_album, $album->ID ); ?>>
|
||||
<?php echo esc_html( $album->post_title ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php echo wc_help_tip( __( 'Select the FediStream album this product represents.', 'wp-fedistream' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="options_group show_if_fedistream_track">
|
||||
<p class="form-field">
|
||||
<label for="_fedistream_linked_track"><?php esc_html_e( 'Linked Track', 'wp-fedistream' ); ?></label>
|
||||
<select id="_fedistream_linked_track" name="_fedistream_linked_track" class="wc-enhanced-select" style="width: 50%;">
|
||||
<option value=""><?php esc_html_e( 'Select a track...', 'wp-fedistream' ); ?></option>
|
||||
<?php foreach ( $tracks as $track ) : ?>
|
||||
<option value="<?php echo esc_attr( $track->ID ); ?>" <?php selected( $linked_track, $track->ID ); ?>>
|
||||
<?php echo esc_html( $track->post_title ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php echo wc_help_tip( __( 'Select the FediStream track this product represents.', 'wp-fedistream' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="options_group">
|
||||
<p class="form-field">
|
||||
<label for="_fedistream_pricing_type"><?php esc_html_e( 'Pricing Type', 'wp-fedistream' ); ?></label>
|
||||
<select id="_fedistream_pricing_type" name="_fedistream_pricing_type" class="wc-enhanced-select" style="width: 50%;">
|
||||
<option value="fixed" <?php selected( $pricing_type, 'fixed' ); ?>><?php esc_html_e( 'Fixed Price', 'wp-fedistream' ); ?></option>
|
||||
<option value="pwyw" <?php selected( $pricing_type, 'pwyw' ); ?>><?php esc_html_e( 'Pay What You Want', 'wp-fedistream' ); ?></option>
|
||||
<option value="nyp" <?php selected( $pricing_type, 'nyp' ); ?>><?php esc_html_e( 'Name Your Price (Free+)', 'wp-fedistream' ); ?></option>
|
||||
</select>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="options_group fedistream-pwyw-options">
|
||||
<?php
|
||||
woocommerce_wp_text_input(
|
||||
array(
|
||||
'id' => '_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,
|
||||
)
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
|
||||
<div class="options_group">
|
||||
<?php
|
||||
woocommerce_wp_checkbox(
|
||||
array(
|
||||
'id' => '_fedistream_include_streaming',
|
||||
'label' => __( 'Include Streaming', 'wp-fedistream' ),
|
||||
'description' => __( 'Purchase unlocks full-quality streaming access.', 'wp-fedistream' ),
|
||||
'value' => $include_streaming,
|
||||
)
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="fedistream_formats_data" class="panel woocommerce_options_panel">
|
||||
<div class="options_group">
|
||||
<p class="form-field">
|
||||
<label><?php esc_html_e( 'Available Formats', 'wp-fedistream' ); ?></label>
|
||||
<span class="fedistream-format-checkboxes">
|
||||
<?php
|
||||
$formats = array(
|
||||
'mp3' => '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 );
|
||||
?>
|
||||
<label style="display: block; margin-bottom: 5px;">
|
||||
<input type="checkbox" name="_fedistream_available_formats[]" value="<?php echo esc_attr( $format ); ?>" <?php checked( $checked ); ?>>
|
||||
<?php echo esc_html( $label ); ?>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</span>
|
||||
</p>
|
||||
<p class="description" style="margin-left: 150px;">
|
||||
<?php esc_html_e( 'Select which audio formats customers can download after purchase.', 'wp-fedistream' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
jQuery(function($) {
|
||||
function togglePricingOptions() {
|
||||
var type = $('#_fedistream_pricing_type').val();
|
||||
if (type === 'pwyw' || type === 'nyp') {
|
||||
$('.fedistream-pwyw-options').show();
|
||||
} else {
|
||||
$('.fedistream-pwyw-options').hide();
|
||||
}
|
||||
}
|
||||
|
||||
$('#_fedistream_pricing_type').on('change', togglePricingOptions);
|
||||
togglePricingOptions();
|
||||
|
||||
// Show/hide tabs based on product type.
|
||||
$('input#_virtual, input#_downloadable').on('change', function() {
|
||||
var type = $('select#product-type').val();
|
||||
if (type === 'fedistream_album' || type === 'fedistream_track') {
|
||||
$('input#_virtual').prop('checked', true);
|
||||
$('input#_downloadable').prop('checked', true);
|
||||
}
|
||||
});
|
||||
|
||||
$('select#product-type').on('change', function() {
|
||||
var type = $(this).val();
|
||||
if (type === 'fedistream_album' || type === 'fedistream_track') {
|
||||
$('input#_virtual').prop('checked', true).trigger('change');
|
||||
$('input#_downloadable').prop('checked', true).trigger('change');
|
||||
}
|
||||
}).trigger('change');
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save product meta.
|
||||
*
|
||||
* @param int $product_id Product ID.
|
||||
* @return void
|
||||
*/
|
||||
public function save_product_meta( int $product_id ): void {
|
||||
// Linked content.
|
||||
if ( isset( $_POST['_fedistream_linked_album'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
update_post_meta( $product_id, '_fedistream_linked_album', absint( $_POST['_fedistream_linked_album'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
}
|
||||
|
||||
if ( isset( $_POST['_fedistream_linked_track'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
update_post_meta( $product_id, '_fedistream_linked_track', absint( $_POST['_fedistream_linked_track'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
}
|
||||
|
||||
// Pricing options.
|
||||
if ( isset( $_POST['_fedistream_pricing_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
update_post_meta( $product_id, '_fedistream_pricing_type', sanitize_text_field( wp_unslash( $_POST['_fedistream_pricing_type'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
}
|
||||
|
||||
if ( isset( $_POST['_fedistream_min_price'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
update_post_meta( $product_id, '_fedistream_min_price', wc_format_decimal( sanitize_text_field( wp_unslash( $_POST['_fedistream_min_price'] ) ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
}
|
||||
|
||||
if ( isset( $_POST['_fedistream_suggested_price'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
update_post_meta( $product_id, '_fedistream_suggested_price', wc_format_decimal( sanitize_text_field( wp_unslash( $_POST['_fedistream_suggested_price'] ) ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
}
|
||||
|
||||
// Streaming access.
|
||||
$include_streaming = isset( $_POST['_fedistream_include_streaming'] ) ? 'yes' : 'no'; // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
update_post_meta( $product_id, '_fedistream_include_streaming', $include_streaming );
|
||||
|
||||
// Available formats.
|
||||
if ( isset( $_POST['_fedistream_available_formats'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
$formats = array_map( 'sanitize_text_field', wp_unslash( $_POST['_fedistream_available_formats'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
update_post_meta( $product_id, '_fedistream_available_formats', $formats );
|
||||
} else {
|
||||
update_post_meta( $product_id, '_fedistream_available_formats', array() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display track preview on product page.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function display_track_preview(): void {
|
||||
global $product;
|
||||
|
||||
if ( ! $product ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$product_type = $product->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 );
|
||||
|
||||
?>
|
||||
<div class="fedistream-product-preview">
|
||||
<h4><?php esc_html_e( 'Preview', 'wp-fedistream' ); ?></h4>
|
||||
<div class="fedistream-mini-player" data-track-id="<?php echo esc_attr( $track_id ); ?>">
|
||||
<button class="fedistream-preview-play" type="button" aria-label="<?php esc_attr_e( 'Play preview', 'wp-fedistream' ); ?>">
|
||||
<span class="dashicons dashicons-controls-play"></span>
|
||||
</button>
|
||||
<div class="fedistream-preview-progress">
|
||||
<div class="fedistream-preview-progress-bar"></div>
|
||||
</div>
|
||||
<?php if ( $duration ) : ?>
|
||||
<span class="fedistream-preview-duration"><?php echo esc_html( gmdate( 'i:s', $duration ) ); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render album preview tracklist.
|
||||
*
|
||||
* @param int $album_id Album ID.
|
||||
* @return void
|
||||
*/
|
||||
private function render_album_preview( int $album_id ): void {
|
||||
$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 ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
?>
|
||||
<div class="fedistream-product-tracklist">
|
||||
<h4><?php esc_html_e( 'Tracklist', 'wp-fedistream' ); ?></h4>
|
||||
<ol class="fedistream-album-tracks">
|
||||
<?php foreach ( $tracks as $track ) : ?>
|
||||
<?php
|
||||
$duration = get_post_meta( $track->ID, '_fedistream_duration', true );
|
||||
?>
|
||||
<li class="fedistream-album-track" data-track-id="<?php echo esc_attr( $track->ID ); ?>">
|
||||
<span class="fedistream-track-title"><?php echo esc_html( $track->post_title ); ?></span>
|
||||
<?php if ( $duration ) : ?>
|
||||
<span class="fedistream-track-duration"><?php echo esc_html( gmdate( 'i:s', $duration ) ); ?></span>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ol>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant streaming/download access on purchase completion.
|
||||
*
|
||||
* @param int $order_id Order ID.
|
||||
* @return void
|
||||
*/
|
||||
public function grant_access_on_purchase( int $order_id ): void {
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
if ( ! $order ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$customer_id = $order->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 '<a href="' . esc_url( get_edit_post_link( $album_id ) ) . '">' . esc_html( $album->post_title ) . '</a>';
|
||||
}
|
||||
} else {
|
||||
echo '<span class="na">–</span>';
|
||||
}
|
||||
} 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 '<a href="' . esc_url( get_edit_post_link( $track_id ) ) . '">' . esc_html( $track->post_title ) . '</a>';
|
||||
}
|
||||
} else {
|
||||
echo '<span class="na">–</span>';
|
||||
}
|
||||
} else {
|
||||
echo '<span class="na">–</span>';
|
||||
}
|
||||
}
|
||||
}
|
||||
416
includes/WooCommerce/StreamingAccess.php
Normal file
416
includes/WooCommerce/StreamingAccess.php
Normal file
@@ -0,0 +1,416 @@
|
||||
<?php
|
||||
/**
|
||||
* Streaming Access Control for WooCommerce.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\WooCommerce;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Controls streaming access based on purchases.
|
||||
*/
|
||||
class StreamingAccess {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
// Filter audio URL access.
|
||||
add_filter( 'fedistream_can_stream_track', array( $this, 'can_stream_track' ), 10, 3 );
|
||||
|
||||
// Add streaming access check to AJAX handler.
|
||||
add_filter( 'fedistream_track_data', array( $this, 'filter_track_data' ), 10, 2 );
|
||||
|
||||
// Handle preview access.
|
||||
add_action( 'init', array( $this, 'handle_preview_request' ) );
|
||||
|
||||
// Add purchase buttons to track display.
|
||||
add_action( 'fedistream_after_track_player', array( $this, 'add_purchase_button' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user can stream a track.
|
||||
*
|
||||
* @param bool $can_stream Default access (true).
|
||||
* @param int $track_id Track ID.
|
||||
* @param int $user_id User ID (0 for guests).
|
||||
* @return bool
|
||||
*/
|
||||
public function can_stream_track( bool $can_stream, int $track_id, int $user_id ): bool {
|
||||
// Check if WooCommerce integration is enabled.
|
||||
if ( ! get_option( 'wp_fedistream_enable_woocommerce', 0 ) ) {
|
||||
return $can_stream;
|
||||
}
|
||||
|
||||
// Check if track requires purchase.
|
||||
$requires_purchase = $this->track_requires_purchase( $track_id );
|
||||
|
||||
if ( ! $requires_purchase ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Guest users can't stream paid content.
|
||||
if ( ! $user_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if user has purchased this track.
|
||||
if ( Integration::user_has_purchased( $user_id, 'track', $track_id ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if user has purchased the album containing this track.
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
if ( $album_id && Integration::user_has_purchased( $user_id, 'album', $album_id ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a track requires purchase to stream.
|
||||
*
|
||||
* @param int $track_id Track ID.
|
||||
* @return bool
|
||||
*/
|
||||
private function track_requires_purchase( int $track_id ): bool {
|
||||
// Find WooCommerce products linked to this track.
|
||||
$products = $this->get_products_for_track( $track_id );
|
||||
|
||||
if ( empty( $products ) ) {
|
||||
// Also check album products.
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
if ( $album_id ) {
|
||||
$products = $this->get_products_for_album( $album_id );
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty( $products ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if any product includes streaming.
|
||||
foreach ( $products as $product ) {
|
||||
$include_streaming = get_post_meta( $product->ID, '_fedistream_include_streaming', true );
|
||||
if ( 'yes' === $include_streaming ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WooCommerce products linked to a track.
|
||||
*
|
||||
* @param int $track_id Track ID.
|
||||
* @return array
|
||||
*/
|
||||
private function get_products_for_track( int $track_id ): array {
|
||||
return get_posts(
|
||||
array(
|
||||
'post_type' => 'product',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
array(
|
||||
'key' => '_fedistream_linked_track',
|
||||
'value' => $track_id,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WooCommerce products linked to an album.
|
||||
*
|
||||
* @param int $album_id Album ID.
|
||||
* @return array
|
||||
*/
|
||||
private function get_products_for_album( int $album_id ): array {
|
||||
return get_posts(
|
||||
array(
|
||||
'post_type' => 'product',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
array(
|
||||
'key' => '_fedistream_linked_album',
|
||||
'value' => $album_id,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter track data for AJAX responses.
|
||||
*
|
||||
* @param array $data Track data.
|
||||
* @param int $track_id Track ID.
|
||||
* @return array Modified track data.
|
||||
*/
|
||||
public function filter_track_data( array $data, int $track_id ): array {
|
||||
$user_id = get_current_user_id();
|
||||
$can_stream = apply_filters( 'fedistream_can_stream_track', true, $track_id, $user_id );
|
||||
|
||||
if ( ! $can_stream ) {
|
||||
// Return preview URL instead of full audio.
|
||||
$data['audio_url'] = $this->get_preview_url( $track_id );
|
||||
$data['preview_only'] = true;
|
||||
$data['purchase_url'] = $this->get_purchase_url( $track_id );
|
||||
} else {
|
||||
$data['preview_only'] = false;
|
||||
}
|
||||
|
||||
// Add purchase status.
|
||||
$data['user_has_purchased'] = $user_id && (
|
||||
Integration::user_has_purchased( $user_id, 'track', $track_id ) ||
|
||||
Integration::user_has_purchased( $user_id, 'album', get_post_meta( $track_id, '_fedistream_album_id', true ) )
|
||||
);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preview URL for a track.
|
||||
*
|
||||
* @param int $track_id Track ID.
|
||||
* @return string
|
||||
*/
|
||||
private function get_preview_url( int $track_id ): string {
|
||||
return add_query_arg(
|
||||
array(
|
||||
'fedistream_preview' => $track_id,
|
||||
'_' => time(), // Cache buster.
|
||||
),
|
||||
home_url( '/' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get purchase URL for a track.
|
||||
*
|
||||
* @param int $track_id Track ID.
|
||||
* @return string
|
||||
*/
|
||||
private function get_purchase_url( int $track_id ): string {
|
||||
// Find product for this track.
|
||||
$products = $this->get_products_for_track( $track_id );
|
||||
|
||||
if ( ! empty( $products ) ) {
|
||||
return get_permalink( $products[0]->ID );
|
||||
}
|
||||
|
||||
// Check for album product.
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
if ( $album_id ) {
|
||||
$album_products = $this->get_products_for_album( $album_id );
|
||||
if ( ! empty( $album_products ) ) {
|
||||
return get_permalink( $album_products[0]->ID );
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle preview request.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle_preview_request(): void {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( ! isset( $_GET['fedistream_preview'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$track_id = absint( $_GET['fedistream_preview'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
|
||||
if ( ! $track_id ) {
|
||||
wp_die( esc_html__( 'Invalid 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' ) );
|
||||
}
|
||||
|
||||
// Get preview settings.
|
||||
$preview_start = (int) get_post_meta( $track_id, '_fedistream_preview_start', true );
|
||||
$preview_duration = (int) get_post_meta( $track_id, '_fedistream_preview_duration', true );
|
||||
|
||||
// Default 30 seconds preview.
|
||||
if ( ! $preview_duration ) {
|
||||
$preview_duration = 30;
|
||||
}
|
||||
|
||||
// For now, serve the full file with range headers.
|
||||
// In production, you'd use FFmpeg to extract a preview clip.
|
||||
$this->serve_audio_preview( $file_path, $preview_start, $preview_duration );
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve audio preview with limited duration.
|
||||
*
|
||||
* @param string $file_path File path.
|
||||
* @param int $start_seconds Start time in seconds.
|
||||
* @param int $duration_seconds Duration in seconds.
|
||||
* @return void
|
||||
*/
|
||||
private function serve_audio_preview( string $file_path, int $start_seconds, int $duration_seconds ): void {
|
||||
$file_size = filesize( $file_path );
|
||||
$mime_type = wp_check_filetype( $file_path )['type'] ?: 'audio/mpeg';
|
||||
|
||||
// Calculate byte range for preview (rough approximation).
|
||||
// This is a simplified approach; proper implementation would use FFmpeg.
|
||||
$duration_total = (int) get_post_meta( $this->get_track_id_from_path( $file_path ), '_fedistream_duration', true );
|
||||
|
||||
if ( $duration_total > 0 ) {
|
||||
$bytes_per_second = $file_size / $duration_total;
|
||||
$start_byte = (int) ( $start_seconds * $bytes_per_second );
|
||||
$end_byte = (int) min( ( $start_seconds + $duration_seconds ) * $bytes_per_second, $file_size - 1 );
|
||||
} else {
|
||||
// Serve first 30% of file as fallback.
|
||||
$start_byte = 0;
|
||||
$end_byte = (int) ( $file_size * 0.3 );
|
||||
}
|
||||
|
||||
// Clean output buffer.
|
||||
while ( ob_get_level() ) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
// Set headers for partial content.
|
||||
header( 'HTTP/1.1 206 Partial Content' );
|
||||
header( 'Content-Type: ' . $mime_type );
|
||||
header( 'Accept-Ranges: bytes' );
|
||||
header( 'Content-Length: ' . ( $end_byte - $start_byte + 1 ) );
|
||||
header( "Content-Range: bytes {$start_byte}-{$end_byte}/{$file_size}" );
|
||||
header( 'Content-Disposition: inline' );
|
||||
header( 'Cache-Control: no-cache, no-store, must-revalidate' );
|
||||
|
||||
// Serve partial content.
|
||||
$fp = fopen( $file_path, 'rb' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
|
||||
if ( $fp ) {
|
||||
fseek( $fp, $start_byte );
|
||||
echo fread( $fp, $end_byte - $start_byte + 1 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread,WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
fclose( $fp ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track ID from file path (for cached lookups).
|
||||
*
|
||||
* @param string $file_path File path.
|
||||
* @return int Track ID or 0.
|
||||
*/
|
||||
private function get_track_id_from_path( string $file_path ): int {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$attachment_id = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT ID FROM {$wpdb->posts} WHERE guid LIKE %s",
|
||||
'%' . $wpdb->esc_like( basename( $file_path ) )
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! $attachment_id ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$track_id = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_fedistream_audio_file' AND meta_value = %d",
|
||||
$attachment_id
|
||||
)
|
||||
);
|
||||
|
||||
return (int) $track_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add purchase button after track player.
|
||||
*
|
||||
* @param int $track_id Track ID.
|
||||
* @return void
|
||||
*/
|
||||
public function add_purchase_button( int $track_id ): void {
|
||||
if ( ! get_option( 'wp_fedistream_enable_woocommerce', 0 ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
// Check if already purchased.
|
||||
if ( $user_id ) {
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
|
||||
if ( Integration::user_has_purchased( $user_id, 'track', $track_id ) ) {
|
||||
echo '<p class="fedistream-purchase-status">' . esc_html__( 'You own this track.', 'wp-fedistream' ) . '</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $album_id && Integration::user_has_purchased( $user_id, 'album', $album_id ) ) {
|
||||
echo '<p class="fedistream-purchase-status">' . esc_html__( 'You own this album.', 'wp-fedistream' ) . '</p>';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Find product for this track.
|
||||
$products = $this->get_products_for_track( $track_id );
|
||||
|
||||
if ( ! empty( $products ) ) {
|
||||
$product = wc_get_product( $products[0]->ID );
|
||||
if ( $product ) {
|
||||
echo '<div class="fedistream-purchase-button">';
|
||||
echo '<a href="' . esc_url( get_permalink( $products[0]->ID ) ) . '" class="button">';
|
||||
echo esc_html__( 'Buy Track', 'wp-fedistream' ) . ' - ' . wp_kses_post( $product->get_price_html() );
|
||||
echo '</a>';
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Also show album option if available.
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
if ( $album_id ) {
|
||||
$album_products = $this->get_products_for_album( $album_id );
|
||||
if ( ! empty( $album_products ) ) {
|
||||
$album_product = wc_get_product( $album_products[0]->ID );
|
||||
$album = get_post( $album_id );
|
||||
if ( $album_product && $album ) {
|
||||
echo '<div class="fedistream-purchase-button fedistream-purchase-album">';
|
||||
echo '<a href="' . esc_url( get_permalink( $album_products[0]->ID ) ) . '" class="button button-secondary">';
|
||||
/* translators: %s: Album name */
|
||||
echo esc_html( sprintf( __( 'Buy Full Album: %s', 'wp-fedistream' ), $album->post_title ) );
|
||||
echo ' - ' . wp_kses_post( $album_product->get_price_html() );
|
||||
echo '</a>';
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
520
includes/WooCommerce/TrackProduct.php
Normal file
520
includes/WooCommerce/TrackProduct.php
Normal file
@@ -0,0 +1,520 @@
|
||||
<?php
|
||||
/**
|
||||
* Track Product Type for WooCommerce.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\WooCommerce;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* FediStream Track product type.
|
||||
*
|
||||
* Digital product representing a single FediStream track.
|
||||
*/
|
||||
class TrackProduct extends \WC_Product {
|
||||
|
||||
/**
|
||||
* Product type.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $product_type = 'fedistream_track';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param int|\WC_Product|object $product Product ID or object.
|
||||
*/
|
||||
public function __construct( $product = 0 ) {
|
||||
parent::__construct( $product );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_type(): string {
|
||||
return 'fedistream_track';
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks are virtual products.
|
||||
*
|
||||
* @param string $context View or edit context.
|
||||
* @return bool
|
||||
*/
|
||||
public function get_virtual( $context = 'view' ): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks are downloadable products.
|
||||
*
|
||||
* @param string $context View or edit context.
|
||||
* @return bool
|
||||
*/
|
||||
public function get_downloadable( $context = 'view' ): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the linked track ID.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_linked_track_id(): int {
|
||||
return (int) $this->get_meta( '_fedistream_linked_track', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the linked track post.
|
||||
*
|
||||
* @return \WP_Post|null
|
||||
*/
|
||||
public function get_linked_track(): ?\WP_Post {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$track = get_post( $track_id );
|
||||
|
||||
if ( ! $track || 'fedistream_track' !== $track->post_type ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $track;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pricing type.
|
||||
*
|
||||
* @return string fixed, pwyw, or nyp
|
||||
*/
|
||||
public function get_pricing_type(): string {
|
||||
return $this->get_meta( '_fedistream_pricing_type', true ) ?: 'fixed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minimum price for PWYW.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function get_min_price(): float {
|
||||
return (float) $this->get_meta( '_fedistream_min_price', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get suggested price for PWYW.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function get_suggested_price(): float {
|
||||
return (float) $this->get_meta( '_fedistream_suggested_price', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if streaming is included.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function includes_streaming(): bool {
|
||||
return 'yes' === $this->get_meta( '_fedistream_include_streaming', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available download formats.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_available_formats(): array {
|
||||
$formats = $this->get_meta( '_fedistream_available_formats', true );
|
||||
|
||||
return is_array( $formats ) ? $formats : array( 'mp3' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track duration in seconds.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_duration(): int {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) get_post_meta( $track_id, '_fedistream_duration', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatted duration.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_formatted_duration(): string {
|
||||
$seconds = $this->get_duration();
|
||||
|
||||
if ( ! $seconds ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$mins = floor( $seconds / 60 );
|
||||
$secs = $seconds % 60;
|
||||
|
||||
return sprintf( '%d:%02d', $mins, $secs );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get artist name(s).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_artist_name(): string {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$artist_ids = get_post_meta( $track_id, '_fedistream_artist_ids', true );
|
||||
|
||||
if ( ! is_array( $artist_ids ) || empty( $artist_ids ) ) {
|
||||
// Fall back to album artist.
|
||||
$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 '';
|
||||
}
|
||||
|
||||
$names = array();
|
||||
foreach ( $artist_ids as $artist_id ) {
|
||||
$artist = get_post( $artist_id );
|
||||
if ( $artist ) {
|
||||
$names[] = $artist->post_title;
|
||||
}
|
||||
}
|
||||
|
||||
return implode( ', ', $names );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get album name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_album_name(): string {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
|
||||
if ( ! $album_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$album = get_post( $album_id );
|
||||
|
||||
return $album ? $album->post_title : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track artwork URL.
|
||||
*
|
||||
* Falls back to album artwork if track has none.
|
||||
*
|
||||
* @param string $size Image size.
|
||||
* @return string
|
||||
*/
|
||||
public function get_track_artwork( string $size = 'medium' ): string {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Try track thumbnail first.
|
||||
$thumbnail_id = get_post_thumbnail_id( $track_id );
|
||||
|
||||
// Fall back to album artwork.
|
||||
if ( ! $thumbnail_id ) {
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
$thumbnail_id = $album_id ? get_post_thumbnail_id( $album_id ) : 0;
|
||||
}
|
||||
|
||||
if ( ! $thumbnail_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$image = wp_get_attachment_image_url( $thumbnail_id, $size );
|
||||
|
||||
return $image ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get audio file URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_audio_url(): string {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$audio_id = get_post_meta( $track_id, '_fedistream_audio_file', true );
|
||||
|
||||
if ( ! $audio_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return wp_get_attachment_url( $audio_id ) ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if track is explicit.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_explicit(): bool {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool) get_post_meta( $track_id, '_fedistream_explicit', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track BPM.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_bpm(): int {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) get_post_meta( $track_id, '_fedistream_bpm', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track musical key.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_musical_key(): string {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return get_post_meta( $track_id, '_fedistream_key', true ) ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ISRC code.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_isrc(): string {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return get_post_meta( $track_id, '_fedistream_isrc', true ) ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get downloads for this product.
|
||||
*
|
||||
* Generates downloadable files based on available formats.
|
||||
*
|
||||
* @param string $context View or edit context.
|
||||
* @return array
|
||||
*/
|
||||
public function get_downloads( $context = 'view' ): array {
|
||||
$downloads = parent::get_downloads( $context );
|
||||
|
||||
// If no manual downloads set, generate from linked track.
|
||||
if ( empty( $downloads ) && $this->get_linked_track_id() ) {
|
||||
$downloads = $this->generate_track_downloads();
|
||||
}
|
||||
|
||||
return $downloads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate download files from linked track.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function generate_track_downloads(): array {
|
||||
$downloads = array();
|
||||
$track = $this->get_linked_track();
|
||||
$formats = $this->get_available_formats();
|
||||
|
||||
if ( ! $track ) {
|
||||
return $downloads;
|
||||
}
|
||||
|
||||
// For each format, create a download entry.
|
||||
foreach ( $formats as $format ) {
|
||||
$format_label = strtoupper( $format );
|
||||
$download_id = 'track-' . $track->ID . '-' . $format;
|
||||
|
||||
$downloads[ $download_id ] = array(
|
||||
'id' => $download_id,
|
||||
'name' => sprintf(
|
||||
/* translators: 1: Track name, 2: Format name */
|
||||
__( '%1$s (%2$s)', 'wp-fedistream' ),
|
||||
$track->post_title,
|
||||
$format_label
|
||||
),
|
||||
'file' => add_query_arg(
|
||||
array(
|
||||
'fedistream_download' => 'track',
|
||||
'track_id' => $track->ID,
|
||||
'format' => $format,
|
||||
),
|
||||
home_url( '/' )
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return $downloads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if purchasable.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_purchasable(): bool {
|
||||
// Must have a linked track.
|
||||
if ( ! $this->get_linked_track_id() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check price for fixed pricing.
|
||||
if ( 'fixed' === $this->get_pricing_type() ) {
|
||||
return $this->get_price() !== '' && $this->get_price() >= 0;
|
||||
}
|
||||
|
||||
// PWYW and NYP are always purchasable.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get price HTML.
|
||||
*
|
||||
* @param string $price Price HTML.
|
||||
* @return string
|
||||
*/
|
||||
public function get_price_html( $price = '' ): string {
|
||||
$pricing_type = $this->get_pricing_type();
|
||||
|
||||
if ( 'nyp' === $pricing_type ) {
|
||||
return '<span class="fedistream-nyp-price">' . esc_html__( 'Name Your Price', 'wp-fedistream' ) . '</span>';
|
||||
}
|
||||
|
||||
if ( 'pwyw' === $pricing_type ) {
|
||||
$min_price = $this->get_min_price();
|
||||
$suggested = $this->get_suggested_price();
|
||||
|
||||
$html = '<span class="fedistream-pwyw-price">';
|
||||
|
||||
if ( $min_price > 0 ) {
|
||||
$html .= sprintf(
|
||||
/* translators: %s: Minimum price */
|
||||
esc_html__( 'From %s', 'wp-fedistream' ),
|
||||
wc_price( $min_price )
|
||||
);
|
||||
} else {
|
||||
$html .= esc_html__( 'Pay What You Want', 'wp-fedistream' );
|
||||
}
|
||||
|
||||
if ( $suggested > 0 ) {
|
||||
$html .= ' <span class="fedistream-suggested">';
|
||||
$html .= sprintf(
|
||||
/* translators: %s: Suggested price */
|
||||
esc_html__( '(Suggested: %s)', 'wp-fedistream' ),
|
||||
wc_price( $suggested )
|
||||
);
|
||||
$html .= '</span>';
|
||||
}
|
||||
|
||||
$html .= '</span>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
return parent::get_price_html( $price );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add to cart validation for PWYW products.
|
||||
*
|
||||
* @param bool $passed Validation passed.
|
||||
* @param int $product_id Product ID.
|
||||
* @param int $quantity Quantity.
|
||||
* @return bool
|
||||
*/
|
||||
public static function validate_add_to_cart( bool $passed, int $product_id, int $quantity ): bool {
|
||||
$product = wc_get_product( $product_id );
|
||||
|
||||
if ( ! $product || 'fedistream_track' !== $product->get_type() ) {
|
||||
return $passed;
|
||||
}
|
||||
|
||||
$pricing_type = $product->get_pricing_type();
|
||||
|
||||
if ( 'pwyw' === $pricing_type || 'nyp' === $pricing_type ) {
|
||||
$custom_price = isset( $_POST['fedistream_custom_price'] ) ? // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
wc_format_decimal( sanitize_text_field( wp_unslash( $_POST['fedistream_custom_price'] ) ) ) : // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
0;
|
||||
|
||||
$min_price = $product->get_min_price();
|
||||
|
||||
if ( 'pwyw' === $pricing_type && $custom_price < $min_price ) {
|
||||
wc_add_notice(
|
||||
sprintf(
|
||||
/* translators: %s: Minimum price */
|
||||
__( 'Please enter at least %s', 'wp-fedistream' ),
|
||||
wc_price( $min_price )
|
||||
),
|
||||
'error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
WC()->session->set( 'fedistream_custom_price_' . $product_id, $custom_price );
|
||||
}
|
||||
|
||||
return $passed;
|
||||
}
|
||||
}
|
||||
1
includes/index.php
Normal file
1
includes/index.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php // Silence is golden.
|
||||
Reference in New Issue
Block a user