Merge pull request #3174 from skitt/spread-smart-shuffle

Smart shuffle: spread episodes evenly
This commit is contained in:
H. Lehmann 2019-05-28 17:44:12 +02:00 committed by GitHub
commit cb3b3ac578
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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
* 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
* collection of individual, pubdate-sorted lists that each contain only items from a specific
* feed.
*
* 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.
* The Smart Shuffle algorithm involves spreading episodes from each feed out over the whole
* queue. To do this, we calculate the number of episodes in each feed, then a common multiple
* (not the smallest); each episode is then spread out, and we sort the resulting list of
* episodes by "spread out factor" and feed name.
*
* 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:
@ -152,8 +149,17 @@ public class QueueSorter {
? (f1, f2) -> f1.getPubDate().compareTo(f2.getPubDate())
: (f1, f2) -> f2.getPubDate().compareTo(f1.getPubDate());
for (Long id : map.keySet()) {
Collections.sort(map.get(id), itemComparator);
// Calculate the spread
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).
@ -161,25 +167,29 @@ public class QueueSorter {
List<List<FeedItem>> feeds = new ArrayList<>(map.values());
Collections.sort(feeds,
// (we use a desc sort here, since we're iterating back-to-front below)
(f1, f2) -> f2.get(0).getFeed().getTitle().compareTo(f1.get(0).getFeed().getTitle()));
(f1, f2) -> f1.get(0).getFeed().getTitle().compareTo(f2.get(0).getFeed().getTitle()));
// Cycle through the (sorted) feed lists in a round-robin fashion, removing the first item
// and adding it back into to the original queue
while (!feeds.isEmpty()) {
// Iterate across the (sorted) list of feeds, removing the first item in each, and
// appending it to the queue. Note that we're iterating back-to-front here, since we
// will be deleting feed lists as they become empty.
for (int i = feeds.size() - 1; i >= 0; --i) {
List<FeedItem> items = feeds.get(i);
queue.add(items.remove(0));
// Removed the last item in this particular feed? Then remove this feed from the
// list of feeds.
if (items.isEmpty()) {
feeds.remove(i);
// Spread each episode out
Map<Long, List<FeedItem>> spreadItems = new HashMap<>();
for (List<FeedItem> feedItems : feeds) {
long thisSpread = spread / feedItems.size();
// Starting from 0 ensures we front-load, so the queue starts with one episode from
// each feed in the queue
long itemSpread = 0;
for (FeedItem feedItem : feedItems) {
if (!spreadItems.containsKey(itemSpread)) {
spreadItems.put(itemSpread, new ArrayList<>());
}
spreadItems.get(itemSpread).add(feedItem);
itemSpread += thisSpread;
}
}
// 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));
}
}
}