diff --git a/models/playlists_model.php b/models/playlists_model.php index 8b8b9f3..87eff6f 100644 --- a/models/playlists_model.php +++ b/models/playlists_model.php @@ -697,7 +697,6 @@ public function resolve($playlist_id, $player_id, $parent_player_id = false, $st $supports = ['audio','video','image','document']; } - // TODO currently no player support for documents, but will be doing this at some point. $support_document = $player_id ? false : true; @@ -726,6 +725,11 @@ public function resolve($playlist_id, $player_id, $parent_player_id = false, $st // track offset for max duration $media_offset = 0.0; + // Global tracking of used media IDs across all selection types + $used_media_ids = []; + $used_station_ids = []; + $manually_selected_media_ids = []; // Track manually selected media separately + foreach ($playlist_items as $playlist_item) { if ($playlist_item['properties']) { $playlist_item['properties'] = json_decode($playlist_item['properties'], true); @@ -744,6 +748,7 @@ public function resolve($playlist_id, $player_id, $parent_player_id = false, $st } if ($media) { + // Manually selected media is always allowed, even if previously used $tmp = ['type' => 'media','id' => $playlist_item['item_id'], 'title' => $media['title'], 'artist' => $media['artist']]; if ($media['type'] == 'image') { $tmp['duration'] = $playlist_item['properties']['duration']; @@ -770,6 +775,8 @@ public function resolve($playlist_id, $player_id, $parent_player_id = false, $st } } $media_items_tmp[] = $tmp; + $manually_selected_media_ids[] = $playlist_item['item_id']; + $used_media_ids[] = $playlist_item['item_id']; } } elseif ($playlist_item['item_type'] == 'dynamic') { // dynamic item @@ -779,18 +786,27 @@ public function resolve($playlist_id, $player_id, $parent_player_id = false, $st $media_search = $this->models->media('search', ['params' => ['query' => $playlist_item['properties']['query']], 'player_id' => $player_id]); $media_items = $media_search[0] ?? []; - // remove unsupported and dayparting exclusions - foreach ($media_items as $index => $media_item) { - if (array_search($media_item['id'], $dayparting_exclude_ids) !== false) { - unset($media_items[$index]); - } - if (!in_array($media_item['type'], $supports)) { - unset($media_items[$index]); - } + // Remove items that are already used in dynamic selection, + // but allow manually selected items to be used in dynamic selection + $media_items = array_filter($media_items, function($media) use ($used_media_ids, $manually_selected_media_ids, $supports, $dayparting_exclude_ids) { + return + // Not used in any previous selection + !in_array($media['id'], $used_media_ids) && + // Supports the player's media type + in_array($media['type'], $supports) && + // Not excluded by dayparting + array_search($media['id'], $dayparting_exclude_ids) === false; + }); + + // Reset used media IDs if all unique items have been exhausted + if (empty($media_items)) { + $media_search = $this->models->media('search', ['params' => ['query' => $playlist_item['properties']['query']], 'player_id' => $player_id]); + $media_items = $media_search[0] ?? []; + $used_media_ids = array_diff($used_media_ids, $manually_selected_media_ids); // Keep manually selected media IDs } if (!empty($media_items)) { - // we keep searching until we have enough items. this allows randomization, but will not have two of the same tracks playing nearby each other. + // we keep searching until we have enough items if ($playlist_item['properties']['num_items']) { while (count($dynamic_items) < $playlist_item['properties']['num_items']) { // randomize our items @@ -807,6 +823,7 @@ public function resolve($playlist_id, $player_id, $parent_player_id = false, $st $tmp['media_type'] = $media['type']; $tmp['context'] = 'Dynamic Selection: ' . $playlist_item['properties']['name']; $dynamic_items[] = $tmp; + $used_media_ids[] = $media['id']; // end loop if we have enough items if (count($dynamic_items) >= $playlist_item['properties']['num_items']) { @@ -827,7 +844,9 @@ public function resolve($playlist_id, $player_id, $parent_player_id = false, $st } $media_offset += $tmp['duration']; $tmp['media_type'] = $media['type']; + $tmp['context'] = 'Dynamic Selection: ' . $playlist_item['properties']['name']; $dynamic_items[] = $tmp; + $used_media_ids[] = $media['id']; } } } @@ -864,13 +883,27 @@ public function resolve($playlist_id, $player_id, $parent_player_id = false, $st $media_items = $this->db->assoc_list(); // remove unsupported and dayparting exclusions - foreach ($media_items as $index => $media) { - if (array_search($media['id'], $dayparting_exclude_ids) !== false) { - unset($media_items[$index]); - } - if (!in_array($media['type'], $supports)) { - unset($media_items[$index]); - } + $media_items = array_filter($media_items, function($media) use ($supports, $dayparting_exclude_ids) { + return in_array($media['type'], $supports) && + array_search($media['id'], $dayparting_exclude_ids) === false; + }); + + // Remove already used station IDs + $media_items = array_filter($media_items, function($media) use (&$used_station_ids) { + return !in_array($media['id'], $used_station_ids); + }); + + // Reset used station IDs if all unique items have been exhausted + if (empty($media_items)) { + $used_station_ids = []; + $this->db->query('SELECT media.* FROM players_station_ids LEFT JOIN media ON players_station_ids.media_id = media.id WHERE player_id="' . $this->db->escape($station_id_player) . '";'); + $media_items = $this->db->assoc_list(); + + // Re-apply filters after reset + $media_items = array_filter($media_items, function($media) use ($supports, $dayparting_exclude_ids) { + return in_array($media['type'], $supports) && + array_search($media['id'], $dayparting_exclude_ids) === false; + }); } // randomize our selection @@ -894,11 +927,12 @@ public function resolve($playlist_id, $player_id, $parent_player_id = false, $st $tmp['media_type'] = $media['type']; $tmp['context'] = 'Station ID'; $media_items_tmp[] = $tmp; + $used_station_ids[] = $media['id']; } } elseif ($playlist_item['item_type'] == 'breakpoint') { $media_items_tmp[] = ['type' => 'breakpoint']; } elseif ($playlist_item['item_type'] == 'custom') { - // get the callback model/method in order for this custom item, add media items specified by the callback method. + // get the callback model/method in order for this custom item, add media items specified by the callback method. $custom_item_query = $playlist_item['properties']; $custom_item_name = $custom_item_query['name'] ?? ''; $this->db->where('name', $custom_item_name); @@ -929,38 +963,41 @@ public function resolve($playlist_id, $player_id, $parent_player_id = false, $st } } - // add our media items from this run to our complete set of media items. + // Add items to return $return = array_merge($return, $media_items_tmp); - // break out of loop if we've met our max duration - if ($max_duration && $media_offset >= $max_duration) { + // Check max duration + if ($max_duration !== null && $media_offset > $max_duration) { break; } } - // remove or limit crossfade as required - foreach ($return as $index => &$item) { - // skip if crossfade not set - if ($item['type'] != 'media' || !isset($item['crossfade'])) { - continue; - } + // remove or limit crossfade as required + foreach ($return as $index => &$item) { + // skip if crossfade not set + if ($item['type'] != 'media' || !isset($item['crossfade'])) { + continue; + } - // limit crossfade to item/next-item duration if (!isset($return[$index + 1])) { $max_crossfade = $item['duration']; - } else { - $max_crossfade = min($item['duration'], $return[$index + 1]['duration']); - } - if ($item['crossfade'] > $max_crossfade) { - $item['crossfade'] = $max_crossfade; } + // limit crossfade to item/next-item duration + if (!isset($return[$index + 1])) { + $max_crossfade = $item['duration']; + } else { + $max_crossfade = min($item['duration'], $return[$index + 1]['duration']); + } + if ($item['crossfade'] > $max_crossfade) { + $item['crossfade'] = $max_crossfade; + } - // remove if last track or next track not audio - if ($index == count($return) - 1 || $return[$index + 1]['media_type'] != 'audio') { - unset($item['crossfade']); + // remove if last track or next track not audio + if ($index == count($return) - 1 || $return[$index + 1]['media_type'] != 'audio') { + unset($item['crossfade']); + } } - } return $return; } -} +} \ No newline at end of file diff --git a/test_playlist_resolve.php b/test_playlist_resolve.php new file mode 100644 index 0000000..99d6e67 --- /dev/null +++ b/test_playlist_resolve.php @@ -0,0 +1,53 @@ +model('Playlists'); + +// Get playlistId and playerId from command-line arguments +if ($argc < 3) { + echo "Usage: php test_playlist_resolve.php \n"; + exit(1); +} + +$playlistId = (int)$argv[1]; +$playerId = (int)$argv[2]; + +// Resolve the playlist +try { + $resolvedItems = $playlistsModel('resolve', + $playlistId, // Specific playlist ID to test + $playerId, // Player ID + false, // No parent player + new DateTime(), // Start time + null // Max duration + ); + + // Print out resolved items in simplified format + echo "Resolved Playlist Items:\n"; + foreach ($resolvedItems as $item) { + echo "{$item['context']}, {$item['id']}\n"; + } + + // Check for duplicates manually + $mediaIds = array_column($resolvedItems, 'id'); + $duplicates = array_diff_assoc($mediaIds, array_unique($mediaIds)); + + echo "\nDuplicates Found: "; + if (empty($duplicates)) { + echo "None\n"; + } else { + echo implode(", ", $duplicates) . "\n"; + } + + // Additional analysis + echo "\nTotal Items: " . count($resolvedItems) . "\n"; + echo "Unique Items: " . count(array_unique($mediaIds)) . "\n"; + +} catch (Exception $e) { + echo "Error resolving playlist: " . $e->getMessage() . "\n"; +}