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