-
-
Notifications
You must be signed in to change notification settings - Fork 15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Order.separateArtists does not preserve the track ordering #238
Comments
Probably you use incorrect function for your purpose. In order to get only one track per artist need to use dedupArtists. let items = // artists or tracks
Order.sort(items, 'artist.popularity', 'desc')
Filter.dedupArtists(items) The purpose of separateArtists is re-order array to create space between the same artists. |
You are right: if I only wanted to keep 1 track per artist, Filter.dedupArtists is the way to go. I had actually forgotten about that function, and achieved the same result by using Order.separateArtists(tracks, tracks.length, false); This does not change the fact that the resultant ordering and selection is unexepected with separateArtists. Ideally though, I would have wanted to allow up to the 2 top tracks from each artist, which I was expecting to work with: Order.separateArtists(tracks, Math.round(numTracks/2), false); That is how I discovered the problem. |
For example, look at let array = [
{ id: 'lion', followers: 10 },
{ id: 'parrot', followers: 15 },
{ id: 'cat', followers: 99 },
{ id: 'dog', followers: 95 },
{ id: 'dog', followers: 45 },
{ id: 'monkey', followers: 20 },
]
Order.separateArtists(array, 2, false);
console.log(
array
.map(a => `${a.followers} ${a.id}`)
.join('\n')
)
With
That's why you see You need to group by artists in order to get more than one top tracks. Something like below. Note that is grouped by first artists of track. function keepTrackCountByArtists(tracks, count) {
let groups = tracks.reduce((artists, track) => {
let key = track.artists[0].id
artists[key] = artists[key] || []
artists[key].push(track)
return artists
}, {})
Combiner.replace(
tracks,
Object
.values(groups)
.map(group => Selector.sliceFirst(group, count))
.flat(1)
)
} |
Interesting approach... I will try it, thank you. Here is how I did it in Python. I will try to port it to JS and see how it behaves. I wrote it quite a while ago, so I am not sure how it behaves; it might very well have the same problem. But if I remember correctly, I did it in such a way that it does not remove tracks if separation cannot be achieved. def score_artist_separation ( tracks, separation=4 ):
"""
This function is used internally by seprate_artists
Calculate the artist separation score, and
produces a list of index for the tracks that
need separation
Args:
tracks: list of spotify track objects
separation: desired artist separation (optional, default=4)
Returns:
score: separation score (0 = fully separated)
separation_index: index of tracks that need separation (list)
"""
score = 0
indexes = set()
# iterate through all the tracks
for x in range(len(tracks) - 1):
# get the main artist id for the current track
artist_id_x = tracks[x]['track']['artists'][0]['id']
# iterate through all the subsequent tracks
for y in range(x + 1, len(tracks)):
# once we reach the desired separation distance, exit
if y - x >= separation:
break
# get the main artist id for the current track
artist_id_y = tracks[y]['track']['artists'][0]['id']
# identical artists found, closer than the desired separation
if artist_id_x == artist_id_y:
# add the inverted distance to the score
score += separation - (y - x)
# add both track to the index of tracks that need separation
indexes.add(x)
indexes.add(y)
# transform the set of index to a list, and sort it
separation_index = list(indexes)
separation_index.sort()
return score, separation_index
def separate_artists( tracks, separation=4 ):
"""
Separate tracks according to the main artist
The order of the tracks returned is random,
but due to the way the separation score is
calculated and processed, multiple runs will
produce similar results.
For true random mode, you need to shuffle the
tracks before calling this function.
Args:
tracks: list of spotify track objects
separation: desired artist separation (optional, default=4)
Returns:
list of artists separated tracks
"""
max_tries = 1000
max_swaps = 100
swaps = 0
# initialise the separation score for the list of tracks, and the index of tracks that need separation
cur_score, indexes = score_artist_separation( tracks, separation )
# try to swap tracks up to a maximum limit of attempts
for i in range(max_tries):
# if we already have total desired separation, or we have not more tracks to attempt separation on, stop
if cur_score == 0 or len(indexes) == 0:
break
# pick one random track, and one random index (track that need separation)
swap_1 = random.choice(indexes)
swap_2 = random.randint(0, len(tracks) - 1)
# swap tracks
tmp = tracks[swap_1]
tracks[swap_1] = tracks[swap_2]
tracks[swap_2] = tmp
# calculate the new artist separation score
new_score, new_indexes = score_artist_separation(tracks, separation)
# if the new score is worse (higher) than the current one, revert the swap
if new_score >= cur_score:
# swap tracks
tmp = tracks[swap_1]
tracks[swap_1] = tracks[swap_2]
tracks[swap_2] = tmp
# increment the swap attempt counter, and terminate if we have exceeded the max number of swaps
swaps += 1
if swaps > max_swaps:
break
else:
# the new score is better (lower)
# reset the swap attempt counter
swaps = 0
# update the current score
cur_score = new_score
# update the list of indexes for tracks that need separation
indexes = new_indexes
# if we reach a score of 0, total desired separation is achieved, and we can stop
if cur_score == 0:
break
print("Artist separation - Desired:", separation, " - Score:", cur_score, " - Remaining:", str(len(indexes)))
return tracks |
I have been using Order.separateArtists in a few programs, and did not notice this issue until now, That is because I did not examine the results. What I have found is that the function has unexpected results regarding the tracks ordering. I was expecting the first instance of a track by an artist to remain at the same place (or higher if other tracks were moved previously). This is not the case at all. In fact it seems the processing starts from the end of the tracklist, and has the side effect of causing the first instances to be removed if seperation cannot be achieved (as expected). Maybe it is even more unexpected than this, with what seems like randonmess thrown in (or maybe some processing goes one way, while other goes the other way).
For example: I am trying to produce a tracklist keeping only the most popular tracks, with only the most popular track of each artist. This is what I get without artist separation:
(first column is popularity, second artist name, third track name)
As you can see, the source has many artist duplicates, which I need to remove. The easiest way to do it should be to apply artist separation, preserving the order, with a separation space of the desired size. This is what it produces:
Those are actually the least popular tracks!
Reversing the processing improves results but is not quite right:
Compared to the first result, you can see that the Romeo Santos and Aventura tracks are not the most popular. If I request more than 10 results, I can also see other tracks from other artists have disappeared altogether.
I think this is an incorrect logic in the separateArtists function. I wrote a similar function in Python a while ago, and I think it was doing it in the correct order. I will try to dig it up and see if I can help fix the code.
For reference, here is code I used to produce those results:
The text was updated successfully, but these errors were encountered: