johnvuko / spotify-playlist

Remove the songs added to the playlist "Remove from spotify" from every playlists
https://spotify-playlist.eivo.fr
MIT License
6 stars 1 forks source link

Remove recently played songs from their playlist (if it's configured for it) #3

Open normanr opened 5 years ago

normanr commented 5 years ago

I'd like a way to have my queued songs stored in an auto-managed playlist. This would make it easier to have different queues for morning vs evening, or driving vs home vs office. (Also would store the queue more resiliently when the app updates !?!)

The service would need to read from recently played songs, and if the play context was a whitelisted playlist, and the song is at the beginning of the playlist, then remove the song from the playlist.

The playlist whitelist would need to be configured on the config screen either multi-choice of existing playlists, or way to create new managed playlists, or maybe just require a fixed tag like "[auto-queue]" in the playlist name.

normanr commented 5 years ago

e.g. this could be used for https://community.spotify.com/t5/Live-Ideas/All-Platforms-Remember-Position-in-Playlist-Album/idi-p/55725 and the various other ideas for multiple or shared playback queues.

johnvuko commented 5 years ago

Hi, I'm not sure to fully understand what you want. What do you mean by "queue"? I only see the current songs playing.

From my understanding, you want a playlist with the content changing depending of the context (hour / location). Can't you just create a playlist home, office, driving...

normanr commented 5 years ago

The behaviour today when switching between playlists is to restart at the beginning of the playlist, but I want to skip songs I've already listened to. So I'd like some way to automatically remove songs as they're played. (I currently manually skip to the last played song, but it's painful to do that with very long playlists.)

For example if the playlist "Foo [queue]" contains [s1, s2, s3, s4, s5], and s1 and s2 are played (and s3 is mid-way playing or paused), then s1 and s2 would be removed so that "Foo [queue]" becomes [s3, s4, s5]. Next time I listen to that playlist it'll start with s3 (and not play s1 or s2 again).

