cultome_player 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +24 -0
  4. data/.rspec +2 -0
  5. data/.travis.yml +7 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +325 -0
  9. data/Rakefile +8 -0
  10. data/bin/cultome_player +39 -0
  11. data/config/environment.yml +28 -0
  12. data/cultome_player.gemspec +35 -0
  13. data/db/001_create_schema.rb +58 -0
  14. data/lib/cultome_player.rb +107 -0
  15. data/lib/cultome_player/command.rb +11 -0
  16. data/lib/cultome_player/command/language.rb +61 -0
  17. data/lib/cultome_player/command/processor.rb +165 -0
  18. data/lib/cultome_player/command/reader.rb +86 -0
  19. data/lib/cultome_player/environment.rb +130 -0
  20. data/lib/cultome_player/events.rb +29 -0
  21. data/lib/cultome_player/media.rb +47 -0
  22. data/lib/cultome_player/objects.rb +15 -0
  23. data/lib/cultome_player/objects/album.rb +21 -0
  24. data/lib/cultome_player/objects/artist.rb +18 -0
  25. data/lib/cultome_player/objects/command.rb +37 -0
  26. data/lib/cultome_player/objects/drive.rb +26 -0
  27. data/lib/cultome_player/objects/genre.rb +16 -0
  28. data/lib/cultome_player/objects/parameter.rb +37 -0
  29. data/lib/cultome_player/objects/response.rb +42 -0
  30. data/lib/cultome_player/objects/song.rb +38 -0
  31. data/lib/cultome_player/player.rb +13 -0
  32. data/lib/cultome_player/player/adapter.rb +14 -0
  33. data/lib/cultome_player/player/adapter/mpg123.rb +143 -0
  34. data/lib/cultome_player/player/interactive.rb +56 -0
  35. data/lib/cultome_player/player/interface.rb +13 -0
  36. data/lib/cultome_player/player/interface/basic.rb +96 -0
  37. data/lib/cultome_player/player/interface/builtin_help.rb +368 -0
  38. data/lib/cultome_player/player/interface/extended.rb +199 -0
  39. data/lib/cultome_player/player/interface/helper.rb +300 -0
  40. data/lib/cultome_player/player/playlist.rb +280 -0
  41. data/lib/cultome_player/plugins.rb +23 -0
  42. data/lib/cultome_player/plugins/help.rb +58 -0
  43. data/lib/cultome_player/state_checker.rb +74 -0
  44. data/lib/cultome_player/utils.rb +95 -0
  45. data/lib/cultome_player/version.rb +3 -0
  46. data/spec/config.yml +0 -0
  47. data/spec/cultome_player/command/processor_spec.rb +168 -0
  48. data/spec/cultome_player/command/reader_spec.rb +45 -0
  49. data/spec/cultome_player/cultome_player_spec.rb +17 -0
  50. data/spec/cultome_player/environment_spec.rb +65 -0
  51. data/spec/cultome_player/events_spec.rb +22 -0
  52. data/spec/cultome_player/media_spec.rb +41 -0
  53. data/spec/cultome_player/player/adapter/mpg123_spec.rb +82 -0
  54. data/spec/cultome_player/player/interface/basic_spec.rb +168 -0
  55. data/spec/cultome_player/player/interface/extended/connect_spec.rb +117 -0
  56. data/spec/cultome_player/player/interface/extended/search_spec.rb +90 -0
  57. data/spec/cultome_player/player/interface/extended/show_spec.rb +36 -0
  58. data/spec/cultome_player/player/interface/extended/shuffle_spec.rb +26 -0
  59. data/spec/cultome_player/player/interface/extended_spec.rb +136 -0
  60. data/spec/cultome_player/player/interface/helper_spec.rb +63 -0
  61. data/spec/cultome_player/player/interface_spec.rb +17 -0
  62. data/spec/cultome_player/player/playlist_spec.rb +301 -0
  63. data/spec/cultome_player/plugins/help_spec.rb +21 -0
  64. data/spec/cultome_player/plugins_spec.rb +19 -0
  65. data/spec/cultome_player/utils_spec.rb +15 -0
  66. data/spec/spec_helper.rb +108 -0
  67. data/spec/test/uno/dos/dos.mp3 +0 -0
  68. data/spec/test/uno/dos/tres/tres.mp3 +0 -0
  69. data/spec/test/uno/uno.mp3 +0 -0
  70. data/tasks/console.rake +19 -0
  71. data/tasks/db.rake +19 -0
  72. data/tasks/run.rake +7 -0
  73. metadata +322 -0
