post_type || 'publish' !== $post->post_status ) { return array(); } $category_ids = self::term_ids( $post_id, 'sg7g_category' ); $tag_ids = self::term_ids( $post_id, 'sg7g_tag' ); if ( empty( $category_ids ) && empty( $tag_ids ) ) { return array(); } $excluded_ids = array_values( array_unique( array_filter( array_map( 'absint', array_merge( array( $post_id ), $excluded_ids ) ) ) ) ); $candidate_ids = self::candidate_ids( $category_ids, $tag_ids, $excluded_ids ); $current_source = self::source_key( $post_id ); if ( empty( $candidate_ids ) ) { return array(); } update_meta_cache( 'post', $candidate_ids ); update_object_term_cache( $candidate_ids, 'sg7g_media' ); $scored = array(); foreach ( $candidate_ids as $candidate_id ) { $candidate_id = absint( $candidate_id ); if ( $candidate_id <= 0 || in_array( $candidate_id, $excluded_ids, true ) ) { continue; } $candidate = get_post( $candidate_id ); if ( ! $candidate instanceof WP_Post || 'publish' !== $candidate->post_status || '' !== (string) $candidate->post_password ) { continue; } $candidate_source = self::source_key( $candidate_id ); if ( '' !== $current_source && '' !== $candidate_source && hash_equals( $current_source, $candidate_source ) ) { continue; } $candidate_categories = self::term_ids( $candidate_id, 'sg7g_category' ); $candidate_tags = self::term_ids( $candidate_id, 'sg7g_tag' ); $category_matches = count( array_intersect( $category_ids, $candidate_categories ) ); $tag_matches = count( array_intersect( $tag_ids, $candidate_tags ) ); if ( 0 === $category_matches && 0 === $tag_matches ) { continue; } $views = SG7G_Views::get( $candidate_id ); $score = ( $category_matches * 100 ) + ( $tag_matches * 30 ); $scored[] = array( 'id' => $candidate_id, 'score' => $score, 'categories' => $category_matches, 'tags' => $tag_matches, 'views' => $views, 'date' => (int) get_post_timestamp( $candidate ), 'source' => $candidate_source, ); } if ( empty( $scored ) ) { return array(); } usort( $scored, static function ( array $left, array $right ): int { foreach ( array( 'score', 'categories', 'tags', 'views', 'date' ) as $key ) { $comparison = $right[ $key ] <=> $left[ $key ]; if ( 0 !== $comparison ) { return $comparison; } } return $right['id'] <=> $left['id']; } ); $result = array(); $seen_sources = '' !== $current_source ? array( $current_source => true ) : array(); foreach ( $scored as $item ) { $source_key = (string) $item['source']; if ( '' !== $source_key && isset( $seen_sources[ $source_key ] ) ) { continue; } $result[] = (int) $item['id']; if ( '' !== $source_key ) { $seen_sources[ $source_key ] = true; } if ( count( $result ) >= $limit ) { break; } } return $result; } /** * Collect candidates from category and tag matches. * * @param int[] $category_ids Category IDs. * @param int[] $tag_ids Tag IDs. * @param int[] $excluded_ids Excluded post IDs. * * @return int[] */ private static function candidate_ids( array $category_ids, array $tag_ids, array $excluded_ids ): array { $tax_query = array( 'relation' => 'OR' ); if ( ! empty( $category_ids ) ) { $tax_query[] = array( 'taxonomy' => 'sg7g_category', 'field' => 'term_id', 'terms' => $category_ids, ); } if ( ! empty( $tag_ids ) ) { $tax_query[] = array( 'taxonomy' => 'sg7g_tag', 'field' => 'term_id', 'terms' => $tag_ids, ); } if ( count( $tax_query ) <= 1 ) { return array(); } $ids = get_posts( array( 'post_type' => 'sg7g_media', 'post_status' => 'publish', 'has_password' => false, 'posts_per_page' => self::MAX_CANDIDATES, 'fields' => 'ids', // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- Bounded exclusion list for related content. 'post__not_in' => $excluded_ids, 'ignore_sticky_posts' => true, 'no_found_rows' => true, 'orderby' => 'date', 'order' => 'DESC', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- Bounded related-content taxonomy match. 'tax_query' => $tax_query, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ) ); return array_values( array_unique( array_filter( array_map( 'absint', $ids ) ) ) ); } /** * Return term IDs safely. * * @return int[] */ private static function term_ids( int $post_id, string $taxonomy ): array { $terms = wp_get_post_terms( $post_id, $taxonomy, array( 'fields' => 'ids' ) ); return is_wp_error( $terms ) ? array() : array_values( array_unique( array_map( 'absint', $terms ) ) ); } /** * Build a canonical source key to suppress duplicate media records. */ private static function source_key( int $post_id ): string { $source = (string) get_post_meta( $post_id, '_sg7g_source', true ); $file_id = absint( get_post_meta( $post_id, '_sg7g_file_id', true ) ); $url = html_entity_decode( (string) get_post_meta( $post_id, '_sg7g_source_url', true ), ENT_QUOTES, 'UTF-8' ); if ( $file_id > 0 ) { return 'file:' . $file_id; } if ( '' === $url ) { return ''; } if ( in_array( $source, array( 'youtube', 'youtube_music' ), true ) ) { $video_id = SG7G_Meta_Boxes::youtube_id( $url ); $playlist_id = SG7G_Meta_Boxes::youtube_playlist_id( $url ); if ( $video_id || $playlist_id ) { return 'youtube:' . $video_id . ':' . $playlist_id; } } if ( 'vimeo' === $source ) { $video_id = SG7G_Meta_Boxes::vimeo_id( $url ); if ( $video_id ) { return 'vimeo:' . $video_id . ':' . SG7G_Meta_Boxes::vimeo_hash( $url ); } } $parts = wp_parse_url( $url ); $path = trim( (string) ( is_array( $parts ) ? ( $parts['path'] ?? '' ) : '' ), '/' ); if ( 'spotify' === $source ) { $segments = self::normalized_segments( $path ); if ( isset( $segments[0] ) && 'embed' === strtolower( $segments[0] ) ) { array_shift( $segments ); } if ( isset( $segments[0], $segments[1] ) ) { return 'spotify:' . strtolower( $segments[0] ) . ':' . $segments[1]; } } if ( 'deezer' === $source ) { $segments = self::normalized_segments( $path ); if ( isset( $segments[0], $segments[1] ) ) { return 'deezer:' . strtolower( $segments[0] ) . ':' . $segments[1]; } } if ( 'apple_music' === $source ) { $query = array(); if ( is_array( $parts ) && ! empty( $parts['query'] ) ) { parse_str( (string) $parts['query'], $query ); } if ( ! empty( $query['i'] ) ) { return 'apple_music:track:' . sanitize_key( (string) $query['i'] ); } if ( preg_match( '/(?:^|\/)(\d+)(?:\/)?$/', $path, $matches ) ) { return 'apple_music:collection:' . $matches[1]; } } if ( ! is_array( $parts ) || empty( $parts['host'] ) ) { return strtolower( trim( $url ) ); } $host = strtolower( preg_replace( '/^www\./', '', (string) $parts['host'] ) ); $path = untrailingslashit( '/' . ltrim( (string) ( $parts['path'] ?? '' ), '/' ) ); $query = array(); if ( ! empty( $parts['query'] ) ) { parse_str( (string) $parts['query'], $query ); foreach ( array_keys( $query ) as $key ) { if ( str_starts_with( strtolower( (string) $key ), 'utm_' ) || in_array( strtolower( (string) $key ), array( 'fbclid', 'gclid', 'si', 'feature' ), true ) ) { unset( $query[ $key ] ); } } ksort( $query ); } return $source . ':' . $host . rawurldecode( $path ) . ( $query ? '?' . http_build_query( $query ) : '' ); } /** * Remove locale prefixes from platform paths. * * @return string[] */ private static function normalized_segments( string $path ): array { $segments = array_values( array_filter( explode( '/', trim( $path, '/' ) ) ) ); if ( isset( $segments[0] ) && ( preg_match( '/^intl-[a-z]{2}$/i', $segments[0] ) || preg_match( '/^[a-z]{2}(?:-[a-z]{2})?$/i', $segments[0] ) ) ) { array_shift( $segments ); } return $segments; } }