Skip to content
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

Support idle #53

Open
Phrogz opened this issue Jan 15, 2016 · 4 comments
Open

Support idle #53

Phrogz opened this issue Jan 15, 2016 · 4 comments

Comments

@Phrogz
Copy link
Contributor

Phrogz commented Jan 15, 2016

The information.rb plugin has an idle command, but it is currently commented out.

I'd like to be able to watch for changes. As it is I'll have to use background threads shelling out to mpc idle to implement the same functionality.

@archseer
Copy link
Owner

@Phrogz
Copy link
Contributor Author

Phrogz commented Jan 17, 2016

https://github.com/archSeer/ruby-mpd#idle

To implement idle, what is needed is a lock that prevents sending commands to the daemon while waiting for the response (except noidle). An intermediate solution would be to queue the commands to send them later, when idle has returned the response.

That's not how I'd do it. Idle is ~useless to me if I can't do anything else once I invoke it. I'd implement idle by having each call to idle spawn another connection in another thread, ideally with an idleloop style option that allows the thread and connection to be re-used indefinitely if desired.

Some background on how I'm using idle, in case it helps shape the reason for my argument above:

My (web-based) music player shows the currently-playing song, progress, volume, and a list of all available playlists, and the "up next" live playlist. I want these to be exactly up-to-date.

I could have the web browser poll the web server, and have the web server poll MPD at high frequency. Turns out to be an ugly amount of network traffic and high server CPU usage. (There are currently over 17k songs in the music database. When all of them are added to the up-next queue, it takes several seconds to ask MPD for the current list.)

I could use ruby-mpd callbacks. However, polling the MPD server at 5Hz for changes seems ugly. I worry that it will also be CPU intensive (though I've not yet tried it.)

Instead, I want to write:

# Starts idling on another (identical) connection under another thread.
# Invokes the block once the idle command returns.
# The continuous:true option causes the idle command to be immediately re-issued
# in the same thread/connection (for convenience and minimal thread thrash).
@mpd.idle_until( 'stored_playlist', continuous:true ){ update_playlists }
@mpd.idle_until( 'playlist database', continuous:true ){ update_upnext }
@mpd.idle_until( 'player mixer options', continuous:true ){ update_status }

Right now I'm using EventMachine.defer to spawn a thread for each of these, and executing mpc idle. I use Faye to send updates to all clients when a change occurs. It's working well at first glance, but I'd rather not shell to mpc when I have ruby-mpd.

def initialize
  @mpd = MPD.new( ENV['MPD_HOST'], ENV['MPD_PORT'] )
  @mpd.connect
  @faye = Faye::Client.new("http://#{ENV['RB3JAY_HOST']}:#{ENV['RB3JAY_PORT']}/faye")
  watch_for_changes
end

def watch_for_changes
  watch_status
  watch_playlists
  watch_upnext
end

def watch_status
  # TODO: replace fast status updates with client-side "dead-reckoning"
  # of progress while playing; notify only for play/pause/song changes.
  # Run at higher than 1Hz so that the progress bar updates ~smoothly.
  EM.add_periodic_timer(0.25) do
    if (info=mpd_status) != @last_status
      send_status( @last_status=info )
    end
  end
end

def watch_playlists
  EM.defer(
    ->( ){ idle_until 'stored_playlist'    },
    ->(_){ send_playlists; watch_playlists }
  )
end

def watch_upnext
  EM.defer(
    ->( ){ idle_until 'playlist', 'database' },
    ->(_){ send_next; watch_upnext           }
  )
end

def idle_until(*events)
  `mpc -h #{ENV['MPD_HOST']} -p #{ENV['MPD_PORT']} idle #{events.join(' ')}`
end

def mpd_status
  @mpd.status
end
def send_status( info=mpd_status )
  @faye.publish '/status', info
end

def up_next
  @mpd.queue.slice(0,ENV['RB3JAY_LISTLIMIT'].to_i).map(&:details)
end
def send_next( songs=up_next )
  @faye.publish '/next', songs
end

def playlists
  @mpd.playlists.map(&:name).grep(/^(?!user-)/).sort
end
def send_playlists( lists=playlists )
  @faye.publish '/playlists', playlists
end

@archseer
Copy link
Owner

I haven't actively been using this library in the past 2 years so I'm unlikely to work on such features, however I will be more than happy to accept pull requests.

@Phrogz
Copy link
Contributor Author

Phrogz commented Jan 19, 2016

I haven't actively been using this library in the past 2 years so I'm unlikely to work on such features, however I will be more than happy to accept pull requests.

Both are good to know. I'll see what I can do. Any suggestion on how to spec or test what I described above (another thread waiting on idle from MPD)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants