Skip to content
This repository
  • 10 commits
  • 22 files changed
  • 0 comments
  • 1 contributor
BIN  app/assets/images/empty-album.png
3  app/assets/javascripts/application.js
@@ -8,4 +8,5 @@
8 8
 //= require jquery-ui
9 9
 //= require jquery_ujs
10 10
 //= require autocomplete-rails
11  
-//= require_tree .
  11
+//= require global
  12
+//= require player
104  app/assets/javascripts/global.js
@@ -32,110 +32,6 @@ $(function() {
32 32
   $('#success').click(fadeCallback);
33 33
   $('#failure').click(fadeCallback);
34 34
 
35  
-  // Track and update the player state.
36  
-  var lastStatus = null;
37  
-  var updateProgress = true;
38  
-  var updateVolume = true;
39  
-
40  
-  function refreshPlayer() {
41  
-    $.getJSON('/player', function (status) {
42  
-      $('.player .controls a').removeClass('submitted');
43  
-
44  
-      if( status.playback_state != 'playing' ) {
45  
-        $('#command_play').removeClass('disable');
46  
-        $('#command_pause').addClass('disable');
47  
-      } else {
48  
-        $('#command_play').addClass('disable');
49  
-        $('#command_pause').removeClass('disable');
50  
-      }
51  
-
52  
-      // Update track data.
53  
-      if (lastStatus == null || status.track_id != lastStatus.track_id) {
54  
-        var albumArtId = status.track_id || 'placeholder';
55  
-        $('.player .album-art').attr('src', '/tracks/' + albumArtId + '/album-art');
56  
-        $('.player .track .title').html(status.title || ' ');
57  
-        $('.player .track .artist').html(status.artist || ' ');
58  
-        $('.player .track .album').html(status.album || ' ');
59  
-      }
60  
-
61  
-      // Update the progress bar.
62  
-      if (updateProgress) {
63  
-        $('.player .progress .bar').css('width', status.percent_complete + '%');
64  
-        $('.player .control-cluster .progress-text').html(status.progress);
65  
-      }
66  
-
67  
-      // Update the volume control.
68  
-      if (updateVolume) {
69  
-        $('.player .volume .mask').css('height', (100 - status.volume) + '%');
70  
-      }
71  
-
72  
-      lastStatus = status;
73  
-    })
74  
-  }
75  
-
76  
-  $('.player .controls a').click(function () {
77  
-    $(this).addClass('submitted');
78  
-    refreshPlayer();
79  
-  })
80  
-
81  
-  // Moving the mouse over the progress bar causes the progress bar to track the X coordinate of the mouse pointer.
82  
-  // Clicking the mouse on the progress bar executes a jump action with the percentage of the X coordinate as a
83  
-  // command parameter.
84  
-  function jumpTracker(event) {
85  
-    var pixels = event.pageX - $(this).offset().left;
86  
-    $('.player .progress .bar').css('width', pixels + 'px');
87  
-  }
88  
-
89  
-  $('.progress').mouseenter(function () {
90  
-    updateProgress = false;
91  
-    $(this).mousemove(jumpTracker);
92  
-  });
93  
-  $('.progress').mouseout(function () {
94  
-    $(this).unbind('mousemove', jumpTracker);
95  
-    if (lastStatus != null) {
96  
-      $('.player .progress .bar').css('width', lastStatus.percent_complete + '%')
97  
-    }
98  
-    updateProgress = true;
99  
-  });
100  
-  $('.progress').click(function (event) {
101  
-    var percent = Math.floor((event.pageX - $(this).offset().left) * 100 / $(this).width());
102  
-    $.ajax({
103  
-      url: '/player',
104  
-      type: 'PUT',
105  
-      data: { 'command': 'jump', 'parameter': percent }
106  
-    });
107  
-  });
108  
-
109  
-  // Moving the mouse over the volume control causes the volume mask to track the Y coordinate of the mouse pointer.
110  
-  // Clicking the mouse on the volume control sends a volume action with the percentage of the Y coordinate as a
111  
-  // command parameter.
112  
-  function volumeTracker(event) {
113  
-    var pixels = event.pageY - $(this).offset().top;
114  
-    $('.player .volume .mask').css('height', pixels + 'px');
115  
-  }
116  
-
117  
-  $('.volume').mouseenter(function () {
118  
-    updateVolume = false;
119  
-    $(this).mousemove(volumeTracker);
120  
-  })
121  
-  $('.volume').mouseout(function () {
122  
-    $(this).unbind('mouseout', volumeTracker);
123  
-    if (lastStatus != null) {
124  
-      $('.player .volume .mask').css('height', (100 - lastStatus.volume) + '%');
125  
-    }
126  
-    updateVolume = true;
127  
-  })
128  
-  $('.volume').click(function (event) {
129  
-    var percent = 100 - Math.floor((event.pageY - $(this).offset().top) * 100 / $(this).height());
130  
-    $.ajax({
131  
-      url: '/player',
132  
-      type: 'PUT',
133  
-      data: { 'command': 'volume', 'parameter': percent }
134  
-    });
135  
-  })
136  
-
137  
-  heartbeat(refreshPlayer);
138  
-
139 35
   // Execute the heartbeat callback.
140 36
   function periodic() {
141 37
     beat()
135  app/assets/javascripts/player.js
... ...
@@ -0,0 +1,135 @@
  1
+// Read and write functionality for the player controls at the top of each page.
  2
+
  3
+function formatSeconds(seconds) {
  4
+  var minutes = Math.floor(seconds / 60);
  5
+  var remaining = Math.floor(seconds - (minutes * 60));
  6
+
  7
+  var trailing = '';
  8
+  if (remaining < 10) {
  9
+    trailing = '0';
  10
+  }
  11
+
  12
+  return minutes + ':' + trailing + remaining;
  13
+}
  14
+
  15
+$(function () {
  16
+  var lastStatus = null;
  17
+  var updateProgress = true;
  18
+  var updateVolume = true;
  19
+
  20
+  function refreshPlayer() {
  21
+    $.getJSON('/player', function (status) {
  22
+      $('.player .controls a').removeClass('submitted');
  23
+
  24
+      if( status.playback_state != 'playing' ) {
  25
+        $('#command_play').removeClass('disable');
  26
+        $('#command_pause').addClass('disable');
  27
+      } else {
  28
+        $('#command_play').addClass('disable');
  29
+        $('#command_pause').removeClass('disable');
  30
+      }
  31
+
  32
+      // Update track data.
  33
+      if (lastStatus == null || status.track_id != lastStatus.track_id) {
  34
+        var albumArtId = status.track_id || 'empty';
  35
+        $('.player .album-art').attr('src', '/tracks/' + albumArtId + '/album-art');
  36
+        $('.player .track .title').html(status.title || ' ');
  37
+        $('.player .track .artist').html(status.artist || ' ');
  38
+        $('.player .track .album').html(status.album || ' ');
  39
+      }
  40
+
  41
+      // Display the error if one is present, or hide it if it has gone away.
  42
+      if (status.error == null) {
  43
+        $('.player .error').css('display', 'none');
  44
+      } else {
  45
+        $('.player .error').html(status.error).css('display', 'block');
  46
+      }
  47
+
  48
+      // Update the progress bar and progress text.
  49
+      if (updateProgress) {
  50
+        var percentComplete = 0;
  51
+        if (status.length != null) {
  52
+          percentComplete = status.seconds * 100 / status.length;
  53
+        }
  54
+
  55
+        $('.player .progress .bar').css('width', percentComplete + '%');
  56
+      }
  57
+
  58
+      // Update the progress text.
  59
+      var progressText = '';
  60
+      if (status.playback_state != 'stopped') {
  61
+        progressText = formatSeconds(status.seconds) + ' / ' + formatSeconds(status.length);
  62
+      }
  63
+      $('.player .control-cluster .progress-text').html(progressText);
  64
+
  65
+      // Update the volume control.
  66
+      if (updateVolume) {
  67
+        $('.player .volume .mask').css('height', (100 - status.volume) + '%');
  68
+      }
  69
+
  70
+      lastStatus = status;
  71
+    })
  72
+  }
  73
+  heartbeat(refreshPlayer);
  74
+
  75
+  $('.player .controls a').click(function () {
  76
+    $(this).addClass('submitted');
  77
+    refreshPlayer();
  78
+  })
  79
+
  80
+  // Moving the mouse over the progress bar causes the progress bar to track the X coordinate of the mouse pointer.
  81
+  // Clicking the mouse on the progress bar executes a jump action with the percentage of the X coordinate as a
  82
+  // command parameter.
  83
+  function jumpTracker(event) {
  84
+    var pixels = event.pageX - $(this).offset().left;
  85
+    $('.player .progress .bar').css('width', pixels + 'px');
  86
+  }
  87
+
  88
+  $('.progress').mouseenter(function () {
  89
+    updateProgress = false;
  90
+    $(this).mousemove(jumpTracker);
  91
+  });
  92
+  $('.progress').mouseleave(function () {
  93
+    $(this).unbind('mousemove', jumpTracker);
  94
+    if (lastStatus != null) {
  95
+      $('.player .progress .bar').css('width', lastStatus.percent_complete + '%')
  96
+    }
  97
+    updateProgress = true;
  98
+  });
  99
+  $('.progress').click(function (event) {
  100
+    var percent = Math.floor((event.pageX - $(this).offset().left) * 100 / $(this).width());
  101
+    $.ajax({
  102
+      url: '/player',
  103
+      type: 'PUT',
  104
+      data: { 'command': 'jump', 'parameter': percent }
  105
+    });
  106
+  });
  107
+
  108
+  // Moving the mouse over the volume control causes the volume mask to track the Y coordinate of the mouse pointer.
  109
+  // Clicking the mouse on the volume control sends a volume action with the percentage of the Y coordinate as a
  110
+  // command parameter.
  111
+  function volumeTracker(event) {
  112
+    var pixels = event.pageY - $(this).offset().top;
  113
+    $('.player .volume .mask').css('height', pixels + 'px');
  114
+  }
  115
+
  116
+  $('.volume').mouseenter(function () {
  117
+    updateVolume = false;
  118
+    $(this).mousemove(volumeTracker);
  119
+  })
  120
+  $('.volume').mouseleave(function () {
  121
+    $(this).unbind('mousemove', volumeTracker);
  122
+    if (lastStatus != null) {
  123
+      $('.player .volume .mask').css('height', (100 - lastStatus.volume) + '%');
  124
+    }
  125
+    updateVolume = true;
  126
+  })
  127
+  $('.volume').click(function (event) {
  128
+    var percent = 100 - Math.floor((event.pageY - $(this).offset().top) * 100 / $(this).height());
  129
+    $.ajax({
  130
+      url: '/player',
  131
+      type: 'PUT',
  132
+      data: { 'command': 'volume', 'parameter': percent }
  133
+    });
  134
+  })
  135
+});
5  app/assets/stylesheets/global.css.scss
@@ -95,6 +95,7 @@ $headingheight: $heading - (2 * $headingpad);
95 95
   .disable { display: none !important }
96 96
 
97 97
   .error {
  98
+    display: none ;
98 99
     position: absolute ;
99 100
     top: 5% ;
100 101
     right: 7% ;
@@ -163,6 +164,8 @@ $headingheight: $heading - (2 * $headingpad);
163 164
     }
164 165
 
165 166
     .track {
  167
+      height: 70px ;
  168
+      margin: 0 ;
166 169
       background: none ;
167 170
 
168 171
       .title {
@@ -183,7 +186,7 @@ $headingheight: $heading - (2 * $headingpad);
183 186
     .progress {
184 187
       cursor: pointer ;
185 188
       position: relative ;
186  
-      margin: 5px 15px 0 0 ;
  189
+      margin-right: 15px ;
187 190
       border-top: solid 1px black ;
188 191
       height: 30px ;
189 192
       .bar {
14  app/controllers/application_controller.rb
... ...
@@ -1,17 +1,3 @@
1 1
 class ApplicationController < ActionController::Base
2 2
   protect_from_forgery
3  
-  before_filter :create_player_client
4  
-
5  
-  private
6  
-
7  
-  def create_player_client
8  
-    @player = Player.new
9  
-    @player.ok?
10  
-
11  
-    @status = @player.status
12  
-    if @status.track_id
13  
-      @status.track = Track.where(:id => @status.track_id).first
14  
-    end
15  
-  end
16  
-
17 3
 end
15  app/controllers/players_controller.rb
... ...
@@ -1,4 +1,7 @@
  1
+require 'mpg123player/common'
  2
+
1 3
 class PlayersController < ApplicationController
  4
+  before_filter :create_player_client
2 5
 
3 6
   def show
4 7
     render :json => @status
@@ -13,4 +16,16 @@ def update
13 16
     end
14 17
   end
15 18
 
  19
+  private
  20
+
  21
+  def create_player_client
  22
+    @player = Mpg123Player::Configuration.client_class.new
  23
+    @player.ok?
  24
+
  25
+    @status = @player.status
  26
+    if @status.track_id
  27
+      @track = Track.where(:id => @status.track_id).first
  28
+    end
  29
+  end
  30
+
16 31
 end
10  app/controllers/tracks_controller.rb
@@ -33,12 +33,10 @@ def show_album
33 33
   end
34 34
 
35 35
   def album_art
36  
-    if params[:id] == 'placeholder'
37  
-      art = AlbumArt.default
38  
-    else
39  
-      @track = Track.find(params[:id])
40  
-      art = @track.album_art
41  
-    end
  36
+    redirect_to view_context.image_path('empty-album.png') and return if params[:id] == 'empty'
  37
+    redirect_to view_context.image_path('missing-album.png') and return if params[:id] == 'placeholder'
  38
+
  39
+    art = Track.find(params[:id]).album_art
42 40
 
43 41
     response.headers['x-placeholder-art'] = 'true' if art.default?
44 42
     if art && art.ok?
43  app/models/client.rb
... ...
@@ -0,0 +1,43 @@
  1
+require 'mpg123player/common'
  2
+
  3
+# Non-ActiveRecord model. Manage status and communications with the mpg123 player process.
  4
+class Client
  5
+  include Mpg123Player
  6
+  include Configurable
  7
+
  8
+  attr_reader :error
  9
+
  10
+  def initialize
  11
+    configure
  12
+  end
  13
+
  14
+  # Issue a command to the server, synchronously if appropriate.
  15
+  def command string, parameter = nil
  16
+    raise '#command not implemented'
  17
+  end
  18
+
  19
+  # Status
  20
+
  21
+  # Return false and set +error+ if the player is in a bad state.
  22
+  def ok?
  23
+    true
  24
+  end
  25
+
  26
+  # Return a Status object representing the current state of the player, ready for JSON rendering.
  27
+  def status
  28
+    Status.stopped
  29
+  end
  30
+
  31
+  def playing?
  32
+    status.playback_state == 'playing'
  33
+  end
  34
+
  35
+  def paused?
  36
+    status.playback_state == 'paused'
  37
+  end
  38
+
  39
+  def stopped?
  40
+    status.playback_state == 'stopped'
  41
+  end
  42
+
  43
+end
5  app/models/development_client.rb
... ...
@@ -0,0 +1,5 @@
  1
+# This client is for interactive development mode (even on the same system that the real websinger-player is running
  2
+# on.) It simulates the "playing" of music and advancement through tracks in real-time with a background thread, but
  3
+# doesn't actually produce any audio.
  4
+class DevelopmentClient < Client
  5
+end
73  app/models/player.rb
... ...
@@ -1,73 +0,0 @@
1  
-require 'mpg123player/common'
2  
-require 'active_support/json'
3  
-
4  
-# Non-ActiveRecord model. Manage status and communications with the mpg123 player process.
5  
-class Player
6  
-  include Mpg123Player
7  
-  include Configurable
8  
-
9  
-  attr_reader :error
10  
-
11  
-  def initialize
12  
-    configure
13  
-  end
14  
-
15  
-  # Controls
16  
-
17  
-  def play ; command 'play' ; end
18  
-  def pause ; command 'pause' ; end
19  
-  def volume percent ; command 'volume', percent ; end
20  
-  def restart ; command 'restart' ; end
21  
-  def skip ; command 'skip' ; end
22  
-  def stop ; command 'stop' ; end
23  
-  def shutdown ; command 'shutdown' ; end
24  
-
25  
-  # Issue a command to the server. Return the command object.
26  
-  def command string, parameter = nil
27  
-    PlayerCommand.create!(:action => string, :parameter => parameter)
28  
-  end
29  
-
30  
-  # Status
31  
-
32  
-  def ok?
33  
-    unless File.readable?(@pid_path)
34  
-      @error = "Can't read the pid file at #{@pid_path}!"
35  
-      return false
36  
-    end
37  
-    pid = File.open(@pid_path) { |f| f.gets(nil) }.to_i
38  
-    Process.kill(0, pid)
39  
-  rescue Errno::ESRCH => e
40  
-    @error = "The server died!"
41  
-    false
42  
-  rescue Errno::EPERM => e
43  
-    @error = "Someone else is running the server!"
44  
-    false
45  
-  else
46  
-    true
47  
-  end
48  
-
49  
-  def status
50  
-    @status ||= Status.from(JSON(status_json))
51  
-  end
52  
-
53  
-  def status_json
54  
-    if File.exist?(@status_path)
55  
-      File.open(@status_path) { |f| f.gets(nil) }
56  
-    else
57  
-      Status.stopped.to_json
58  
-    end
59  
-  end
60  
-
61  
-  def playing?
62  
-    status.playback_state == 'playing'
63  
-  end
64  
-
65  
-  def paused?
66  
-    status.playback_state == 'paused'
67  
-  end
68  
-
69  
-  def stopped?
70  
-    status.playback_state == 'stopped'
71  
-  end
72  
-
73  
-end
54  app/models/production_client.rb
... ...
@@ -0,0 +1,54 @@
  1
+require 'active_support/json'
  2
+
  3
+# Client implementation for use in production. This Client actually communicates with a remote Mpg123player::Server
  4
+# process, lauched with `rake websinger:player`, and results in actual music playing over the speakers.
  5
+class ProductionClient < Client
  6
+
  7
+  # Issue a command to the server and wait for the remote process to handle it, within the currently configured
  8
+  # +command_timeout+.
  9
+  def command string, parameter = nil
  10
+    cmd = PlayerCommand.create!(:action => string, :parameter => parameter)
  11
+
  12
+    Rails.logger.silence(WARN) do
  13
+      wait_time = 0
  14
+      until wait_time >= @command_timeout
  15
+        wait_time += sleep(@command_poll)
  16
+        return true unless PlayerCommand.exist?(cmd.id)
  17
+      end
  18
+    end
  19
+
  20
+    # Timed out waiting for command acceptance
  21
+    false
  22
+  end
  23
+
  24
+  # Query the current player status, setting +error+ and returning false if something is wrong.
  25
+  def ok?
  26
+    unless File.readable?(@pid_path)
  27
+      @error = "Can't read the pid file at #{@pid_path}!"
  28
+      return false
  29
+    end
  30
+    pid = File.open(@pid_path) { |f| f.gets(nil) }.to_i
  31
+    Process.kill(0, pid)
  32
+  rescue Errno::ESRCH => e
  33
+    @error = "The server died!"
  34
+    false
  35
+  rescue Errno::EPERM => e
  36
+    @error = "Someone else is running the server!"
  37
+    false
  38
+  else
  39
+    true
  40
+  end
  41
+
  42
+  # Populate the Status object by reading the JSON written to disk by the server process, amending it with our local
  43
+  # +error+ if one is present.
  44
+  def status
  45
+    @status ||= if File.readable?(@status_path)
  46
+      Status.from(JSON(File.read(@status_path)))
  47
+    else
  48
+      Status.stopped
  49
+    end
  50
+    @status.error = @error
  51
+    @status
  52
+  end
  53
+
  54
+end
4  app/models/test_client.rb
... ...
@@ -0,0 +1,4 @@
  1
+# A Client implementation for use in test cases. Requires explicit methods to drive its lifecycle (advancing through
  2
+# tracks and so on).
  3
+class TestClient < Client
  4
+end
21  app/views/players/_control.html.haml
... ...
@@ -1,20 +1,19 @@
1 1
 .player
2  
-  = image_tag album_art_track_path(@status.track_id || 'placeholder'), :class => 'album-art', :alt => 'Album art', :size => '90x90'
  2
+  = image_tag image_path('empty-album.png'), :class => 'album-art', :alt => 'Album art', :size => '90x90'
3 3
   .volume
4  
-    .mask{ :style => "height: #{100 - @status.volume}%" }
5  
-  - if @player.error
6  
-    %p.error= @player.error
  4
+    .mask{ :style => "height: 0%" }
  5
+  %p.error
7 6
   .central
8 7
     .control-cluster
9 8
       %p.controls
10 9
         = command_link 'restart'
11  
-        = command_link 'pause', @player.playing?
12  
-        = command_link 'play', ! @player.playing?
  10
+        = command_link 'pause', false
  11
+        = command_link 'play'
13 12
         = command_link 'skip'
14  
-      %p.progress-text= @status.progress_s
  13
+      %p.progress-text
15 14
     .track
16  
-      %p.title= @status.title
17  
-      %p.artist= @status.artist
18  
-      %p.album= @status.album
  15
+      %p.title
  16
+      %p.artist
  17
+      %p.album
19 18
     .progress
20  
-      .bar{ :style => "width: #{@status.percent_complete}%" }
  19
+      .bar{ :style => "width: 0%" }
5  config/environments/development.rb
... ...
@@ -1,3 +1,5 @@
  1
+require 'mpg123player/common'
  2
+
1 3
 Websinger::Application.configure do
2 4
   # Settings specified here will take precedence over those in config/application.rb
3 5
 
@@ -27,4 +29,7 @@
27 29
 
28 30
   # Expands the lines which load the assets
29 31
   config.assets.debug = true
  32
+
  33
+  # Use the development client (which plays no actual sound).
  34
+  Mpg123Player::Configuration.client_class = DevelopmentClient
30 35
 end
3  config/environments/production.rb
@@ -57,4 +57,7 @@
57 57
 
58 58
   # Send deprecation notices to registered listeners
59 59
   config.active_support.deprecation = :notify
  60
+
  61
+  # Use the production client.
  62
+  Mpg123Player::Configuration.client_class = ProductionClient
60 63
 end
3  config/environments/test.rb
@@ -36,4 +36,7 @@
36 36
 
37 37
   # Print deprecation notices to the stderr
38 38
   config.active_support.deprecation = :stderr
  39
+
  40
+  # Use the testing client (which requires explicit driving methods).
  41
+  Mpg123Player::Configuration.client_class = TestClient
39 42
 end
10  config/initializers/001_mpg123player.rb → config/initializers/mpg123player.rb
@@ -14,21 +14,21 @@
14 14
 
15 15
 # The home directory for the player daemon's user.  Must be owned by the daemon user.
16 16
 #
17  
-# Mpg123Player::Configuration.base_path = '/var/websinger'
  17
+# Mpg123Player::Configuration.base_path = Rails.root.join('tmp')
18 18
 
19 19
 # A path used to communicate the status of the player daemon's process.
20 20
 #
21  
-# Mpg123Player::Configuration.pid_path = '/var/websinger/player.pid'
  21
+# Mpg123Player::Configuration.pid_path = Rails.root.join('tmp', 'pids', 'player.pid')
22 22
 
23 23
 # This file will be written periodically by the player daemon with a data structure containing information about the
24 24
 # current track.
25 25
 #
26  
-# Mpg123Player::Configuration.status_path = '/var/websinger/status.yaml'
  26
+# Mpg123Player::Configuration.status_path = Rails.root.join('tmp', 'status.yaml')
27 27
 
28 28
 # A log file containing output from the player daemon.
29 29
 #
30  
-# Mpg123Player::Configuration.log_path = '/var/websinger/player.log'
  30
+# Mpg123Player::Configuration.log_path = Rails.root.join('log', 'player.log')
31 31
 
32 32
 # Another log file that traps the $stderr output from the player.  Look here for ALSA errors and so on.
33 33
 #
34  
-# Mpg123Player::Configuration.error_log_path = '/var/websinger/errors.log'
  34
+# Mpg123Player::Configuration.error_log_path = Rails.root.join('log', 'player.err.log')
90  lib/mpg123player/common.rb
@@ -2,26 +2,11 @@ module Mpg123Player
2 2
 
3 3
 module Configuration
4 4
   class << self
5  
-    def player_path ; @player_path || '/usr/bin/mpg123' ; end
6  
-    def player_path= path ; @player_path = path ; end
7  
-
8  
-    def base_path ; @base_path || '/var/websinger' ; end
9  
-    def base_path= path ; @base_path = path ; end
10  
-
11  
-    def pid_path ; @pid_path || "#{base_path}/player.pid" ; end
12  
-    def status_path ; @status_path || "#{base_path}/status.yaml" ; end
13  
-    def log_path ; @log_path || "#{base_path}/player.log" ; end
14  
-    def error_log_path ; @error_log_path || "#{base_path}/errors.log" ; end
15  
-    def server_port ; @server_port || 12340 ; end
16  
-
17  
-    def pid_path= path ; @pid_path = path ; end
18  
-    def status_path= path ; @status_path = path ; end
19  
-    def log_path= path ; @log_path = path ; end
20  
-    def error_log_path= path ; @error_log_path = path ; end
21  
-    def server_port= port ; @server_port = port ; end
22  
-
23  
-    def user ; @username || 'websinger' ; end
24  
-    def user= name ; @username = name ; end
  5
+    attr_accessor :player_path, :user
  6
+    attr_accessor :base_path, :pid_path, :status_path, :log_path, :error_log_path
  7
+    attr_accessor :command_poll, :command_timeout
  8
+
  9
+    attr_accessor :client_class
25 10
   end
26 11
 end
27 12
 
@@ -29,78 +14,79 @@ module Configurable
29 14
   attr_accessor :log_path, :error_log_path
30 15
 
31 16
   def configure
32  
-    @player_path = Configuration.player_path
33  
-    @status_path = Configuration.status_path
34  
-    @pid_path = Configuration.pid_path
35  
-    @log_path = Configuration.log_path
36  
-    @error_log_path = Configuration.error_log_path
37  
-    @server_port = Configuration.server_port
  17
+    @player_path = Configuration.player_path || '/usr/bin/mpg123'
  18
+    @base_path = Configuration.base_path || Rails.root.join('tmp')
  19
+    @status_path = Configuration.status_path || @base_path.join('status.yaml')
  20
+    @pid_path = Configuration.pid_path || @base_path.join('pids', 'player.pid')
  21
+    @log_path = Configuration.log_path || Rails.root.join('log', 'player.log')
  22
+    @error_log_path = Configuration.error_log_path || Rails.root.join('log', 'player.err.log')
  23
+    @command_poll = (Configuration.command_poll || 0.1).to_f
  24
+    @command_timeout = (Configuration.command_timeout || 5).to_f
38 25
   end
39 26
 end
40 27
 
41 28
 Commands = %q{play pause jump volume restart skip stop shutdown}
42 29
 
43 30
 class Status
44  
-  attr_accessor :title, :artist, :album, :track_id
45  
-  attr_accessor :seconds, :track_length, :volume
  31
+  attr_accessor :playback_state, :error
  32
+  attr_accessor :artist, :album, :title, :length, :track_id
  33
+  attr_accessor :seconds, :volume
46 34
 
47 35
   attr_accessor :track
48 36
 
49  
-  attr_accessor :playback_state
50  
-
51 37
   def initialize
52 38
     @playback_state = :playing
53 39
     @seconds = 0
54  
-    @volume = 0
55  
-  end
56  
-
57  
-  def percent_complete
58  
-    @track ? (@seconds / @track.length) * 100 : 0
  40
+    @volume = 100
59 41
   end
60 42
 
61  
-  def progress_s
62  
-    @track ? "#{to_minutes_s @seconds} / #{to_minutes_s @track.length}" : '0:00 / 0:00'
  43
+  # Update this status object to reflect that +track+ is now playing, reseting playback progress.
  44
+  def on_track track
  45
+    @track = track
  46
+    @track_id = track.id
  47
+    @artist = track.artist
  48
+    @album = track.album
  49
+    @title = track.title
  50
+    @length = track.length
  51
+    @seconds = 0
63 52
   end
64 53
 
65 54
   def clear
66  
-    @title = @artist = @album = @track_id = nil
  55
+    @artist = @album = @title = @length = @track_id = @track = nil
67 56
     @seconds = 0
  57
+    @volume = 100
  58
+    @error = nil
68 59
   end
69 60
 
  61
+  # Determine whether or not this Status has changed sufficiently from +other+ to justify rewriting it to disk.
70 62
   def is_close_to? other
71  
-    return false unless @title == other.title
  63
+    return false unless @playback_state == other.playback_state
72 64
     return false unless @artist == other.artist
73 65
     return false unless @album == other.album
  66
+    return false unless @title == other.title
  67
+    return false unless @length == other.length
74 68
     return false unless @track_id == other.track_id
75 69
     return false unless @volume == other.volume
76  
-    return false unless @playback_state == other.playback_state
  70
+    return false unless @error == other.error
77 71
 
78 72
     return false unless (@seconds - other.seconds).abs < 1
79 73
 
80 74
     true
81 75
   end
82 76
 
  77
+  # Convert this Status object to a Hash for conversion to JSON.
83 78
   def to_hash
84 79
     { :playback_state => @playback_state,
  80
+      :error => @error,
85 81
       :artist => @artist,
86 82
       :album => @album,
87 83
       :title => @title,
  84
+      :length => @length,
88 85
       :track_id => @track_id,
89 86
       :seconds => @seconds,
90  
-      :volume => @volume,
91  
-      :progress => progress_s,
92  
-      :percent_complete => percent_complete }
93  
-  end
94  
-
95  
-  protected
96  
-
97  
-  def to_minutes_s seconds
98  
-    minutes = seconds.to_i / 60
99  
-    "#{minutes}:#{(seconds - minutes * 60).to_i.to_s.rjust(2, '0')}"
  87
+      :volume => @volume }
100 88
   end
101 89
 
102  
-  public
103  
-
104 90
   def self.stopped
105 91
     inst = new
106 92
     inst.playback_state = :stopped
70  lib/mpg123player/player.rb → lib/mpg123player/server.rb
@@ -8,7 +8,7 @@
8 8
 
9 9
 module Mpg123Player
10 10
 
11  
-class Player
  11
+class Server
12 12
   include Configurable
13 13
   include ActiveSupport::BufferedLogger::Severity
14 14
 
@@ -18,21 +18,22 @@ class Player
18 18
 
19 19
   def initialize poll_time = 1, log_level = Logger::INFO
20 20
     configure
  21
+
  22
+    # Initialize the logger.
  23
+    @logger = Logger.new(@log_path, 1, 1024 * 1024)
  24
+    @logger.level = log_level
  25
+
21 26
     check_paths
22 27
 
23 28
     @poll_time = 1 # Seconds, may be fractional
24  
-    @status = Status.new
  29
+    @status = Status.stopped
25 30
     @last_status = @status.dup
26 31
     @shutting_down = false
27  
-
28  
-    # Initialize the logger.
29  
-    @logger = Logger.new(@log_path, 1, 1024 * 1024)
30  
-    @logger.level = log_level
31 32
   end
32 33
 
33 34
   # Lifecycle events.
34 35
 
35  
-  # Open a pipe to the player executible and start the parsing loop. Only return once a shutdown command is processed.
  36
+  # Open a pipe to the player executable and start the parsing loop. Only return once a shutdown command is processed.
36 37
   def main_loop
37 38
     @logger.info 'Starting player.'
38 39
     @pipe = IO.popen("#{@player_path} -R -", 'w+')
@@ -72,9 +73,8 @@ def advance
72 73
   # Player process controls.
73 74
 
74 75
   def load_track t, state = :playing
75  
-    @status.track_id = t.id
  76
+    @status.on_track t
76 77
     @status.playback_state = state
77  
-    @status.seconds = 0
78 78
 
79 79
     command = state == :playing ? 'L' : 'LP'
80 80
     execute "#{command} #{t.path}"
@@ -90,12 +90,7 @@ def pause_action
90 90
     execute 'P' if @status.playback_state != :paused
91 91
   end
92 92
 
93  
-  # TODO remove when player controls are reorganized.
94  
-  def stop_action
95  
-    execute 'S' if @status.playback_state != :stopped
96  
-  end
97  
-
98  
-  # Jump to an absolute track position in seconds.
  93
+  # Jump to an absolute track position, specified in seconds.
99 94
   def jump_action seconds
100 95
     unless seconds =~ /[0-9]+/
101 96
       @logger.error "Invalid jump offset: #{seconds}"
@@ -123,10 +118,14 @@ def restart_action
123 118
     update_status
124 119
   end
125 120
 
126  
-  # Skip to the next enqueued track, if one is present. Do nothing if the queue is empty.
  121
+  # Skip to the next enqueued track, if one is present. Stop playback if the queue is empty.
127 122
   def skip_action
128 123
     e = EnqueuedTrack.top
129  
-    load_track(e.track, @status.playback_state) unless e.nil?
  124
+    if e.nil?
  125
+      execute 'S'
  126
+    else
  127
+      load_track(e.track, @status.playback_state)
  128
+    end
130 129
   end
131 130
 
132 131
   # Unload the track and stop the current player.
@@ -169,54 +168,29 @@ def process_line line
169 168
     # Jump feedback.
170 169
     return if line =~ /^@J/
171 170
 
172  
-    # ID3v2 metadata tags
173  
-    if md = /^@I ID3v2.([^:]+):(.+)/.match(line)
174  
-      getter, setter = md[1], "#{md[1]}="
175  
-      if @status.respond_to?(setter) && @status.respond_to?(getter)
176  
-        @status.send(setter, md[2].strip) if @status.send(getter).nil?
177  
-        update_status
178  
-      else
179  
-        @logger.debug "Ignoring tag: #{getter}"
180  
-      end
181  
-      return
182  
-    end
183  
-
184  
-    # ID3 tag
185  
-    if md = /^@I ID3:(.{30})(.{30})(.{30})/.match(line)
186  
-      @status.title = md[1].strip
187  
-      @status.artist = md[2].strip
188  
-      @status.album = md[3].strip
189  
-      update_status
190  
-      return
191  
-    end
192  
-
193  
-    # In the absense of parseable ID3 data just grab whatever
194  
-    if md = /^@I (.+)/.match(line)
195  
-      @status.title ||= md[1]
196  
-      update_status
197  
-      return
198  
-    end
  171
+    # ID3v2 metadata tags. We pull track data from ActiveRecord instead.
  172
+    return if line =~ /^@I/
199 173
 
200  
-    # Frame info (during playback)
  174
+    # Frame info (during playback).
201 175
     if md = /^@F [0-9-]+ [0-9-]+ ([0-9.-]+) ([0-9.-]+)/.match(line)
202 176
       @status.seconds = md[1].to_f
203 177
       update_status
204 178
       return
205 179
     end
206 180
 
207  
-    # Playing status changed
  181
+    # Playing status changed.
208 182
     if md = /^@P (\d+)/.match(line)
209 183
       transition_to_state([:stopped, :paused, :playing][md[1].to_i])
210 184
       return
211 185
     end
212 186
 
213  
-    # Error
  187
+    # Error.
214 188
     if md = /^@E (.+)/.match(line)
215 189
       @logger.error md[1]
216 190
       return
217 191
     end
218 192
 
219  
-    # Volume
  193
+    # Volume change.
220 194
     if md = /^@V (\d+)/.match(line)
221 195
       @status.volume = md[1].to_i
222 196
       update_status
6  lib/tasks/player.rake
@@ -5,10 +5,10 @@ namespace :websinger do
5 5
 
6 6
   desc 'The MP3 player daemon.  Should be managed by init.'
7 7
   task :player => :environment do
8  
-    require 'mpg123player/player'
  8
+    require 'mpg123player/server'
9 9
 
10  
-    player = Mpg123Player::Player.new
11  
-    player.main_loop
  10
+    server = Mpg123Player::Server.new
  11
+    server.main_loop
12 12
   end
13 13
 
14 14
 end
13  test/functional/tracks_controller_test.rb
@@ -25,9 +25,16 @@ class TracksControllerTest < ActionController::TestCase
25 25
 
26 26
   test "explicit request for placeholder album art" do
27 27
     get :album_art, { :id => 'placeholder' }
28  
-    assert_response :success
29  
-    assert_equal 'image/png', @response.header['Content-Type']
30  
-    assert_equal 'true', @response.header['x-placeholder-art']
  28
+
  29
+    assert_response :redirect
  30
+    assert @response.header['Location'].ends_with?('missing-album.png')
  31
+  end
  32
+
  33
+  test "request for empty album art" do
  34
+    get :album_art, { :id => 'empty' }
  35
+
  36
+    assert_response :redirect
  37
+    assert @response.header['Location'].ends_with?('empty-album.png')
31 38
   end
32 39
 
33 40
 end

No commit comments for this range

Something went wrong with that request. Please try again.