cultome_player 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +24 -0
- data/.rspec +2 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +325 -0
- data/Rakefile +8 -0
- data/bin/cultome_player +39 -0
- data/config/environment.yml +28 -0
- data/cultome_player.gemspec +35 -0
- data/db/001_create_schema.rb +58 -0
- data/lib/cultome_player.rb +107 -0
- data/lib/cultome_player/command.rb +11 -0
- data/lib/cultome_player/command/language.rb +61 -0
- data/lib/cultome_player/command/processor.rb +165 -0
- data/lib/cultome_player/command/reader.rb +86 -0
- data/lib/cultome_player/environment.rb +130 -0
- data/lib/cultome_player/events.rb +29 -0
- data/lib/cultome_player/media.rb +47 -0
- data/lib/cultome_player/objects.rb +15 -0
- data/lib/cultome_player/objects/album.rb +21 -0
- data/lib/cultome_player/objects/artist.rb +18 -0
- data/lib/cultome_player/objects/command.rb +37 -0
- data/lib/cultome_player/objects/drive.rb +26 -0
- data/lib/cultome_player/objects/genre.rb +16 -0
- data/lib/cultome_player/objects/parameter.rb +37 -0
- data/lib/cultome_player/objects/response.rb +42 -0
- data/lib/cultome_player/objects/song.rb +38 -0
- data/lib/cultome_player/player.rb +13 -0
- data/lib/cultome_player/player/adapter.rb +14 -0
- data/lib/cultome_player/player/adapter/mpg123.rb +143 -0
- data/lib/cultome_player/player/interactive.rb +56 -0
- data/lib/cultome_player/player/interface.rb +13 -0
- data/lib/cultome_player/player/interface/basic.rb +96 -0
- data/lib/cultome_player/player/interface/builtin_help.rb +368 -0
- data/lib/cultome_player/player/interface/extended.rb +199 -0
- data/lib/cultome_player/player/interface/helper.rb +300 -0
- data/lib/cultome_player/player/playlist.rb +280 -0
- data/lib/cultome_player/plugins.rb +23 -0
- data/lib/cultome_player/plugins/help.rb +58 -0
- data/lib/cultome_player/state_checker.rb +74 -0
- data/lib/cultome_player/utils.rb +95 -0
- data/lib/cultome_player/version.rb +3 -0
- data/spec/config.yml +0 -0
- data/spec/cultome_player/command/processor_spec.rb +168 -0
- data/spec/cultome_player/command/reader_spec.rb +45 -0
- data/spec/cultome_player/cultome_player_spec.rb +17 -0
- data/spec/cultome_player/environment_spec.rb +65 -0
- data/spec/cultome_player/events_spec.rb +22 -0
- data/spec/cultome_player/media_spec.rb +41 -0
- data/spec/cultome_player/player/adapter/mpg123_spec.rb +82 -0
- data/spec/cultome_player/player/interface/basic_spec.rb +168 -0
- data/spec/cultome_player/player/interface/extended/connect_spec.rb +117 -0
- data/spec/cultome_player/player/interface/extended/search_spec.rb +90 -0
- data/spec/cultome_player/player/interface/extended/show_spec.rb +36 -0
- data/spec/cultome_player/player/interface/extended/shuffle_spec.rb +26 -0
- data/spec/cultome_player/player/interface/extended_spec.rb +136 -0
- data/spec/cultome_player/player/interface/helper_spec.rb +63 -0
- data/spec/cultome_player/player/interface_spec.rb +17 -0
- data/spec/cultome_player/player/playlist_spec.rb +301 -0
- data/spec/cultome_player/plugins/help_spec.rb +21 -0
- data/spec/cultome_player/plugins_spec.rb +19 -0
- data/spec/cultome_player/utils_spec.rb +15 -0
- data/spec/spec_helper.rb +108 -0
- data/spec/test/uno/dos/dos.mp3 +0 -0
- data/spec/test/uno/dos/tres/tres.mp3 +0 -0
- data/spec/test/uno/uno.mp3 +0 -0
- data/tasks/console.rake +19 -0
- data/tasks/db.rake +19 -0
- data/tasks/run.rake +7 -0
- metadata +322 -0
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'readline'
|
2
|
+
|
3
|
+
module CultomePlayer::Command
|
4
|
+
module Reader
|
5
|
+
|
6
|
+
# Display a prompt and read user input.
|
7
|
+
#
|
8
|
+
# @param prompt [String] The message to display to user for arcking for input.
|
9
|
+
# @return [String] The user input readed.
|
10
|
+
def read_command(prompt)
|
11
|
+
command_reader.readline(prompt, true)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Lazy getter for readline object.
|
15
|
+
#
|
16
|
+
# @return [Readline] The readline object
|
17
|
+
def command_reader
|
18
|
+
return Readline if @command_reader_initialized
|
19
|
+
|
20
|
+
Readline.completion_append_character = ""
|
21
|
+
Readline.basic_word_break_characters = Readline.basic_word_break_characters.delete("@")
|
22
|
+
Readline.completion_proc = completion_proc
|
23
|
+
@command_reader_initialized = true
|
24
|
+
return Readline
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def completion_proc
|
30
|
+
proc do |word|
|
31
|
+
if Readline.line_buffer.empty?
|
32
|
+
# linea en blanco y no sabe los comandos
|
33
|
+
options = semantics.keys #return
|
34
|
+
else
|
35
|
+
tks = Readline.line_buffer.split
|
36
|
+
if tks.length == 1
|
37
|
+
options = complete_action(tks[0], word)
|
38
|
+
elsif tks.length > 1
|
39
|
+
options = complete_parameter(tks[0], word)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
options = [] if options.nil?
|
44
|
+
options << word if options.empty?
|
45
|
+
options << " " if options.all?{|o| o.start_with?("<")}
|
46
|
+
options # final return
|
47
|
+
end # proc
|
48
|
+
end
|
49
|
+
|
50
|
+
def get_command_param_options(cmd, word)
|
51
|
+
if word.empty?
|
52
|
+
# completa! mostramos los parametros disponibles para el comando
|
53
|
+
if semantics.keys.include?(cmd)
|
54
|
+
# mostramos las opciones de parametros IFF acepta parametros
|
55
|
+
params = semantics[cmd].source.gsub(/^\^literal/, '').gsub(/\[\\s\][+*]/, "").gsub(/[()*$]/, '')
|
56
|
+
|
57
|
+
params.split(/[| ]/).collect{|p| "<#{p}>"} unless params.empty?
|
58
|
+
end
|
59
|
+
else
|
60
|
+
yield if block_given?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def complete_parameter(cmd, word)
|
65
|
+
options = get_command_param_options(cmd, word) do
|
66
|
+
# esta acompletando un parametro
|
67
|
+
if word.start_with?("/") || word.start_with?("~/")
|
68
|
+
expanded_path = File.expand_path(word)
|
69
|
+
expanded_path += "/" if File.directory?(expanded_path)
|
70
|
+
Dir[expanded_path + "*"].grep(/^#{Regexp.escape(expanded_path)}/).collect{|d| "#{d}/"}
|
71
|
+
elsif word.start_with?("@")
|
72
|
+
%w{@library @search @playlist @history @queue @song @artist @album @drives}.grep(/^#{word}/)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def complete_action(cmd, word)
|
78
|
+
# escribio una parabra..
|
79
|
+
get_command_param_options(cmd, word) do
|
80
|
+
# incompleta! require acompletar el action actual
|
81
|
+
semantics.keys.grep(/^#{Regexp.escape(Readline.line_buffer)}/).collect{|s| "#{s} "}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
module CultomePlayer
|
5
|
+
module Environment
|
6
|
+
|
7
|
+
# Get the db_adapter environment configuration value.
|
8
|
+
#
|
9
|
+
# @return [String] The db_adapter value for teh selected environment.
|
10
|
+
def db_adapter
|
11
|
+
env_config['db_adapter'] || raise('environment problem:environment information not loaded')
|
12
|
+
end
|
13
|
+
|
14
|
+
# Get the db_file environment configuration value.
|
15
|
+
#
|
16
|
+
# @return [String] The db_file value for teh selected environment.
|
17
|
+
def db_file
|
18
|
+
env_config['db_file'] || raise('environment problem:environment information not loaded')
|
19
|
+
end
|
20
|
+
|
21
|
+
# Get the db_log_file environment configuration value.
|
22
|
+
#
|
23
|
+
# @return [String] The db_log_file value for teh selected environment.
|
24
|
+
def db_log_file
|
25
|
+
env_config['db_log_file'] || raise('environment problem:environment information not loaded')
|
26
|
+
end
|
27
|
+
|
28
|
+
# Get the file_types environment configuration value.
|
29
|
+
#
|
30
|
+
# @return [String] The file_types value for teh selected environment.
|
31
|
+
def file_types
|
32
|
+
env_config['file_types'] || raise('environment problem:environment information not loaded')
|
33
|
+
end
|
34
|
+
|
35
|
+
# Get the config_file environment configuration value.
|
36
|
+
#
|
37
|
+
# @return [String] The config_file value for teh selected environment.
|
38
|
+
def config_file
|
39
|
+
env_config['config_file'] || raise('environment problem:environment information not loaded')
|
40
|
+
end
|
41
|
+
|
42
|
+
# Get the mplayer_pipe environment configuration value.
|
43
|
+
#
|
44
|
+
# @return [String] The mplayer_pipe value for teh selected environment.
|
45
|
+
def mplayer_pipe
|
46
|
+
env_config['mplayer_pipe'] || raise('environment problem:environment information not loaded')
|
47
|
+
end
|
48
|
+
|
49
|
+
# Get the stdout (not STDOUT) for the player.
|
50
|
+
#
|
51
|
+
# @return [IO] The stdout for the player.
|
52
|
+
def stdout
|
53
|
+
STDOUT
|
54
|
+
end
|
55
|
+
|
56
|
+
# Gets the player configurations.
|
57
|
+
#
|
58
|
+
# @return [Hash] Player configuration.
|
59
|
+
def player_config
|
60
|
+
@player_config ||= {}
|
61
|
+
end
|
62
|
+
|
63
|
+
# Gets the environment configurations.
|
64
|
+
#
|
65
|
+
# @return [Hash] Environment configuration.
|
66
|
+
def env_config
|
67
|
+
@env_config ||= {}
|
68
|
+
end
|
69
|
+
|
70
|
+
# Get the current environment name.
|
71
|
+
#
|
72
|
+
# @return [Symbol] The current environment name.
|
73
|
+
def current_env
|
74
|
+
@current_env
|
75
|
+
end
|
76
|
+
|
77
|
+
# Extract the configuration for the environment and setup valriables.
|
78
|
+
#
|
79
|
+
# @param env [Symbol] The name of the environment to load.
|
80
|
+
# @param check_db [Boolean] Flag to decide if the database schema should be checked.
|
81
|
+
def prepare_environment(env, check_db=true)
|
82
|
+
env_config = YAML.load_file File.expand_path('config/environment.yml')
|
83
|
+
@env_config = env_config[env.to_s]
|
84
|
+
@current_env = env.to_sym
|
85
|
+
raise 'environment problem:environment not found' if @env_config.nil?
|
86
|
+
expand_paths @env_config
|
87
|
+
create_required_files @env_config
|
88
|
+
load_master_config @env_config['config_file']
|
89
|
+
check_db_schema if check_db
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def check_db_schema
|
95
|
+
Rake.load_rakefile 'Rakefile'
|
96
|
+
Rake.application.load_imports
|
97
|
+
swallow_stdout{ Rake.application.invoke_task("db:create[#{current_env}]") }
|
98
|
+
end
|
99
|
+
|
100
|
+
def load_master_config(config_file)
|
101
|
+
@player_config = YAML.load_file(config_file) || {}
|
102
|
+
@player_config['main'] ||= {}
|
103
|
+
end
|
104
|
+
|
105
|
+
def create_required_files(env_config)
|
106
|
+
env_config.each do |k,v|
|
107
|
+
if k.end_with?('_file')
|
108
|
+
unless File.exist?(v)
|
109
|
+
%x[mkdir -p '#{File.dirname(v)}' && touch '#{v}']
|
110
|
+
raise 'environment problem:cannot create required files' unless $?.success?
|
111
|
+
end
|
112
|
+
elsif k.end_with?('_pipe')
|
113
|
+
unless File.exist?(v)
|
114
|
+
%x[mkfifo '#{v}']
|
115
|
+
raise 'environment problem:cannot create required pipe' unless $?.success?
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def expand_paths(env_config)
|
122
|
+
env_config.each do |k,v|
|
123
|
+
if k.end_with?('_file') || k.end_with?('_pipe')
|
124
|
+
env_config[k] = File.expand_path(v)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module CultomePlayer
|
2
|
+
module Events
|
3
|
+
|
4
|
+
# Lazy getter of registered event listeners.
|
5
|
+
#
|
6
|
+
# @return [Hash] With event names as the keys and values are the listeners registered to that event.
|
7
|
+
def listeners
|
8
|
+
@listeners ||= Hash.new{|h,k| h[k] = [] }
|
9
|
+
end
|
10
|
+
|
11
|
+
# Register a listener to an event.
|
12
|
+
#
|
13
|
+
# @param event [Symbol] The event name.
|
14
|
+
# @param listener [Object] Implements a callback with the name on_<event name>.
|
15
|
+
# @return [Object] The registered listener.
|
16
|
+
def register_listener(event, listener)
|
17
|
+
listeners[event] << listener
|
18
|
+
return listener
|
19
|
+
end
|
20
|
+
|
21
|
+
# Broadcast an event to all the registered listeners.
|
22
|
+
#
|
23
|
+
# @param event [Symbol] The event name.
|
24
|
+
# @param data [Array] The information sended to the listeners.
|
25
|
+
def emit_event(event, *data)
|
26
|
+
listeners[event].collect{|l| l.send("on_#{event}".to_sym, *data) }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'taglib'
|
2
|
+
|
3
|
+
module CultomePlayer
|
4
|
+
module Media
|
5
|
+
|
6
|
+
# Get information from ID3 tags in a mp3.
|
7
|
+
#
|
8
|
+
# @param filepath [String] The absolute path to the mp3 file.
|
9
|
+
# @param opc [Hash] Additional parameters. Actually only :library_path is supported.
|
10
|
+
# @return [Hash] With information extracted from ID3 tags.
|
11
|
+
def extract_from_mp3(filepath, opc={})
|
12
|
+
info = nil
|
13
|
+
TagLib::FileRef.open(filepath) do |mp3|
|
14
|
+
unless mp3.nil?
|
15
|
+
info = {
|
16
|
+
# file information
|
17
|
+
file_path: filepath,
|
18
|
+
library_path: opc[:library_path],
|
19
|
+
# song information
|
20
|
+
album: mp3.tag.album,
|
21
|
+
artist: mp3.tag.artist,
|
22
|
+
genre: mp3.tag.genre,
|
23
|
+
name: mp3.tag.title,
|
24
|
+
track: mp3.tag.track,
|
25
|
+
year: mp3.tag.year,
|
26
|
+
duration: mp3.audio_properties.length,
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
# si no se encontro nombre de la cancion en las etiquestas, usamos el nombre del archivo
|
31
|
+
info[:name] = filepath.split('/').last if info[:name].nil?
|
32
|
+
# limpiamos la informacion un poco
|
33
|
+
return polish_mp3_info(info)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def polish_mp3_info(info)
|
39
|
+
[:genre, :name, :artist, :album].each{|k| info[k] = info[k].downcase.strip.titleize unless info[k].nil? }
|
40
|
+
[:track, :year].each{|k| info[k] = info[k].to_i if info[k] =~ /\A[\d]+\Z/ }
|
41
|
+
info[:duration] = info[:duration].to_i
|
42
|
+
|
43
|
+
return info
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'cultome_player/utils'
|
2
|
+
|
3
|
+
require 'cultome_player/objects/command'
|
4
|
+
require 'cultome_player/objects/parameter'
|
5
|
+
require 'cultome_player/objects/response'
|
6
|
+
require 'cultome_player/objects/song'
|
7
|
+
require 'cultome_player/objects/artist'
|
8
|
+
require 'cultome_player/objects/album'
|
9
|
+
require 'cultome_player/objects/genre'
|
10
|
+
require 'cultome_player/objects/drive'
|
11
|
+
|
12
|
+
module CultomePlayer
|
13
|
+
module Objects
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module CultomePlayer
|
4
|
+
module Objects
|
5
|
+
# The ActiveRecord model for Album objects.
|
6
|
+
class Album < ActiveRecord::Base
|
7
|
+
has_many :songs
|
8
|
+
has_many :artists, through: :songs
|
9
|
+
|
10
|
+
def to_s
|
11
|
+
str = c4(":::: Album: ")
|
12
|
+
str += c13(self.name)
|
13
|
+
str += c4(" \\ Artist: ")
|
14
|
+
unless self.artists.nil? || self.artists.empty?
|
15
|
+
str += c11(self.artists.uniq.collect{|a| a.name}.join(', '))
|
16
|
+
end
|
17
|
+
str += c4(" ::::")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module CultomePlayer
|
4
|
+
module Objects
|
5
|
+
# The ActiveRecord model for Artist objects.
|
6
|
+
class Artist < ActiveRecord::Base
|
7
|
+
has_many :songs
|
8
|
+
has_many :albums, through: :songs
|
9
|
+
has_many :similars, as: :similar
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
str = c4(":::: Artist: ")
|
13
|
+
str += c11(self.name)
|
14
|
+
str += c4(" ::::")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module CultomePlayer
|
2
|
+
module Objects
|
3
|
+
class Command
|
4
|
+
attr_reader :action
|
5
|
+
attr_reader :parameters
|
6
|
+
|
7
|
+
def initialize(action, parameters)
|
8
|
+
@action = action[:value]
|
9
|
+
@parameters = parameters.collect{|p| Parameter.new(p) }
|
10
|
+
end
|
11
|
+
|
12
|
+
# Returns the parameters, optionally filtered by type
|
13
|
+
#
|
14
|
+
# @param type [Symbol] Parameter type to filter the results
|
15
|
+
# @return [List<Parameter>] The parameters associated with the command, optionally filtered.
|
16
|
+
def params(type=nil)
|
17
|
+
return @parameters if type.nil?
|
18
|
+
@parameters.select{|p| p.type == type}
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns a map that contains parameter type as key and a list of the parameters of that type as value.
|
22
|
+
#
|
23
|
+
# @return [Hash<Symbol, List<Parameter>>] Parameters grouped by type.
|
24
|
+
def params_groups
|
25
|
+
@parameters.collect{|p| p.type }.each_with_object({}){|type,acc| acc[type] = params(type) }
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns a list with only the parameters values of certain type.
|
29
|
+
#
|
30
|
+
# @param type [Symbol] The type of parameters.
|
31
|
+
# @return [List<Object>] The values of the parameters.
|
32
|
+
def params_values(type)
|
33
|
+
params(type).map{|p| p.value }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module CultomePlayer
|
4
|
+
module Objects
|
5
|
+
# The ActiveRecord model for Drive objects.
|
6
|
+
class Drive < ActiveRecord::Base
|
7
|
+
has_many :songs
|
8
|
+
|
9
|
+
def connected?
|
10
|
+
connected
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
str = c4(":::: Drive: ")
|
15
|
+
str += c14(self.name)
|
16
|
+
str += c4(" => ")
|
17
|
+
str += c14(self.songs.size.to_s)
|
18
|
+
str += c4(" songs => ")
|
19
|
+
str += c14(self.path)
|
20
|
+
str += c4(" => ")
|
21
|
+
str += connected ? c3("Online") : c2("Offline")
|
22
|
+
str += c4(" ::::")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module CultomePlayer
|
4
|
+
module Objects
|
5
|
+
# The ActiveRecord model for Genre objects.
|
6
|
+
class Genre < ActiveRecord::Base
|
7
|
+
has_and_belongs_to_many :songs
|
8
|
+
|
9
|
+
def to_s
|
10
|
+
str = c4(":::: Genre: ")
|
11
|
+
str += c15(self.name)
|
12
|
+
str += c4(" ::::")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module CultomePlayer
|
2
|
+
module Objects
|
3
|
+
class Parameter
|
4
|
+
include CultomePlayer::Utils
|
5
|
+
|
6
|
+
# Initialize a parameter with the data provided.
|
7
|
+
#
|
8
|
+
# @param data [Hash] Contains the keys :criteria, :value, :type
|
9
|
+
def initialize(data)
|
10
|
+
@data = data
|
11
|
+
end
|
12
|
+
|
13
|
+
# Get the criteria asocciated with the parameter, if any.
|
14
|
+
def criteria
|
15
|
+
return nil if @data[:criteria].nil?
|
16
|
+
@data[:criteria].to_sym
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns the value associated with the parameter in its appropiated type.
|
20
|
+
#
|
21
|
+
# @return [Object] The value of the parameter.
|
22
|
+
def value
|
23
|
+
return is_true_value?(@data[:value]) if @data[:type] == :boolean
|
24
|
+
return @data[:value].to_i if @data[:type] == :number
|
25
|
+
return @data[:value].to_sym if @data[:type] == :object
|
26
|
+
@data[:value]
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns the type associated with the parameter.
|
30
|
+
#
|
31
|
+
# @return [Symbol] The type of the parameter.
|
32
|
+
def type
|
33
|
+
@data[:type]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|