Smart shuffle: spread episodes evenly

This reworks the sort algorithm used in smart shuffle so that episodes
are spread out evenly, which avoids episodes bunching up at the bottom
of the queue when one feed has more episodes than others, and avoids
running through feeds with few episodes very quickly.

Signed-off-by: Stephen Kitt <steve@sk2.org>
This commit is contained in:
Stephen Kitt 2019-05-09 18:38:34 +02:00
parent c9b17c14f1
commit 0a1a54d28d
No known key found for this signature in database
GPG Key ID: 80D302F5886D839C
1 changed files with 35 additions and 25 deletions

View File

@ -104,13 +104,10 @@ public class QueueSorter {
* prefer a more balanced ordering that avoids having to listen to clusters of consecutive * prefer a more balanced ordering that avoids having to listen to clusters of consecutive
* episodes from the same feed. This is what "Smart Shuffle" tries to accomplish. * episodes from the same feed. This is what "Smart Shuffle" tries to accomplish.
* *
* The Smart Shuffle algorithm involves choosing episodes (in round-robin fashion) from a * The Smart Shuffle algorithm involves spreading episodes from each feed out over the whole
* collection of individual, pubdate-sorted lists that each contain only items from a specific * queue. To do this, we calculate the number of episodes in each feed, then a common multiple
* feed. * (not the smallest); each episode is then spread out, and we sort the resulting list of
* * episodes by "spread out factor" and feed name.
* Of course, clusters of consecutive episodes <i>at the end of the queue</i> may be
* unavoidable. This seems unlikely to be an issue for most users who presumably maintain
* large queues with new episodes continuously being added.
* *
* For example, given a queue containing three episodes each from three different feeds * For example, given a queue containing three episodes each from three different feeds
* (A, B, and C), a simple pubdate sort might result in a queue that looks like the following: * (A, B, and C), a simple pubdate sort might result in a queue that looks like the following:
@ -152,8 +149,17 @@ public class QueueSorter {
? (f1, f2) -> f1.getPubDate().compareTo(f2.getPubDate()) ? (f1, f2) -> f1.getPubDate().compareTo(f2.getPubDate())
: (f1, f2) -> f2.getPubDate().compareTo(f1.getPubDate()); : (f1, f2) -> f2.getPubDate().compareTo(f1.getPubDate());
for (Long id : map.keySet()) { // Calculate the spread
Collections.sort(map.get(id), itemComparator);
long spread = 0;
for (Map.Entry<Long, List<FeedItem>> mapEntry : map.entrySet()) {
List<FeedItem> feedItems = mapEntry.getValue();
Collections.sort(feedItems, itemComparator);
if (spread == 0) {
spread = feedItems.size();
} else if (feedItems.size() % spread != 0){
spread *= feedItems.size();
}
} }
// Create a list of the individual FeedItems lists, and sort it by feed title (ascending). // Create a list of the individual FeedItems lists, and sort it by feed title (ascending).
@ -161,25 +167,29 @@ public class QueueSorter {
List<List<FeedItem>> feeds = new ArrayList<>(map.values()); List<List<FeedItem>> feeds = new ArrayList<>(map.values());
Collections.sort(feeds, Collections.sort(feeds,
// (we use a desc sort here, since we're iterating back-to-front below) (f1, f2) -> f1.get(0).getFeed().getTitle().compareTo(f2.get(0).getFeed().getTitle()));
(f1, f2) -> f2.get(0).getFeed().getTitle().compareTo(f1.get(0).getFeed().getTitle()));
// Cycle through the (sorted) feed lists in a round-robin fashion, removing the first item // Spread each episode out
// and adding it back into to the original queue Map<Long, List<FeedItem>> spreadItems = new HashMap<>();
for (List<FeedItem> feedItems : feeds) {
while (!feeds.isEmpty()) { long thisSpread = spread / feedItems.size();
// Iterate across the (sorted) list of feeds, removing the first item in each, and // Starting from 0 ensures we front-load, so the queue starts with one episode from
// appending it to the queue. Note that we're iterating back-to-front here, since we // each feed in the queue
// will be deleting feed lists as they become empty. long itemSpread = 0;
for (int i = feeds.size() - 1; i >= 0; --i) { for (FeedItem feedItem : feedItems) {
List<FeedItem> items = feeds.get(i); if (!spreadItems.containsKey(itemSpread)) {
queue.add(items.remove(0)); spreadItems.put(itemSpread, new ArrayList<>());
// Removed the last item in this particular feed? Then remove this feed from the }
// list of feeds. spreadItems.get(itemSpread).add(feedItem);
if (items.isEmpty()) { itemSpread += thisSpread;
feeds.remove(i);
} }
} }
// Go through the spread items and add them to the queue
List<Long> spreads = new ArrayList<>(spreadItems.keySet());
Collections.sort(spreads);
for (long itemSpread : spreads) {
queue.addAll(spreadItems.get(itemSpread));
} }
} }
} }