sync_songs 0.0.1
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.
- data/.gitignore +8 -0
- data/CONTRIBUTING.org +14 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +51 -0
- data/LICENSE.org +2 -0
- data/README.org +117 -0
- data/Rakefile +19 -0
- data/bin/sync_songs +69 -0
- data/development.org +53 -0
- data/lib/sync_songs/cli.rb +280 -0
- data/lib/sync_songs/controller.rb +435 -0
- data/lib/sync_songs/services/csv_cli.rb +26 -0
- data/lib/sync_songs/services/csv_controller.rb +64 -0
- data/lib/sync_songs/services/csv_set.rb +65 -0
- data/lib/sync_songs/services/grooveshark_cli.rb +26 -0
- data/lib/sync_songs/services/grooveshark_controller.rb +77 -0
- data/lib/sync_songs/services/grooveshark_set.rb +107 -0
- data/lib/sync_songs/services/lastfm_cli.rb +46 -0
- data/lib/sync_songs/services/lastfm_controller.rb +78 -0
- data/lib/sync_songs/services/lastfm_set.rb +177 -0
- data/lib/sync_songs/services/service_controller.rb +55 -0
- data/lib/sync_songs/song.rb +99 -0
- data/lib/sync_songs/song_set.rb +30 -0
- data/lib/sync_songs/version.rb +6 -0
- data/lib/sync_songs.rb +27 -0
- data/plan.org +17 -0
- data/sync_songs.gemspec +25 -0
- data/test/unit/sample_data/sample_data.rb +23 -0
- data/test/unit/services/test_csv_set.rb +54 -0
- data/test/unit/services/test_service_controller.rb +30 -0
- data/test/unit/suite_data.rb +12 -0
- data/test/unit/test_song.rb +129 -0
- data/test/unit/test_song_set.rb +134 -0
- metadata +165 -0
@@ -0,0 +1,107 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'grooveshark'
|
4
|
+
require_relative '../song_set'
|
5
|
+
|
6
|
+
# Public: Classes for syncing sets of songs.
|
7
|
+
module SyncSongs
|
8
|
+
# Public: A set of Grooveshark songs.
|
9
|
+
class GroovesharkSet < SongSet
|
10
|
+
|
11
|
+
# Public: Creates a Grooveshark set by logging in to Grooveshark
|
12
|
+
# with the given user.
|
13
|
+
#
|
14
|
+
# user - A String naming a Grooveshark user.
|
15
|
+
# password - A String naming the password of the user.
|
16
|
+
#
|
17
|
+
# Raises Grooveshark::InvalidAuthentication if authentication
|
18
|
+
# fails.
|
19
|
+
# Raises SocketError if the network connection fails.
|
20
|
+
def initialize(user, password)
|
21
|
+
super()
|
22
|
+
|
23
|
+
# Setup a Grooveshark session.
|
24
|
+
@client = Grooveshark::Client.new
|
25
|
+
@session = @client.session
|
26
|
+
|
27
|
+
login(user, password)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Public: Get the user's favorites from Grooveshark.
|
31
|
+
#
|
32
|
+
# Raises Grooveshark::GeneralError if the network connection fails.
|
33
|
+
#
|
34
|
+
# Returns self.
|
35
|
+
def favorites
|
36
|
+
@user.favorites.each { |s| add(Song.new(s.name, s.artist, s.album)) }
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
# Public: Add the songs in the given set to the user's favorite
|
41
|
+
# on Grooveshark.
|
42
|
+
#
|
43
|
+
# other - A SongSet to add from.
|
44
|
+
#
|
45
|
+
# Raises Grooveshark::GeneralError if the network connection fails.
|
46
|
+
#
|
47
|
+
# Returns an array of the songs that was added.
|
48
|
+
def addToFavorites(other)
|
49
|
+
songsAdded = []
|
50
|
+
|
51
|
+
other.each do |s|
|
52
|
+
songsAdded << s if s.id && @user.add_favorite(s.id)
|
53
|
+
end
|
54
|
+
|
55
|
+
songsAdded
|
56
|
+
end
|
57
|
+
|
58
|
+
# Public: Searches for the given song set at Grooveshark.
|
59
|
+
#
|
60
|
+
# other - SongSet to search for.
|
61
|
+
# strict_search - True if search should be strict (default: true).
|
62
|
+
#
|
63
|
+
# Raises Grooveshark::GeneralError if the network connection
|
64
|
+
# fails.
|
65
|
+
# Raises Grooveshark::ApiError if token is invalid.
|
66
|
+
#
|
67
|
+
# Returns a SongSet.
|
68
|
+
def search(other, strict_search = true)
|
69
|
+
result = SongSet.new
|
70
|
+
|
71
|
+
# Search for songs that are not already in this set and return
|
72
|
+
# them if they are sufficiently similar.
|
73
|
+
other.each do |song|
|
74
|
+
@client.search_songs(song.to_search_term).each do |s|
|
75
|
+
other = Song.new(s.name, s.artist, s.album,
|
76
|
+
Float(s.duration), s.id)
|
77
|
+
|
78
|
+
if strict_search
|
79
|
+
next unless song.eql?(other)
|
80
|
+
else
|
81
|
+
next unless song.similar?(other)
|
82
|
+
end
|
83
|
+
result << other
|
84
|
+
end
|
85
|
+
end
|
86
|
+
result
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
# Internal: Tries to login to Grooveshark with the given user.
|
92
|
+
#
|
93
|
+
# user - A String naming a Grooveshark user.
|
94
|
+
# password - A String naming the password of the user.
|
95
|
+
#
|
96
|
+
# Raises Grooveshark::InvalidAuthentication if authentication
|
97
|
+
# fails.
|
98
|
+
def login(user, password)
|
99
|
+
@user = @client.login(user, password)
|
100
|
+
rescue Grooveshark::InvalidAuthentication => e
|
101
|
+
raise Grooveshark::InvalidAuthentication, "#{e.message} An "\
|
102
|
+
'authenticated user is required for getting data from '\
|
103
|
+
'Grooveshark'
|
104
|
+
raise
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'highline/import'
|
4
|
+
require 'launchy'
|
5
|
+
|
6
|
+
# Public: Classes for syncing sets of songs.
|
7
|
+
module SyncSongs
|
8
|
+
# Public: Command-line interface for a Last.fm set of songs.
|
9
|
+
class LastfmCLI
|
10
|
+
|
11
|
+
# Public: Creates a CLI.
|
12
|
+
#
|
13
|
+
# controller - A Controller for a Last.fm set of songs.
|
14
|
+
# ui - General user interface to use.
|
15
|
+
def initialize(controller, ui)
|
16
|
+
@controller = controller
|
17
|
+
@ui = ui
|
18
|
+
end
|
19
|
+
|
20
|
+
# Public: Asks for a String naming a Last.fm API key and returns
|
21
|
+
# it.
|
22
|
+
def apiKey
|
23
|
+
ask("Last.fm API key for #{@controller.user}? ") { |q| q.echo = false }
|
24
|
+
end
|
25
|
+
|
26
|
+
# Public: Asks for a String naming a Last.fm API key and returns
|
27
|
+
# it.
|
28
|
+
def apiSecret
|
29
|
+
ask('Last.fm API secret for '\
|
30
|
+
"#{@controller.user}? ") { |q| q.echo = false }
|
31
|
+
end
|
32
|
+
|
33
|
+
# Public: Asks the user to authorize this tool with Last.fm and
|
34
|
+
# wait for input.
|
35
|
+
#
|
36
|
+
# url - A String naming a URL to use for authorization.
|
37
|
+
def authorize(url)
|
38
|
+
Launchy.open(url)
|
39
|
+
agree('A page asking for authorization with Last.fm should be '\
|
40
|
+
'open in your web browser. You need to approve it '\
|
41
|
+
'before proceeding. Continue? (y/n) ') do |q|
|
42
|
+
q.responses[:not_valid] = 'Enter y for yes or n for no'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require_relative 'lastfm_cli'
|
4
|
+
require_relative 'lastfm_set'
|
5
|
+
|
6
|
+
# Public: Classes for syncing sets of songs.
|
7
|
+
module SyncSongs
|
8
|
+
# Public: Controller for a Last.fm set of songs.
|
9
|
+
class LastfmController < ServiceController
|
10
|
+
|
11
|
+
# Public: Hash of types of services associated with what they
|
12
|
+
# support.
|
13
|
+
SERVICES = {loved: :rw}
|
14
|
+
|
15
|
+
# Public: Creates a controller.
|
16
|
+
#
|
17
|
+
# user - A String naming the user name or the file path for the
|
18
|
+
# service.
|
19
|
+
# name - A String naming the name of the service.
|
20
|
+
# type - A String naming the service type.
|
21
|
+
# ui - General user interface to use.
|
22
|
+
def initialize(user, name, type, ui)
|
23
|
+
super(user, name, type, ui)
|
24
|
+
|
25
|
+
@service_ui = LastfmCLI.new(self, @ui)
|
26
|
+
@set = LastfmSet.new(@service_ui.apiKey,
|
27
|
+
@service_ui.apiSecret,
|
28
|
+
@user)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Public: Wrapper for Last.fm loved songs.
|
32
|
+
def loved
|
33
|
+
@set.loved
|
34
|
+
rescue ArgumentError, EOFError, Lastfm::ApiError, SocketError, Timeout::Error => e
|
35
|
+
@ui.fail("Failed to get #{type} from #{name} #{user}\n"\
|
36
|
+
"#{e.message.strip}", 1, e)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Public: Wrapper for adding to Last.fm loved songs. Authorizes
|
40
|
+
# the session before adding songs.
|
41
|
+
#
|
42
|
+
# other - A SongSet to add from.
|
43
|
+
def addToLoved(other)
|
44
|
+
# TODO Store token somewhere instead and only call URL if there is no
|
45
|
+
# stored token.
|
46
|
+
authorized = nil
|
47
|
+
|
48
|
+
@mutex.synchronize do
|
49
|
+
authorized = @service_ui.authorize(@set.authorizeURL)
|
50
|
+
end
|
51
|
+
|
52
|
+
if authorized
|
53
|
+
begin
|
54
|
+
@set.authorizeSession
|
55
|
+
@set.addToLoved(other)
|
56
|
+
rescue Lastfm::ApiError, SocketError => e
|
57
|
+
@ui.fail("Failed to add #{type} to #{name} #{user}\n"\
|
58
|
+
"#{e.message.strip}", 1, e)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Public: Wrapper for searching for the given song set at Last.fm.
|
64
|
+
#
|
65
|
+
# other - SongSet to search for.
|
66
|
+
# limit - Maximum limit for search results (default:
|
67
|
+
# @set.limit).
|
68
|
+
# strict_search - True if search should be strict (default: true).
|
69
|
+
#
|
70
|
+
# Returns a SongSet.
|
71
|
+
def search(other, limit = @set.limit, strict_search = true)
|
72
|
+
@set.search(other, limit, strict_search)
|
73
|
+
rescue ArgumentError, EOFError, Errno::EINVAL, SocketError,
|
74
|
+
Timeout::Error => e
|
75
|
+
@ui.fail("Failed to search #{name} #{user}\n#{e.message.strip}", 1, e)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'lastfm'
|
4
|
+
require_relative '../song_set'
|
5
|
+
|
6
|
+
# Public: Classes for syncing sets of songs.
|
7
|
+
module SyncSongs
|
8
|
+
# Public: A set of Grooveshark songs.
|
9
|
+
class LastfmSet < SongSet
|
10
|
+
|
11
|
+
# Public: Default limit for API calls.
|
12
|
+
DEFAULT_LIMIT = 1_000_000
|
13
|
+
|
14
|
+
attr_reader :limit
|
15
|
+
|
16
|
+
# Public: Creates a Last.fm set by logging in to Last.fm with the
|
17
|
+
# specified user.
|
18
|
+
#
|
19
|
+
# api_key - Last.fm API key.
|
20
|
+
# api_secret - Last.fm secret for API key.
|
21
|
+
# username - The username of the Last.fm user.
|
22
|
+
# limit - The maximum number of results from calls (default:
|
23
|
+
# DEFAULT_LIMIT).
|
24
|
+
def initialize(api_key, api_secret, username = nil, limit = DEFAULT_LIMIT)
|
25
|
+
super()
|
26
|
+
@api_key = api_key
|
27
|
+
@username = username
|
28
|
+
@lastfm = Lastfm.new(api_key, api_secret)
|
29
|
+
@limit = limit
|
30
|
+
end
|
31
|
+
|
32
|
+
# Public: Get the user's loved songs from Last.fm. The reason this
|
33
|
+
# takes ages to complete is that it first has to search tracks to
|
34
|
+
# find them and then get info for each found track to be able to
|
35
|
+
# get the album title.
|
36
|
+
#
|
37
|
+
# username - The username of the user to authenticate (default:
|
38
|
+
# @username).
|
39
|
+
#
|
40
|
+
# limit - The maximum number of loved tracks to get (default:
|
41
|
+
# @limit).
|
42
|
+
#
|
43
|
+
# Raises ArgumentError from xml-simple some reason.
|
44
|
+
# Raises EOFError when end of file is reached.
|
45
|
+
# Raises Lastfm::ApiError if the username is invalid or there is a
|
46
|
+
# temporary error.
|
47
|
+
# Raises SocketError if the connection fails.
|
48
|
+
# Raises Timeout::Error if the connection fails.
|
49
|
+
#
|
50
|
+
# Returns self.
|
51
|
+
def loved(username = @username, limit = @limit)
|
52
|
+
lov = @lastfm.user.get_loved_tracks(user: username,
|
53
|
+
api_key: @api_key,
|
54
|
+
limit: limit)
|
55
|
+
|
56
|
+
if lov # Remove if API is fixed.
|
57
|
+
lov = [lov] unless lov.is_a?(Array) # Remove if API is fixed.
|
58
|
+
|
59
|
+
lov.each do |l|
|
60
|
+
|
61
|
+
# Get metadata for loved track.
|
62
|
+
s = @lastfm.track.get_info(track: l['name'],
|
63
|
+
artist: l['artist']['name'])
|
64
|
+
|
65
|
+
add(Song.new(s['name'], s['artist']['name'],
|
66
|
+
# Not all Last.fm tracks belong to an album.
|
67
|
+
s.key?('album') ? s['album']['title'] : nil,
|
68
|
+
Float(s['duration']) / 1_000, s['id']))
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
# Public: Add the songs in the given set to the given user's loved
|
76
|
+
# songs on Last.fm. This method requires an authorized session
|
77
|
+
# which is gotten by getting the user to authorize via the url
|
78
|
+
# given by authorizeURL and then running authorize.
|
79
|
+
#
|
80
|
+
# other - A SongSet to add from.
|
81
|
+
#
|
82
|
+
# Raises Lastfm::ApiError if the Last.fm token has not been
|
83
|
+
# authorized or if the song is not recognized.
|
84
|
+
# Raises SocketError if the network connection fails.
|
85
|
+
#
|
86
|
+
# Returns an array of the songs that was added.
|
87
|
+
def addToLoved(other)
|
88
|
+
songsAdded = []
|
89
|
+
|
90
|
+
if @lastfm.session
|
91
|
+
other.each do |s|
|
92
|
+
songsAdded << s if @lastfm.track.love(track: s.name,
|
93
|
+
artist: s.artist)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
songsAdded
|
98
|
+
end
|
99
|
+
|
100
|
+
# Public: Searches for the given song set at Last.fm. The reason
|
101
|
+
# this takes ages to complete is that it first has to search
|
102
|
+
# tracks to find them and then get info for each found track to be
|
103
|
+
# able to get the album title.
|
104
|
+
#
|
105
|
+
# other - SongSet to search for.
|
106
|
+
# limit - Maximum limit for search results (default:
|
107
|
+
# @limit).
|
108
|
+
# strict_search - True if search should be strict (default: true).
|
109
|
+
#
|
110
|
+
# Raises ArgumentError from xml-simple some reason.
|
111
|
+
# Raises EOFError when end of file is reached.
|
112
|
+
# Raises Errno::EINVAL if the network connection fails.
|
113
|
+
# Raises SocketError if the network connection fails.
|
114
|
+
# Raises Timeout::Error if the network connection fails.
|
115
|
+
#
|
116
|
+
# Returns a SongSet.
|
117
|
+
def search(other, limit = @limit, strict_search = true)
|
118
|
+
result = SongSet.new
|
119
|
+
|
120
|
+
# Search for songs and return them if they are sufficiently
|
121
|
+
# similar.
|
122
|
+
other.each do |song|
|
123
|
+
# The optional parameter artist for track.search does not seem
|
124
|
+
# to work so it is not used.
|
125
|
+
search_result = @lastfm.track.search(track: song.to_search_term,
|
126
|
+
limit: limit)['results']['trackmatches']['track'].compact
|
127
|
+
|
128
|
+
found_songs = []
|
129
|
+
|
130
|
+
search_result.each do |r|
|
131
|
+
found_songs << @lastfm.track.get_info(track: r['name'],
|
132
|
+
artist: r['artist'])
|
133
|
+
end
|
134
|
+
|
135
|
+
unless found_songs.empty?
|
136
|
+
found_songs.each do |f|
|
137
|
+
other = Song.new(f['name'], f['artist']['name'],
|
138
|
+
# Not all Last.fm tracks belong to an album.
|
139
|
+
f.key?('album') ? f['album']['title'] : nil,
|
140
|
+
Float(f['duration']) / 1_000, f['id'])
|
141
|
+
if strict_search
|
142
|
+
next unless song.eql?(other)
|
143
|
+
else
|
144
|
+
next unless song.similar?(other)
|
145
|
+
end
|
146
|
+
result << other
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
result
|
151
|
+
end
|
152
|
+
|
153
|
+
# Public: Return an URL for authorizing a Last.fm session.
|
154
|
+
#
|
155
|
+
# Raises SocketError if the network connection fails.
|
156
|
+
def authorizeURL
|
157
|
+
@token = @lastfm.auth.get_token
|
158
|
+
"http://www.last.fm/api/auth/?api_key=#@api_key&token=#@token"
|
159
|
+
end
|
160
|
+
|
161
|
+
|
162
|
+
# Public: Authorize a Last.fm session (needed for certain calls to
|
163
|
+
# Last.fm, such as addToLoved). Get the user to authorize via the
|
164
|
+
# URL returned by authorizeURL before calling this method.
|
165
|
+
#
|
166
|
+
# Raises SocketError if the network connection fails.
|
167
|
+
def authorizeSession
|
168
|
+
if @token
|
169
|
+
@lastfm.session = @lastfm.auth.get_session(token: @token)['key']
|
170
|
+
else
|
171
|
+
fail StandardError, "Before calling #{__method__} a token "\
|
172
|
+
'must be authorized, e.g. by calling authorizeURL and '\
|
173
|
+
'getting the user to authorize via that URL'
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require_relative '../song_set'
|
4
|
+
|
5
|
+
# Public: Classes for syncing sets of songs.
|
6
|
+
module SyncSongs
|
7
|
+
# Public: Controller for a service.
|
8
|
+
class ServiceController
|
9
|
+
|
10
|
+
attr_reader :user, :name, :type, :set, :ui
|
11
|
+
attr_accessor :action, :strict_search, :interactive, :search_result,
|
12
|
+
:songs_to_add, :added_songs
|
13
|
+
|
14
|
+
# Public: Create a service controller.
|
15
|
+
#
|
16
|
+
# controller - The main controller.
|
17
|
+
# user - A String naming the user name or the file path for
|
18
|
+
# the service.
|
19
|
+
# name - A String naming the name of the service.
|
20
|
+
# type - A String naming the service type.
|
21
|
+
def initialize(controller, user, name, type)
|
22
|
+
@controller = controller
|
23
|
+
@user = user
|
24
|
+
@name = name
|
25
|
+
@type = type
|
26
|
+
@action = action
|
27
|
+
@ui = @controller.ui # General user interface to use.
|
28
|
+
@mutex = @controller.mutex
|
29
|
+
|
30
|
+
@search_result = SongSet.new
|
31
|
+
@songs_to_add = SongSet.new
|
32
|
+
end
|
33
|
+
|
34
|
+
# Public: Returns true if this service controller is equal to the
|
35
|
+
# compared service controller. This method and hash are defined so
|
36
|
+
# that Sets of service controllers behave reasonably, i.e. service
|
37
|
+
# controller for the same user/file, name and type should be
|
38
|
+
# treated as equal.
|
39
|
+
#
|
40
|
+
# other - Service controller that this song is compared with.
|
41
|
+
def eql?(other)
|
42
|
+
user.casecmp(other.user) == 0 &&
|
43
|
+
name.casecmp(other.name) == 0 &&
|
44
|
+
type.casecmp(other.type) == 0
|
45
|
+
end
|
46
|
+
|
47
|
+
# Public: Makes a hash value for this object and returns it. This
|
48
|
+
# method and eql? are defined so that Sets of service controllers
|
49
|
+
# behave reasonably, i.e. service controller for the same
|
50
|
+
# user/file, name and type should be treated as equal.
|
51
|
+
def hash
|
52
|
+
[user, name, type].join('').downcase.hash
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
# Public: Classes for syncing sets of songs.
|
4
|
+
module SyncSongs
|
5
|
+
# Public: Stores a song.
|
6
|
+
class Song
|
7
|
+
include Comparable
|
8
|
+
|
9
|
+
# Public: Returns the name of the song and the artist performing the
|
10
|
+
# song.
|
11
|
+
attr_reader :name, :artist, :album, :duration, :id
|
12
|
+
|
13
|
+
# Public: Creates a song. Leading and trailing whitespace is
|
14
|
+
# removed as it has no semantic significance for songs.
|
15
|
+
#
|
16
|
+
# name - The String name of the song.
|
17
|
+
# artist - The String artist performing the song.
|
18
|
+
# album - The String album the song is found on (default: nil).
|
19
|
+
# duration - The Float duration of the song in seconds (default:
|
20
|
+
# nil).
|
21
|
+
# id - An String id of for the song (default: nil).
|
22
|
+
# Is service relative.
|
23
|
+
#
|
24
|
+
# Raises ArgumentError if the artist or the name is empty.
|
25
|
+
def initialize(name, artist, album = nil, duration = nil, id = nil)
|
26
|
+
@name = name.strip
|
27
|
+
@artist = artist.strip
|
28
|
+
@album = album.strip if album
|
29
|
+
@duration = Float(duration) if duration
|
30
|
+
@id = id
|
31
|
+
|
32
|
+
if @name.empty? || @artist.empty?
|
33
|
+
fail ArgumentError, 'Songs must have a non-empty name and artist'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Public: Comparison -- returns -1 if other song is greater than,
|
38
|
+
# 0 if other song equal to and +1 if other song is less than this
|
39
|
+
# song.
|
40
|
+
#
|
41
|
+
# other - Song that this song is compared with.
|
42
|
+
def <=>(other)
|
43
|
+
comp = name.casecmp(other.name)
|
44
|
+
|
45
|
+
comp = artist.casecmp(other.artist) if comp == 0
|
46
|
+
|
47
|
+
if comp == 0 && album && other.album
|
48
|
+
comp = album.casecmp(other.album)
|
49
|
+
end
|
50
|
+
|
51
|
+
comp
|
52
|
+
end
|
53
|
+
|
54
|
+
# Public: Returns true if this song is equal to the compared song.
|
55
|
+
#
|
56
|
+
# other - Song that this song is compared with.
|
57
|
+
def eql?(other)
|
58
|
+
(self <=> other) == 0
|
59
|
+
end
|
60
|
+
|
61
|
+
# Public: Returns true if this song includes the other song.
|
62
|
+
#
|
63
|
+
# other - Song that this song is compared with.
|
64
|
+
def include?(other)
|
65
|
+
name.downcase.include?(other.name.downcase) &&
|
66
|
+
artist.downcase.include?(other.artist.downcase)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Public: Returns true if this song is similar to the compared
|
70
|
+
# song.
|
71
|
+
#
|
72
|
+
# other - Song that this song is compared with.
|
73
|
+
def similar?(other)
|
74
|
+
# Since the other song is more probably a song from a search in
|
75
|
+
# a big database with many versions of every song the following
|
76
|
+
# test order should perform better.
|
77
|
+
other.include?(self) || include?(other)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Public: Makes a hash value for this object and returns it.
|
81
|
+
def hash
|
82
|
+
[artist, name, album].compact.join('').downcase.hash
|
83
|
+
end
|
84
|
+
|
85
|
+
# Public: Returns the song formatted as a string.
|
86
|
+
def to_s
|
87
|
+
s = [artist, name, album].compact
|
88
|
+
s << Time.at(duration).utc.strftime('%H:%M:%S') if duration
|
89
|
+
s.join(' - ')
|
90
|
+
end
|
91
|
+
|
92
|
+
# Public: Returns the song formatted as appropriately for use in a
|
93
|
+
# search query.
|
94
|
+
def to_search_term
|
95
|
+
# When including album search on Last.fm barely finds anything.
|
96
|
+
[artist, name].compact.join(' ')
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'delegate'
|
4
|
+
require 'set'
|
5
|
+
require_relative 'song'
|
6
|
+
|
7
|
+
# Public: Classes for syncing sets of songs.
|
8
|
+
module SyncSongs
|
9
|
+
# Public: A set of songs.
|
10
|
+
class SongSet < SimpleDelegator
|
11
|
+
|
12
|
+
# Public: Creates a new set.
|
13
|
+
#
|
14
|
+
# *songs - If songs are provided they are added to the set
|
15
|
+
# (default: nil).
|
16
|
+
def initialize(*songs)
|
17
|
+
@songs = Set.new
|
18
|
+
super(@songs)
|
19
|
+
songs.each { |song| @songs << song } if songs
|
20
|
+
end
|
21
|
+
|
22
|
+
# Public: Returns songs that are in the given list but not in this
|
23
|
+
# list, i.e. songs that are exclusive to the given list.
|
24
|
+
#
|
25
|
+
# other - SongList to compare this list to.
|
26
|
+
def exclusiveTo(other)
|
27
|
+
other - @songs
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/sync_songs.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler/setup'
|
5
|
+
|
6
|
+
PATH = './sync_songs/'
|
7
|
+
SERVICES_PATH = "#{PATH}services/"
|
8
|
+
|
9
|
+
# Load the library.
|
10
|
+
require_relative "#{PATH}cli"
|
11
|
+
require_relative "#{PATH}controller"
|
12
|
+
require_relative "#{PATH}version"
|
13
|
+
|
14
|
+
require_relative "#{SERVICES_PATH}service_controller"
|
15
|
+
|
16
|
+
# Load sub classes of ServiceController.
|
17
|
+
require_relative "#{SERVICES_PATH}csv_controller"
|
18
|
+
require_relative "#{SERVICES_PATH}grooveshark_controller"
|
19
|
+
require_relative "#{SERVICES_PATH}lastfm_controller"
|
20
|
+
|
21
|
+
# Internal: A sync direction.
|
22
|
+
Struct.new('Direction',
|
23
|
+
# Internal: An array of two keys two services to sync
|
24
|
+
# between.
|
25
|
+
:services,
|
26
|
+
# Internal: The direction to sync in.
|
27
|
+
:direction)
|
data/plan.org
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# -*- mode:org; indent-tabs-mode:nil; tab-width:2 -*-
|
2
|
+
#+title: Plan
|
3
|
+
|
4
|
+
* TODO Plan
|
5
|
+
- Use https://gist.github.com/fnichol/1912050
|
6
|
+
- Remove =lov = [lov] unless lov.is_a?(Array)= from last_fm_set.rb if https://github.com/youpy/ruby-lastfm/issues/52 is fixed.
|
7
|
+
- Go through all documentation and fix it.
|
8
|
+
- Fix all warnings upstream.
|
9
|
+
- Add tests for all new things.
|
10
|
+
- Add tests for songs with album.
|
11
|
+
- Use watir for testing last.fm auth.
|
12
|
+
- Add examples to documentation as in the Tomdoc specification.
|
13
|
+
- Add argument types to documentation, from Tomdoc: "The expected type (or types) of each argument SHOULD be clearly indicated in the explanation. When you specify a type, use the proper classname of the type (for instance, use 'String' instead of 'string' to refer to a String type)."
|
14
|
+
- Make singleton classes that can carry API keys and passwords for a particular user so that one can check if such an instance is running and use it rather than asking the user for the same password again.
|
15
|
+
- Consider adding support for the following: librefm, rhythmbox, gogoyoko, jamendo.
|
16
|
+
- Use YAML.dump and lib tmpdir to store Last.fm token?
|
17
|
+
- Integrate with Travis CI to automate tests.
|
data/sync_songs.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- coding: utf-8; mode: ruby -*-
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'sync_songs/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |gem|
|
8
|
+
gem.name = 'sync_songs'
|
9
|
+
gem.version = SyncSongs::VERSION
|
10
|
+
gem.authors = ['Sleft']
|
11
|
+
gem.email = ['fte08eas@student.lu.se']
|
12
|
+
gem.description = 'Sync sets of songs'
|
13
|
+
gem.summary = 'SyncSongs'
|
14
|
+
gem.homepage = 'https://github.com/Sleft/sync_songs'
|
15
|
+
|
16
|
+
gem.files = `git ls-files`.split($/)
|
17
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
18
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
19
|
+
gem.require_paths = ['lib']
|
20
|
+
gem.add_runtime_dependency 'grooveshark', ['~>0.2.7']
|
21
|
+
gem.add_runtime_dependency 'highline', ['~>1.6.16']
|
22
|
+
gem.add_runtime_dependency 'lastfm', ['~>1.17.0']
|
23
|
+
gem.add_runtime_dependency 'launchy', ['~>2.2.0']
|
24
|
+
gem.add_runtime_dependency 'thor', ['~>0.18.1']
|
25
|
+
end
|