sync_songs 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ # Ignore editor backups
2
+ *~
3
+ .bundle
4
+ *.gem
5
+ .bundle
6
+ test_lastfm_set.rb
7
+ test_grooveshark_set.rb
8
+ test_csv_set*.csv
data/CONTRIBUTING.org ADDED
@@ -0,0 +1,14 @@
1
+ # -*- mode:org; indent-tabs-mode:nil; tab-width:2 -*-
2
+
3
+ * Contributing
4
+
5
+ ** Issues
6
+
7
+ If you have a question, found a bug or want to make a suggestion please go ahead and [[https://github.com/Sleft/sync_songs/issues/new][post it as an issue]] but make sure it is not already reported. Please be as clear as possible and provide details. If the issue has to do with the program failing please include output from the program with the =--debug= and the =-v= options activated.
8
+
9
+ ** Development
10
+
11
+ If you want to contribute please:
12
+
13
+ - Follow the guidelines in development.org.
14
+ - Fork the project and commit changes to an aptly named topic branch which you then make a pull request for.
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ # -*- coding: utf-8; mode: ruby -*-
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ ruby '1.9.3'
data/Gemfile.lock ADDED
@@ -0,0 +1,51 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ sync_songs (0.0.0)
5
+ grooveshark (>= 0.2.7)
6
+ highline (>= 1.6.16)
7
+ lastfm (>= 1.17.0)
8
+ launchy (>= 2.2.0)
9
+ thor (>= 0.18.1)
10
+
11
+ GEM
12
+ remote: https://rubygems.org/
13
+ specs:
14
+ activesupport (3.2.13)
15
+ i18n (= 0.6.1)
16
+ multi_json (~> 1.0)
17
+ addressable (2.3.3)
18
+ grooveshark (0.2.7)
19
+ json (>= 1.4.6)
20
+ rest-client (>= 1.5.1)
21
+ uuid (~> 2.0)
22
+ highline (1.6.16)
23
+ httparty (0.10.2)
24
+ multi_json (~> 1.0)
25
+ multi_xml (>= 0.5.2)
26
+ i18n (0.6.1)
27
+ json (1.7.7)
28
+ lastfm (1.17.0)
29
+ activesupport (>= 3.0.3)
30
+ httparty
31
+ xml-simple
32
+ launchy (2.2.0)
33
+ addressable (~> 2.3)
34
+ macaddr (1.6.1)
35
+ systemu (~> 2.5.0)
36
+ mime-types (1.21)
37
+ multi_json (1.7.2)
38
+ multi_xml (0.5.3)
39
+ rest-client (1.6.7)
40
+ mime-types (>= 1.16)
41
+ systemu (2.5.2)
42
+ thor (0.18.1)
43
+ uuid (2.3.7)
44
+ macaddr (~> 1.0)
45
+ xml-simple (1.1.2)
46
+
47
+ PLATFORMS
48
+ ruby
49
+
50
+ DEPENDENCIES
51
+ sync_songs!
data/LICENSE.org ADDED
@@ -0,0 +1,2 @@
1
+ sync_songs is copyrighted free software and licensed under the same
2
+ terms as Ruby, see http://www.ruby-lang.org/en/about/license.txt
data/README.org ADDED
@@ -0,0 +1,117 @@
1
+ # -*- mode:org; indent-tabs-mode:nil; tab-width:2 -*-
2
+
3
+ * sync_songs
4
+
5
+ With sync_songs you can sync sets of songs between services. If you have one set of song at one service and another song set at another service you can use sync_songs to merge the song sets. sync_songs can also be used to backup song sets by the ability to spread them across several services. Additionaly sync_songs can be used to diff song sets.
6
+
7
+ Currently sync_songs supports the following services:
8
+ - csv (on the form =name, artist, album, duration, id= where only the first two fields are required)
9
+ - Grooveshark
10
+ - Last.fm
11
+
12
+ sync_songs can be used as standalone but also as a library.
13
+
14
+ ** Installation
15
+
16
+ To use sync_songs one has to have [[http://www.ruby-lang.org][Ruby]] installed. The easiest way to install Ruby is to use a package management system. If you are on a Debian-based distribution you can issue the following terminal command to install Ruby:
17
+ #+BEGIN_EXAMPLE
18
+ sudo apt-get install ruby1.9.1
19
+ #+END_EXAMPLE
20
+
21
+ The following describes three ways of obtaining and installing. The first way is recommended for users and the second way is recommended for developers.
22
+
23
+ *** Gem
24
+
25
+ This is the best method to install for most purposes. It requires RubyGems which on Debian-based distributions can be installed via the following command:
26
+ #+BEGIN_EXAMPLE
27
+ sudo apt-get install rubygems1.9.1
28
+ #+END_EXAMPLE
29
+
30
+ Then you can install sync_songs and its dependencies via the following command:
31
+ #+BEGIN_EXAMPLE
32
+ sudo gem install sync_songs
33
+ #+END_EXAMPLE
34
+
35
+ *** Git
36
+
37
+ This method is good if you want to help develop sync_songs. It requires Git which on Debian-based distributions can be installed via the following command:
38
+ #+BEGIN_EXAMPLE
39
+ sudo apt-get install git
40
+ #+END_EXAMPLE
41
+
42
+ To get the dependencies for sync_songs one can use bundler which can be installed via RubyGems (see above for installation instructions) in the following way:
43
+ #+BEGIN_EXAMPLE
44
+ sudo gem install bundler
45
+ #+END_EXAMPLE
46
+
47
+ To install sync_songs =cd= to an empty directory and do
48
+ #+BEGIN_EXAMPLE
49
+ git clone https://github.com/Sleft/sync_songs.git .
50
+ #+END_EXAMPLE
51
+ to clone the git repository into that directory. You can use the same command when you want to update it. To install the dependencies issue the following the same directory:
52
+ #+BEGIN_EXAMPLE
53
+ bundle
54
+ #+END_EXAMPLE
55
+
56
+ *** Archive
57
+
58
+ This method is not recommended but good if you for some reason cannot use RubyGems or Git. [[https://github.com/Sleft/sync_songs/archive/master.zip][Download]] an archive and extract to the directory you want to install in. Install the dependencies listed in the [[https://github.com/Sleft/sync_songs/blob/master/sync_songs.gemspec][gemspec]].
59
+
60
+ ** Usage
61
+
62
+ If you want to use sync_songs simply to sync songs between different services you probably want to use it as standalone.
63
+
64
+ *** Standalone
65
+
66
+ Issue the following command to learn about how to use sync_songs:
67
+ #+BEGIN_EXAMPLE
68
+ sync_songs help
69
+ #+END_EXAMPLE
70
+
71
+ The most common way of using sync_songs is probably to sync between two services by issuing a command of the following form:
72
+ #+BEGIN_EXAMPLE
73
+ sync_songs sync --color -vs user1:service1:favorites user2:service2:favorites
74
+ #+END_EXAMPLE
75
+ The =--color= option is recommended as it contributes to legibility. The =-v= option is recommended as it explains what is being done. Note that fetching song data from services may take some time due to limitations of bandwidth and due to limitations of particular services.
76
+
77
+ The above example does not work as it uses placeholder services. For a list of supported services one can issue
78
+ #+BEGIN_EXAMPLE
79
+ sync_songs supp
80
+ #+END_EXAMPLE
81
+ If one has a user named mary at Grooveshark and a user named smith at Last.fm one can use the following to sync between them:
82
+ #+BEGIN_EXAMPLE
83
+ sync_songs sync --color -vs mary:grooveshark:favorites smith:lastfm:loved
84
+ #+END_EXAMPLE
85
+
86
+ To sync between more than two services just add additional services as arguments to the =-s= option. For example, to also sync to a csv file one can add it as an argument:
87
+ #+BEGIN_EXAMPLE
88
+ sync_songs sync --color -vs user1:service1:favorites user2:service2:favorites file_path:csv:library
89
+ #+END_EXAMPLE
90
+ Note that syncing to a csv is a way of backing up songs from services.
91
+
92
+ To diff songs one can proceed as above but by replacing the =sync= command with =diff=, e.g.
93
+ #+BEGIN_EXAMPLE
94
+ sync_songs diff --color -vs user1:service1:favorites user2:service2:favorites
95
+ #+END_EXAMPLE
96
+
97
+ *** Library
98
+
99
+ If you want to integrate sync_songs in a project add the following line to the project's gemspec:
100
+ #+BEGIN_EXAMPLE
101
+ gem.add_runtime_dependency 'sync_songs'
102
+ #+END_EXAMPLE
103
+ Alternatively add the following line to your Gemfile:
104
+ #+BEGIN_EXAMPLE
105
+ gem 'sync_songs'
106
+ #+END_EXAMPLE
107
+ Now you should be able to =require sync_songs=.
108
+
109
+ Note that you can use bundler to get dependencies for sync_songs, see installation via Git above.
110
+
111
+ * License
112
+
113
+ See LICENSE.org.
114
+
115
+ * Contributing and development
116
+
117
+ See CONTRIBUTING.org.
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ # -*- coding: utf-8; mode: ruby -*-
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new do |t|
7
+ t.libs << 'test'
8
+ t.test_files = FileList['test/unit/test*.rb',
9
+ 'test/unit/services/test*.rb']
10
+ end
11
+
12
+ desc 'Run tests'
13
+ task default: :test
14
+
15
+ # task test: :rubocop
16
+
17
+ task :style do
18
+ sh 'rubocop'
19
+ end
data/bin/sync_songs ADDED
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- coding: utf-8; mode: ruby -*-
3
+
4
+ require 'thor'
5
+ require_relative '../lib/sync_songs.rb'
6
+
7
+ # Public: Classes for syncing sets of songs.
8
+ module SyncSongs
9
+ class Cli < Thor
10
+ class_option :verbose, type: :boolean, aliases: '-v',
11
+ desc: 'Explain what is being done'
12
+ class_option :debug, type: :boolean, desc: 'Debug mode'
13
+ class_option :color, type: :boolean, desc: 'Color mode'
14
+ services_option = [:services, {type: :array, required: true,
15
+ banner: Controller::INPUT_FORM,
16
+ desc: 'At least two users or file paths '\
17
+ 'each paired with a service and a type, '\
18
+ 'e.g. -s user1:grooveshark:favorites '\
19
+ 'songs.csv:csv:library',
20
+ aliases: '-s'}]
21
+
22
+ desc 'sync', 'Sync sets of songs'
23
+ long_desc 'Syncs sets of songs between the given services.'
24
+ method_option(*services_option)
25
+ def sync
26
+ setupController
27
+ @controller.sync
28
+ end
29
+
30
+ desc 'diff', 'Diff sets of songs'
31
+ long_desc 'Diffs sets of songs between the given services.'
32
+ method_option(*services_option)
33
+ def diff
34
+ setupController
35
+ @controller.diff
36
+ end
37
+
38
+ desc 'supp', 'List supported services'
39
+ long_desc <<-LONGDESC
40
+ Prints a list of the supported services.
41
+ LONGDESC
42
+ def supp
43
+ @controller = Controller.new(CLI.new(options[:verbose],
44
+ options[:debug],
45
+ options[:color]),
46
+ nil)
47
+ @controller.showSupportedServices
48
+ end
49
+
50
+ desc 'version', 'Shows the version number'
51
+ long_desc 'Shows the version number. Please include the version'\
52
+ 'in bug reports. If there is an error please produce it with the'\
53
+ 'debug option and include the output from it in the bug report.'
54
+ def version
55
+ puts "#{$PROGRAM_NAME} #{VERSION}"
56
+ end
57
+
58
+ private
59
+
60
+ def setupController
61
+ @controller = Controller.new(CLI.new(options[:verbose],
62
+ options[:debug],
63
+ options[:color]),
64
+ options[:services])
65
+ end
66
+ end
67
+
68
+ Cli.start(ARGV)
69
+ end
data/development.org ADDED
@@ -0,0 +1,53 @@
1
+ # -*- mode:org; indent-tabs-mode:nil; tab-width:2 -*-
2
+
3
+ * Development
4
+
5
+ sync_songs is designed to be used as standalone and also to provide a useful library for syncing sets of songs between services. One goal is for sync_songs to handle any number of services. Another goal is for it to be easy to add support for new services.
6
+
7
+ ** General guidelines
8
+
9
+ - Document code with [[http://tomdoc.org/][TomDoc]].
10
+ - If possible, include tests for every feature.
11
+ - Follow [[https://github.com/bbatsov/ruby-style-guide][The Ruby Style Guide]] as much as possible. This means that [[https://github.com/bbatsov/rubocop][rubocop]] can be used to check style (get it via =sudo gem install rubocop=).
12
+
13
+ ** Project structure
14
+
15
+ *** File structure
16
+
17
+ The directory structure is as follows:
18
+ - ./ :: Main project files, e.g. Rakefile, gem files and readme.
19
+ - bin :: Scripts meant to be executed by the user are placed here.
20
+ - lib :: Contains a file that loads the library.
21
+ - sync_songs :: The main code.
22
+ - services :: The code relating to particular services.
23
+ - test ::
24
+ - unit :: Tests of the =Test::Unit= framework. This directory should have a structure that corresponds to lib/sync_songs. Thus, this directory contains tests for the main code and also test suites.
25
+ - sample_data :: Sample data for tests of the parent directory.
26
+ - services :: Tests relating to particular services.
27
+ - sample_data :: Sample data for tests of the parent directory.
28
+
29
+ *** Code structure
30
+
31
+ The code is structured along MVC pattern. In lib/sync_songs there is a controller, controller.rb, which handles the main logic. The controller interacts with the user via a user interface, for example cli.rb -- a command line interface. Entity classes of the program are based on the classes =Song= and =SongSet=.
32
+
33
+ In .lib/sync_songs/services the classes relating to particular services are found. Each service has a controller which the main controller communicates with. If a particular service needs to interact with the user it should have its own user interface which is called by its controller. Each service has an entity class for getting and setting songs.
34
+
35
+ ** How to add a service
36
+
37
+ For a service to be meaningful it needs to be possible to get songs from it or set songs to it. Obviously it is best if both getting and setting is possible as it makes possible for sync in both directions. This mean that it needs to be possible to get and/or set songs to the service in question via Ruby.
38
+
39
+ As described under the heading "Code structure" above each service should have a controller, a user interface (if it needs to interact with the user) and an entity class. When adding a service it is probably best to start by designing the entity class.
40
+
41
+ The entity class for a particular service, which I will call a service set, should be a subclass of =SongSet= and named ServicenameSet. This provides it with the ability to store objects of the class =Song= via the methods =<<= and =add=. Every service set needs to have at least two methods. It needs to have a constructor that calls the constructor of its super class and does any setup that is necessary for the particular service. It also needs a method for getting or setting songs. The method for getting songs should be named after which kind of songs it gets, e.g. if it gets favorite songs it should be called =favorites= (if there is no given name for the type of song please call the method =library=), and it should get those songs from the service and add them to the =SongSet=. The method for setting songs should be named =addTo= followed by the type of songs it adds, e.g. =addToFavorites= if it adds favorites, and it should take a =SongSet= as a parameter and add those to the given type of songs of the service. When there is a method for setting songs there also needs to be a method for searching songs named =search= which takes a =SongSet= and an optional boolean value defaulting to true for whether to do strict search as arguments. The search method should search for the songs given as argument and return any matches. A service set is not responsible for handling exceptions but any exceptions thrown by any of its methods should be documented in TomDoc style. For an example of a simple service set see lib/sync_songs/services/csv_set.rb.
42
+
43
+ The controller for a particular service should be a subclass of =ServiceController= and it should be named ServicenameController. The controller is responsible for all communication with the main controller and it should setup the service set via its constructor. It should also have wrappers for the getters, setters and search methods of the service set and if any of them throws an exception it should be handled. For an example of a controller for a service see lib/sync_songs/services/csv_controller.rb.
44
+
45
+ A user interface for a particular service is only necessary if it needs to interact with the user. The service user interface should be named ServicenameInterfacetype and if possible it should use methods of the main user interface of the same type. Note that the service user interface should be called by the service controller only. For an example of a service user interface see lib/sync_songs/services/csv_cli.rb.
46
+
47
+ ** Adding a user interface
48
+
49
+ As mentioned sync_songs is designed to have a replaceable user interface. If one wants to make a new user interface one needs to construct a main user interface and a user interface for every service that needs one. If the main controller needs to be changed to support other user interface that is a flaw in the main controller and fixes for such flaws are encouraged.
50
+
51
+ ** Plan
52
+
53
+ The plan is for sync_songs to work as expected, have as few bugs as possible and support more services. Specific plans are documented in plan.org. Also see [[https://github.com/Sleft/sync_songs/issues][issues]].
@@ -0,0 +1,280 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'highline/import'
4
+
5
+ # Public: Classes for syncing sets of songs.
6
+ module SyncSongs
7
+ # Public: Command-line interface.
8
+ class CLI
9
+ # Public: A character for answering yes to a question.
10
+ YES_ANSWER = 'y'
11
+ # Public: A character that the user can input to quit what is
12
+ # currently happening. Sometimes it means to quit to program
13
+ # altogether, sometimes it means to merely to quit the current
14
+ # dialog.
15
+ QUIT_ANSWER = 'q'
16
+ # Public: Message asking for yes, no or quit.
17
+ YN_OPTIONS_MSG = 'Enter y for yes, n for no or q to quit'
18
+ # Public: Validator for yes, no or quit questions.
19
+ YN_VALIDATOR = /\A[yn#{QUIT_ANSWER}]/i
20
+
21
+ attr_reader :color
22
+
23
+ # Public: Creates a command-line interface.
24
+ #
25
+ # verbose - True if interface is verbose (default:
26
+ # nil).
27
+ # debug - True if interface is in debug mode
28
+ # (default: nil), this means e.g. that
29
+ # stack traces for exceptions are printed.
30
+ # color - True if color formatted output should be
31
+ # used (default: nil).
32
+ # possible_directions - A hash of possible sync directions
33
+ # between two given services mapped to
34
+ # descriptions of those directions (default:
35
+ # nil).
36
+ def initialize(verbose = nil, debug = nil, color = nil,
37
+ possible_directions = nil)
38
+ @verbose = verbose
39
+ @debug = debug
40
+ @possible_directions = possible_directions
41
+ HighLine::use_color = color
42
+
43
+ directionsMessage if @possible_directions
44
+
45
+ HighLine.color_scheme = HighLine::ColorScheme.new do |cs|
46
+ cs[:em] = [:bold]
47
+ cs[:verbose] = [:blue]
48
+ cs[:verbose_direction] = [:cyan, :bold]
49
+ cs[:error] = [:red, :bold]
50
+ cs[:even_row] = [:green]
51
+ cs[:odd_row] = [:magenta]
52
+ end
53
+
54
+ @row = true # To track even and odd rows for colorizing.
55
+ end
56
+
57
+ # Public: Asks for directions to write in and return them.
58
+ #
59
+ # directions - A two dimensional array where each element is of
60
+ # the form ['service1', '?', 'service2'].
61
+ #
62
+ # Returns an two dimensional array where each element is of the
63
+ # form ['service1', '</=/>', 'service2'].
64
+ def askDirections(directions)
65
+ directions.each do |d|
66
+ d[1] = askDirection("#{d.join(' ')} ")
67
+
68
+ exitOption(d[1])
69
+
70
+ if @verbose
71
+ say("<%= color(%q(#{d.first.join(' ')}), :verbose) %> "\
72
+ "<%= color(%q(#{d[1]}), :verbose_direction) %> "\
73
+ "<%= color(%q(#{d.last.join(' ')}), :verbose) %>")
74
+ end
75
+ end
76
+
77
+ directions
78
+ end
79
+
80
+ # Public: Asks if strict search should be used for the given
81
+ # service and returns the answer.
82
+ #
83
+ # s - A String describing a service
84
+ #
85
+ # Returns true if the user answer that strict search should be
86
+ # used for the given service.
87
+ def strict_search(s)
88
+ input = ask("<%= color(%q(Strict search), :em) %> for #{s}? ") do |q|
89
+ q.responses[:not_valid] = 'A strict search is recommended '\
90
+ 'as a wide search may generate too many hits. '\
91
+ "#{YN_OPTIONS_MSG}"
92
+ q.default = YES_ANSWER
93
+ q.validate = YN_VALIDATOR
94
+ end
95
+
96
+ exitOption(input)
97
+
98
+ input.casecmp(YES_ANSWER) == 0
99
+ end
100
+
101
+ # Public: Asks if interactive mode should be used for the given
102
+ # service and returns the answer.
103
+ #
104
+ # s - A String describing a service
105
+ #
106
+ # Returns true if the user answer that interactive mode should be
107
+ # used for the given service.
108
+ def interactive(s)
109
+ input = ask("<%= color(%q(Interactive mode), :em) %> for #{s}? ") do |q|
110
+ q.responses[:not_valid] = 'In interactive mode you will for '\
111
+ 'every found song be asked whether to add it. Interactive '\
112
+ 'mode is recommended for everything but services you have '\
113
+ "direct access to, such as text files. #{YN_OPTIONS_MSG}"
114
+ q.default = YES_ANSWER
115
+ q.validate = YN_VALIDATOR
116
+ end
117
+
118
+ exitOption(input)
119
+
120
+ input.casecmp(YES_ANSWER) == 0
121
+ end
122
+
123
+ # Public: For every of the given songs, ask whether to add it and
124
+ # return an array of songs to add.
125
+ #
126
+ # service - A String naming a service.
127
+ # songs - An Array of songs to ask about.
128
+ #
129
+ # Return an Array of songs to add.
130
+ def askAddSongs(service, songs)
131
+ songs_to_add = []
132
+
133
+ songs.each do |s|
134
+ add = askAddSong(s, service)
135
+
136
+ # Stop asking if the user press quit
137
+ break if add.casecmp(QUIT_ANSWER) == 0
138
+
139
+ songs_to_add << s if add.casecmp(YES_ANSWER) == 0
140
+ end
141
+
142
+ songs_to_add
143
+ end
144
+
145
+ # Public: Shows the given message and exits with the given exit
146
+ # code.
147
+ #
148
+ # msg - A String naming a failure message.
149
+ # exit_code - Exit code to use, see
150
+ # http://tldp.org/LDP/abs/html/exitcodes.html for
151
+ # details (default: 1).
152
+ # exception - The Exception causing the failure (default: nil).
153
+ def fail(msg, exit_code = 1, exception = nil)
154
+ failMessage(msg)
155
+
156
+ if @debug && exception
157
+ p exception
158
+ puts exception.backtrace
159
+ end
160
+
161
+ exit(exit_code)
162
+ end
163
+
164
+ # Public: Shows the given message.
165
+ #
166
+ # msg - A String or an Enumerable naming a message.
167
+ def message(msg)
168
+ puts msg
169
+ end
170
+
171
+ # Public: Shows the given message.
172
+ #
173
+ # msg - A String or an Enumerable naming a message.
174
+ def emMessage(msg)
175
+ styleMessage(msg, :em)
176
+ end
177
+
178
+ # Public: Prints the given message if in verbose mode.
179
+ #
180
+ # msg - A String or an an Enumerable of Strings naming a verbose
181
+ # message.
182
+ def verboseMessage(msg)
183
+ styleMessage(msg, :verbose) if @verbose
184
+ end
185
+
186
+ # Public: Shows the given fail message.
187
+ #
188
+ # msg - A String or an Enumerable naming a message.
189
+ def failMessage(msg)
190
+ styleMessage(msg, :error)
191
+ end
192
+
193
+ # Public: Shows the supported services.
194
+ def supportedServices
195
+ msg = []
196
+
197
+ Controller.supportedServices.each do |service, type_action|
198
+ type_msg = []
199
+ type_action.each do |type, action|
200
+ type_msg << "#{type} <%= color(%q(#{action}), :even_row) %>"
201
+ end
202
+ msg << "<%= color(%q(#{service}), :em) %>: #{type_msg.join(', ')}"
203
+ end
204
+
205
+ say(msg.join("\n"))
206
+ end
207
+
208
+ # Public: Sets the possible directions.
209
+ #
210
+ # val - A hash of possible sync directions between two given
211
+ # services mapped to descriptions of those directions.
212
+ def possible_directions=(val)
213
+ @possible_directions = val
214
+ directionsMessage
215
+ end
216
+
217
+ private
218
+
219
+ # Internal: Prints the given message with the given style.
220
+ #
221
+ # msg - A String or an an Enumerable of Strings naming a
222
+ # message.
223
+ # color - A Symbol representing an ERB style.
224
+ def styleMessage(msg, style)
225
+ if @verbose
226
+ if msg.respond_to? :each
227
+ msg.each { |m| styleMessage(m, style) }
228
+ else
229
+ say("<%= color(%q(#{msg}), '#{style}') %>")
230
+ end
231
+ end
232
+ end
233
+
234
+ # Internal: Asks whether to add the given song to the given
235
+ # service.
236
+ #
237
+ # song - A String naming a song.
238
+ # service - A String naming a service.
239
+ def askAddSong(song, service)
240
+ question = "Add <%= color(%q(#{song} to #{service}), "
241
+ question << (@row ? ':even_row' : ':odd_row')
242
+ @row = !@row
243
+
244
+ ask("#{question}) %>? ") do |q|
245
+ q.responses[:not_valid] = YN_OPTIONS_MSG
246
+ q.default = YES_ANSWER
247
+ q.validate = YN_VALIDATOR
248
+ end
249
+ end
250
+
251
+ # Internal: Ask which direction to sync for the given services.
252
+ #
253
+ # question - A String naming a question asking for which direction
254
+ # to sync in between to services.
255
+ #
256
+ # Returns a String naming the direction to sync in.
257
+ def askDirection(question)
258
+ ask(question) do |q|
259
+ q.responses[:not_valid] = @DIRECTIONS_MSG
260
+ q.default = '='
261
+ q.validate = lambda { |a| @possible_directions.key?(a.to_sym) || a == QUIT_ANSWER }
262
+ end
263
+ end
264
+
265
+ # Internal: Exits if input is a character for quitting.
266
+ #
267
+ # input - Input String from user.
268
+ def exitOption(input)
269
+ exit if input.casecmp(QUIT_ANSWER) == 0
270
+ end
271
+
272
+ # Internal: Create a message describing what sync directions the
273
+ # user can choose for any two services.
274
+ def directionsMessage
275
+ @DIRECTIONS_MSG = @possible_directions.map { |k, v| "<%= color(%q(#{k}), :verbose_direction) %> for #{v}" }
276
+ @DIRECTIONS_MSG = "Enter #{@DIRECTIONS_MSG.join(", ")}"
277
+ @DIRECTIONS_MSG << " or <%= color(%q(#{QUIT_ANSWER}), :verbose_direction) %> to quit"
278
+ end
279
+ end
280
+ end