@@ -0,0 +1,199 @@
1
+
2
+ module CultomePlayer::Player::Interface
3
+ module Extended
4
+
5
+ # For more information on this command refer to user manual or inline help in interactive mode.
6
+ def search(cmd)
7
+ songs = select_songs_with cmd
8
+
9
+ if songs.empty?
10
+ failure('It matches not even one')
11
+ else
12
+ playlists[:focus] <= songs
13
+ success(songs: songs, response_type: :songs)
14
+ end
15
+ end
16
+
17
+ # For more information on this command refer to user manual or inline help in interactive mode.
18
+ def show(cmd)
19
+ if cmd.params.empty?
20
+ if playing?
21
+ #mostramos la cancion actual
22
+ msg = get_progress_bar_with_labels(playback_position, playback_length, 20, format_secs(playback_position), format_secs(playback_length))
23
+ return success(message: "#{current_song.to_s}\n#{msg}", song: current_song)
24
+ else
25
+ return failure("Nothing to show yet. Try with 'play' first.")
26
+ end
27
+
28
+ # with parameters
29
+ else
30
+ list_to_show = cmd.params(:object).reduce([]) do |acc, p|
31
+ acc + case p.value
32
+ when :playlist then current_playlist.to_a
33
+ when :current then playlists[:current].to_a
34
+ when :history then playlists[:history].to_a
35
+ when :queue then playlists[:queue].to_a
36
+ when :focus then playlists[:focus].to_a
37
+ when :search then playlists[:search].to_a
38
+
39
+ when :song then return success(message: current_song.to_s, song: current_song)
40
+ when :artist then return success(message: current_artist.to_s, artist: current_song)
41
+ when :album then return success(message: current_album.to_s, album: current_song)
42
+
43
+ when :drives then Drive.all
44
+ when :artists then return Artist.all
45
+ when :albums then return Album.all
46
+ when :genres then return Genre.all
47
+
48
+ when :library then whole_library.to_a
49
+
50
+ when :recently_added then [] # TODO implement
51
+ when :recently_played then [] # TODO implement
52
+ when :more_played then [] # TODO implement
53
+ when :less_played then [] # TODO implement
54
+ when :populars then [] # TODO implement
55
+
56
+ else []
57
+ end
58
+ end
59
+
60
+ if list_to_show.empty?
61
+ return failure("I checked and there is nothing there.")
62
+ else
63
+ playlists[:focus] <= list_to_show
64
+ return success(list: list_to_show, response_type: :list)
65
+ end
66
+ end
67
+ end
68
+
69
+ # For more information on this command refer to user manual or inline help in interactive mode.
70
+ def enqueue(cmd)
71
+ songs = select_songs_with cmd
72
+ if songs.empty?
73
+ failure("No songs found with this criteria. Sorry, nothing was enqueued.")
74
+ else
75
+ playlists[:queue] << songs
76
+ msg = "These songs were enqueued:\n"
77
+ songs.each {|s,idx| msg << " #{s.to_s}\n"}
78
+
79
+ success(message: msg, enqueued: songs)
80
+ end
81
+ end
82
+
83
+ # For more information on this command refer to user manual or inline help in interactive mode.
84
+ def shuffle(cmd)
85
+ if cmd.params.empty?
86
+ if playlists[:current].shuffling?
87
+ return success(message: "Everyday I'm shuffling!", shuffling: true)
88
+ else
89
+ return success(message: "No shuffling", shuffling: false)
90
+ end
91
+ else
92
+ turn_on = cmd.params(:boolean).first.value
93
+ turn_on ? playlists[:current].shuffle : playlists[:current].order
94
+ return success(message: turn_on ? "Now we're shuffling!" : "Shuffle is now off")
95
+ end
96
+ end
97
+
98
+ # For more information on this command refer to user manual or inline help in interactive mode.
99
+ def connect(cmd)
100
+ path = cmd.params(:path).first
101
+ name = cmd.params(:literal).first
102
+
103
+ if path.nil?
104
+ # with only literal parameter
105
+ raise 'invalid parameter:missing parameters' if name.nil?
106
+
107
+ # es una reconexion...
108
+ drive = Drive.find_by(name: name.value)
109
+ raise 'invalid name:the named drive doesnt exists' if drive.nil?
110
+
111
+ if drive.connected
112
+ failure("What you mean? Drive 'name.value' is connected.")
113
+ else
114
+ if drive.update_attributes({connected: true})
115
+ success(message: "Drive '#{name.value}' was reconnected.")
116
+ else
117
+ failure("Something went wrong and I couldnt reconnect drive '#{name.value}'. Try again later please.")
118
+ end
119
+ end
120
+ else
121
+ # with path and literal parameter
122
+ raise 'invalid path:the directory is invalid' unless Dir.exist?(path.value)
123
+ raise 'invalid name:name required' if name.nil?
124
+
125
+ # es una creacion o actualizacion...
126
+ # checamos si la unidad existe
127
+ root_path = File.expand_path(path.value)
128
+ drive = Drive.find_by(path: root_path)
129
+ # la creamos si no existe...
130
+ is_update = !drive.nil?
131
+ drive = Drive.create!(name: name.value, path: root_path) unless is_update
132
+
133
+ track_info = get_files_in_tree(root_path, file_types).each_with_object([]) do |filepath, acc|
134
+ acc << extract_from_mp3(filepath, library_path: root_path)
135
+ end
136
+
137
+ # insertamos las nuevas y actualizamos las existentes
138
+ updated = update_song(track_info)
139
+ imported = insert_song(track_info)
140
+
141
+ success(message: connect_response_msg(imported, updated),
142
+ files_detected: track_info.size,
143
+ files_imported: imported,
144
+ files_updated: updated,
145
+ drive_updated: is_update)
146
+ end
147
+ end
148
+
149
+ # For more information on this command refer to user manual or inline help in interactive mode.
150
+ def disconnect(cmd)
151
+ name = cmd.params(:literal).first.value
152
+ drive = Drive.find_by(name: name)
153
+ raise "Drive '#{name}' dont exist." if drive.nil?
154
+
155
+ if drive.connected
156
+ if drive.update(connected: false)
157
+ success(message: "Drive '#{name}' is now disconnected.")
158
+ else
159
+ failure("I cant disconnect drive '#{name}', something weird happened. Maybe if you again later works.")
160
+ end
161
+ else
162
+ failure("The drive '#{name}' is already disconnected.")
163
+ end
164
+ end
165
+
166
+ # For more information on this command refer to user manual or inline help in interactive mode.
167
+ def ff(cmd)
168
+ ff_in_secs = 10
169
+
170
+ unless cmd.params(:number).empty?
171
+ ff_in_secs = cmd.params(:number).first.value
172
+ end
173
+
174
+ ff_in_player ff_in_secs
175
+
176
+ return success(message: "Fast Forwarded by #{ff_in_secs} secs")
177
+ end
178
+
179
+ # For more information on this command refer to user manual or inline help in interactive mode.
180
+ def fb(cmd)
181
+ fb_in_secs = 10
182
+
183
+ unless cmd.params(:number).empty?
184
+ fb_in_secs = cmd.params(:number).first.value
185
+ end
186
+
187
+ fb_in_player fb_in_secs
188
+
189
+ return success(message: "Fast Backwarded by #{fb_in_secs} secs")
190
+ end
191
+
192
+ # For more information on this command refer to user manual or inline help in interactive mode.
193
+ def repeat(cmd)
194
+ repeat_in_player
195
+ return success(message: "Repeating " + current_song.to_s)
196
+ end
197
+ end
198
+ end
199
+
@@ -0,0 +1,300 @@
1
+ module CultomePlayer::Player::Interface
2
+ module Helper
3
+ include CultomePlayer::Objects
4
+
5
+ VALID_SONG_ATTR = [:name, :year, :track, :duration, :relative_path, :artist_id, :album_id, :drive_id]
6
+
7
+ # Returns a string representation of a number of seconds in the format: mm:ss
8
+ #
9
+ # @param secs [Integer] Number of seconds to be represented.
10
+ # @return [String] The number of seconds formatted as mm:ss.
11
+ def format_secs(secs)
12
+ mins = secs.to_i / 60
13
+ secs_left = secs.to_i % 60
14
+ return "#{mins.to_s.rjust(2, "0")}:#{secs_left.to_s.rjust(2, "0")}"
15
+ end
16
+
17
+ # Returns a representation of a progress bar.
18
+ #
19
+ # @param current [Integer] The actual progress.
20
+ # @param total [Integer] The total progress to achive.
21
+ # @param total [Integer] Optional, the total progress to achive. Default 100.
22
+ # @param size [Integer] Optional, the width of the bar. Default 10.
23
+ # @param left [String] Optional, prefix to append. Default ''.
24
+ # @param right [String] Optional, postfix to append. Default ''.
25
+ # @return [String] The string representation of a progress bar.
26
+ def get_progress_bar_with_labels(current, total=100, size=10, left='', right='')
27
+ bar = get_progress_bar(current, total, size)
28
+ return "#{left} #{bar} #{right}".strip
29
+ end
30
+
31
+ # (see #get_progress_bar_with_labels)
32
+ def get_progress_bar(current, total=100, size=10)
33
+ factor = total > 0 ? current / total.to_f : 0
34
+ bars = ( factor * size ).floor
35
+ total = "_" * size
36
+ total[0,bars] = "#" * bars
37
+ return "|#{total}|"
38
+ end
39
+
40
+ # Generate a query and a array of values to replace into the query for a given set of parameters.
41
+ #
42
+ # @param params [List<Parameter>] The list of serach parameter to prepare.
43
+ # @return [(List<String>,List<String>)] The query and the values set to use.
44
+ def process_for_search(params)
45
+ return nil, [] if params.empty?
46
+
47
+ query, values = case params.first.type
48
+ when :object then process_object_for_search(params)
49
+ when :criteria then process_criteria_for_search(params)
50
+ when :literal then process_literal_for_search(params)
51
+ end
52
+
53
+ raise 'invalid command:invalid search criteria' if query =~ /^[\(\)]+$/
54
+ return query, values
55
+ end
56
+
57
+ # Return a list of absolute path of files in the path which has extension.
58
+ #
59
+ # @param path [String] The path to searlook for files.
60
+ # @param extension [List<String>] The list of extension to filter the files with.
61
+ # @return [List<String>] The absolute paths to the files found.
62
+ def get_files_in_tree(path, *extensions)
63
+ return extensions.each_with_object([]) do |ext, files|
64
+ files << Dir.glob("#{path}/**/*.#{ext}")
65
+ end.flatten
66
+ end
67
+
68
+ # Insert a new song into database. Except if its already present by path.
69
+ #
70
+ # @param new_info [List<Hash>] Has contains the keys :artist_id, :album_id, :drive_id, :relative_path, :library_path (optional).
71
+ # @return [Integer] The number of songs writed.
72
+ def insert_song(new_info)
73
+ existing_paths = get_unique_paths
74
+ to_be_processed = new_info.select{|s| !existing_paths.include?(s[:file_path]) }
75
+ return to_be_processed.count do |info|
76
+ write_song(info)
77
+ end
78
+ end
79
+
80
+ # Updates a song into database
81
+ #
82
+ # @param new_info [List<Hash>] Has contains the keys :artist_id, :album_id, :drive_id, :relative_path, :library_path (optional).
83
+ # @return [Integer] The number of songs updated.
84
+ def update_song(new_info)
85
+ existing_paths = get_unique_paths
86
+ to_be_processed = new_info.select{|s| existing_paths.include?(s[:file_path]) }
87
+ return to_be_processed.count do |info|
88
+ # extraemos la cancion almacenada.. si existe
89
+ song = Song.includes(:drive).where("drives.path||'/'||songs.relative_path = ?", info[:file_path]).references(:drives).first
90
+
91
+ song.nil? ? false : write_song(info, song)
92
+ end
93
+ end
94
+
95
+ # Extract the full list of songs connected.
96
+ #
97
+ # @return [List<Song>] The full list of songs connected in library.
98
+ def whole_library
99
+ Song.connected.to_a
100
+ end
101
+
102
+ # Play the next song in queue playlist.
103
+ #
104
+ # @return [Song] The song programed.
105
+ def play_queue
106
+ song = playlists[:queue].remove_next
107
+ play_in_player song
108
+ return song
109
+ end
110
+
111
+ # Select songs from the library and current and focus playlist.
112
+ #
113
+ # @param cmd [Command] The user command.
114
+ # @return [List<Song>] The list of songs picked.
115
+ def select_songs_with(cmd)
116
+ found_songs = search_songs_with(cmd)
117
+ from_focus = get_from_focus(cmd.params(:number))
118
+ from_playlists = get_from_playlists(cmd.params_values(:object))
119
+ results = found_songs + from_focus + from_playlists
120
+ return results
121
+ end
122
+
123
+ # Search in library for songs that fullfil the command parameters.
124
+ #
125
+ # @param cmd [Command] The user command.
126
+ # @return [List<Song>] The list of songs found.
127
+ def search_songs_with(cmd)
128
+ criteria_query, criteria_values = process_for_search(cmd.params(:criteria))
129
+ literal_query, literal_values = process_for_search(cmd.params(:literal))
130
+ object_query, object_values = process_for_search(cmd.params(:object))
131
+ # preparamos la query completa con sus parametros
132
+ search_query = [criteria_query, object_query, literal_query].compact.collect{|q| "(#{q})" }.join(" or ")
133
+ search_values = [criteria_values, object_values, literal_values].flatten.compact
134
+ # hacemos la query!
135
+ return search_query.empty? ? [] : Song.includes(:artist, :album).connected.where(search_query, *search_values).references(:artist, :album).to_a
136
+ end
137
+
138
+ # Get a list of songs from selected playlists, only if the playlist exist.
139
+ #
140
+ # @param lists [List<Symbol>] The names of the playlists to check.
141
+ # @return [List<Song>] The songs in the valid playlists.
142
+ def get_from_playlists(lists)
143
+ valid_lists = lists.select{|list_name| playlist?(list_name) }
144
+ return playlists[*valid_lists].songs
145
+ end
146
+
147
+ # Try to find the player object by name.
148
+ #
149
+ # @param name [Symbol] The name of the player object
150
+ # @return [Object] The player object found, if any.
151
+ def player_object(name)
152
+ case name
153
+ when :song then playlists[:current].current
154
+ else raise 'unknown player object:unknown player object'
155
+ end
156
+ end
157
+
158
+ # Check if a command has the format to be considered a ply inline (dont create a playlist).
159
+ #
160
+ # @param cmd [Command] The command to check.
161
+ # @return [Boolean] True if is considered to be played inline. False otherwise.
162
+ def play_inline?(cmd)
163
+ if cmd.action == "play"
164
+ return true if cmd.params.all?{|p| p.type == :number }
165
+ if cmd.params.size == 1
166
+ p = cmd.params.first
167
+ return p.type == :object && p.value == :song
168
+ end
169
+ end
170
+
171
+ return false
172
+ end
173
+
174
+ # Select the connect action message depending on the imported and updated parameters.
175
+ #
176
+ # @param imported [Integer] The number of imported songs.
177
+ # @param updated [Integer] The number of updated songs.
178
+ # @return [String] The appropiated message to show to user.
179
+ def connect_response_msg(imported, updated)
180
+ message = ""
181
+ if imported > 0
182
+ message += "Songs imported: #{imported}."
183
+ end
184
+
185
+ if updated > 0
186
+ message += "Songs updated: #{updated}."
187
+ end
188
+
189
+ return message
190
+ end
191
+
192
+ private
193
+
194
+ def get_from_focus(params)
195
+ params.map do |p|
196
+ playlists[:focus].at p.value - 1
197
+ end
198
+ end
199
+
200
+ def get_unique_paths
201
+ Song.all.collect{|m| m.path }.uniq
202
+ end
203
+
204
+ def artist_id(artist_name)
205
+ return 0 if artist_name.blank?
206
+ artist = Artist.where(name: artist_name).first_or_create
207
+ return artist.id
208
+ end
209
+
210
+ def album_id(album_name)
211
+ return 0 if album_name.blank?
212
+ album = Album.where(name: album_name).first_or_create
213
+ return album.id
214
+ end
215
+
216
+ def drive_id(library_path)
217
+ return 0 if library_path.blank?
218
+ drive = Drive.where(path: library_path).first_or_create
219
+ return drive.id
220
+ end
221
+
222
+ def add_genre_to(song, genre)
223
+ unless genre.blank?
224
+ song.genres << Genre.where(name: genre).first_or_create
225
+ end
226
+ end
227
+
228
+ def write_song(info, song=nil)
229
+ info[:artist_id] = artist_id(info[:artist])
230
+ info[:album_id] = album_id(info[:album])
231
+ info[:drive_id] = drive_id(info[:library_path])
232
+ info[:relative_path] = info[:file_path].gsub("#{info[:library_path]}/", '')
233
+
234
+ song_attr = info.select{|k,v| VALID_SONG_ATTR.include?(k) }
235
+
236
+ if song.nil?
237
+ song = Song.create!(song_attr)
238
+ else
239
+ song.update_attributes(song_attr)
240
+ end
241
+
242
+ add_genre_to(song, info[:genre])
243
+
244
+ return song.persisted?
245
+ end
246
+
247
+ def process_literal_for_search(params)
248
+ literals = params.collect do |p|
249
+ {query: 'artists.name like ? or albums.name like ? or songs.name like ?', value: ["%#{p.value}%", "%#{p.value}%", "%#{p.value}%"] }
250
+ end
251
+
252
+ query = literals.collect{|o| o[:query] }.join(" or ")
253
+ values = literals.collect{|o| o[:value]}
254
+
255
+ return query, values
256
+ end
257
+
258
+ def process_object_for_search(params)
259
+ objs = params.collect do |p|
260
+ case p.value
261
+ when :artist then {query: 'artists.name = ?', value: current_artist.name }
262
+ when :album then {query: 'albums.name = ?', value: current_album.name }
263
+ when :song then {query: 'songs.name = ?', value: current_song.name }
264
+ when :library then {query: 'songs.id > 0'}
265
+ else raise 'invalid search:unknown type'
266
+ end unless playlist?(p.value)
267
+ end.compact
268
+
269
+ return nil, [] if objs.empty?
270
+
271
+ query = objs.collect{|o| o[:query] }.join(" or ")
272
+ values = objs.collect{|o| o[:value]}.compact
273
+
274
+ return query, values
275
+ end
276
+
277
+ def process_criteria_for_search(params)
278
+ default = Hash.new{|h,k| h[k] = {count: 0, query: "", values: []} }
279
+ # analizamos los criterios
280
+ criterios = params.each_with_object(default) do |p, acc|
281
+ info = acc[p.criteria] # creamos nuevo mapa o sacamos el existente
282
+
283
+ info[:count] += 1
284
+ info[:query] << " or " if info[:count] > 1
285
+ case p.criteria
286
+ when :a then info[:query] << "artists.name like ?"
287
+ when :b then info[:query] << "albums.name like ?"
288
+ when :t then info[:query] << "songs.name like ?"
289
+ end
290
+ info[:values] << "%#{p.value}%"
291
+ end
292
+
293
+ query = criterios.values.collect{|c| "(#{c[:query]})" }.join(" and ")
294
+ values = criterios.values.collect{|c| c[:values] }.flatten
295
+ return query, values
296
+ end
297
+
298
+ end
299
+ end
300
+