sync_songs 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|