When all the songs on the queue playlist have been played the queue playlist will be empty (which is why it needs to be a filter/opt-in for certain playlists only), but I also though of adding the played songs to a "Foo [played]" playlist before removing from the "Foo [queue]" playlist to keep a history of the songs that were played from the queue playlist. From the previous example the played playlist would then contain [s1, s2]. (And once the queue playlist is empty the played playlist could be re-added to the queue playlist if you wanted to listen to them again, or not if you don't)

With regards to location or device, I'll just create multiple 'queue' playlists and switch between them manually.

johnvuko commented 5 years ago

Ok, unfortunately I can't do that. It will require to develop a mobile app and even if, I don't think I have enough control over Spotify.

normanr commented 5 years ago

Retrieving recently played tracks is already in the Spotify API. It's documented here: https://developer.spotify.com/documentation/web-api/reference/player/get-recently-played/

johnvuko commented 5 years ago

Yes but I would still need to create a mobile application. What you want need to be "real time", so I would need to make a lot of calls to the Spotify API. Currently, I'm calling their API every 30 minutes and I still have a lot of errors 500 and rate limit.

Also this is not in the scope of this project, which is dedicated to solve the problem discussed here https://community.spotify.com/t5/Live-Ideas/Delete-song-from-playlist-in-Now-playing-menu-Mobile/idc-p/1711248.

normanr commented 5 years ago

It doesn't need to be real-time as recently played returns the last 50 songs by default. Checking every 30 or 60 minutes would be more than sufficient, because I only return to a playlist 8-12 hours later.

Within your existing framework I'm guessing this logic would be 20-40 lines. Should I take a stab at what I think it would be and send a pull request? (I'm not sure if I'll be able to test the changes, so it may not even compile at first attempt, but the logic and intent should be clear)

johnvuko commented 5 years ago

Maybe you can write a pseudo logic / code here, in comment.

What I see:

But I would need to create a database table to let you select what playlists you want to have this system, to check if the playlist still exists in Spotify, create and remove auto managed playlists depending of this. For avoid useless requests to Spotify API (which has a rate limit very low) I will have to store in the database, the last play song timestamp's to avoid unnecessary requests.

With all of this, you will only have a playlist without the previous listened songs, but if you loop on the playlist, you will not have this previous songs. Also if you go listen another playlist and come back on the first one, you will start again from the beginning because by listening the second one you will have lost the last played songs from the first one and the system will have put back the songs.

We can't do a "communicating vessels system" because I don't know what playlist you are listening to, I can only duplicate a playlist and remove already played songs.

But all of this seems to be more than 40 lines of code.

normanr commented 5 years ago

yes, i think the logic can be very similar to what you described (and even simpler) because we can require the user to create the playlist with a given suffix and add tracks to it. Once the playlist is played through it will be empty and it's up to the user to refill it however they want. The recently played API returns the context for tracks which links back to the playlist or album, so that can be used to know which playlist to remove songs from. Including comments and without the saved played it's just over 40 lines; the saved played features adds less than another 20 lines:

in db/schema:

t.datetime last_checked
t.boolean saved_played  # would probably need default and changes to app/views/home/index.html.erb and app/controllers/users_controller.rb too

in app/models/user.rb:

# existing return statements in spotify may need to be changed, or this logic can be added to another method and called before any early exits
# this re-uses playlist_ids from spotify

last_checked = self.last_checked || Time.now.to_i * 1000  # don't process entire history of new users
history = client.recently_played(after=last_checked)
for item in history.items.sort_by(|i| i.played_at)  # sort is required for saved_played
  next if !%w{playlist playlist-v2}.include? item.context.type 
  playlist_id = item.context.uri.rpartition(':')[2]
  if !playlist_ids.include?(playlist_id)
    # why don't we know about the playlist???
    # should playlist-read-collaborative be added to the default scope in config/initializers/omniauth.rb?
    next
  end
  next if !playlist_ids[playlist_id].name.endswith(' [queue]')  # do we need an opt-in preference or is this suffix check sufficient?
  if !tracks_by_playlist_id.include?(playlist_id)
    tracks_by_playlist_id[playlist_id] = client.tracks(playlist_id)
    # the above call could be optimized as it's unnecessary to paginate until 
    # position_to_check exceeds the page size
  end
  playlist_tracks = tracks_by_playlist_id[playlist_id]
  track_index = playlist_tracks.find_index(|t| t.uri == item.track.uri]
  next if track_index.nil?
  if !tracks_to_remove_by_playlist_id.include?(playlist_id)
    tracks_to_remove_by_playlist_id[playlist_id] = []
  end
  tracks_to_remove_by_playlist_id[playlist_id].push({
    uri: item.track.uri
    position: track_index
  })
end
for playlist_id, tracks in tracks_to_remove_by_playlist_id.each
  playlist = playlist_ids[playlist_id]
  if self.saved_played
    # this feature is optional, it provides an history of what played
    played_name = playlist['name'].sub(' [queue]', ' [played]')
    played_lists = playlists.select {|p| p['name'] == played_name }
    if played_lists.size > 1
      played = client.fix_duplicates(played)
    elif played_lists.size = 1
      played = played[0]
    else:
      played = client.create_playlist(played_name)  # and update existing caller to pass SpotifyService::PLAYLIST_NAME
    client.add_tracks(played['id'], tracks.map {|x| { uri: x['track']['uri'] } })
  end
  client.delete_tracks_with_position(playlist, tracks)
end
self.update_column(:last_checked, history.cursors.after)

in services/spotify_service.rb:

# add name parameter to existing method
def create_playlist(name)
   params = { name = name, ... }
   ...

# add new methods
def recently_played(after)
  params = {
    after: after
  }
  request(:get, "me/player/recently-played", params)

def delete_tracks_with_position(playlist, tracks)
  playlist_id = playlist['id']
  params = {
    tracks: tracks
    snapshot_id: playlist['snapshot_id']
  }
  request(:delete, "playlists/#{playlist_id}/tracks", params)