spotify-to-mp3 0.4.1 → 0.4.2
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/{README.markdown → README.md} +4 -4
- data/bin/spotify-to-mp3 +3 -189
- data/lib/spotify_to_mp3.rb +58 -0
- data/lib/spotify_to_mp3/services.rb +40 -0
- data/lib/spotify_to_mp3/songs.rb +65 -0
- data/lib/spotify_to_mp3/sources.rb +43 -0
- data/spotify-to-mp3.gemspec +2 -1
- metadata +67 -61
@@ -42,6 +42,10 @@ Make sure you have rubygems installed and configured:
|
|
42
42
|
|
43
43
|
## Changelog
|
44
44
|
|
45
|
+
2012-01-11
|
46
|
+
|
47
|
+
- Touch already downloaded songs. This way songs no more in the download list can be spotted easily.
|
48
|
+
|
45
49
|
2011-10-03
|
46
50
|
|
47
51
|
- Make it work on ruby 1.8.7 (Snow Leopard)
|
@@ -53,11 +57,7 @@ Make sure you have rubygems installed and configured:
|
|
53
57
|
## TODO
|
54
58
|
|
55
59
|
- Consider multiple artists songs
|
56
|
-
- Estimated download time
|
57
|
-
- Per song
|
58
|
-
- Total
|
59
60
|
- Distinguish internal errors from user errors
|
60
61
|
- Filter Grooveshark results by artist, title and length
|
61
62
|
- Split code in multiple files
|
62
|
-
- --sync option to add/remove \*.mp3 files to match songs file
|
63
63
|
- It seems Grooveshark API fails after hundreds of calls
|
data/bin/spotify-to-mp3
CHANGED
@@ -1,193 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
|
4
|
-
require 'grooveshark'
|
5
|
-
require 'open-uri'
|
6
|
-
require 'colorize'
|
7
|
-
require 'cgi'
|
3
|
+
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
8
4
|
|
9
|
-
|
10
|
-
def initialize
|
11
|
-
@context = {
|
12
|
-
:grooveshark => Grooveshark.new,
|
13
|
-
:spotify => Spotify.new
|
14
|
-
}
|
15
|
-
end
|
5
|
+
require 'spotify_to_mp3'
|
16
6
|
|
17
|
-
|
18
|
-
begin
|
19
|
-
file = ARGV.first
|
20
|
-
raise "No songs file specified. Usage: #{$0} file" if file.nil?
|
21
|
-
|
22
|
-
File.open(file).each_line{|song_id|
|
23
|
-
song_id.strip!
|
24
|
-
next if song_id.empty?
|
25
|
-
begin
|
26
|
-
song = UnresolvedSong.new(@context, song_id)
|
27
|
-
print "Resolving \"#{song}\" "
|
28
|
-
song = song.resolve
|
29
|
-
print "-> Searching \"#{song}\" on Grooveshark "
|
30
|
-
song = song.from_grooveshark
|
31
|
-
print "-> Downloading \"#{song}\" "
|
32
|
-
if song.downloaded
|
33
|
-
puts "Already exists, skipping".green
|
34
|
-
else
|
35
|
-
song.download
|
36
|
-
puts "Done".green
|
37
|
-
end
|
38
|
-
rescue
|
39
|
-
puts "#{$!}".red
|
40
|
-
end
|
41
|
-
}
|
42
|
-
rescue
|
43
|
-
puts "#{$!}".red
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
private
|
48
|
-
|
49
|
-
class UnresolvedSong
|
50
|
-
def initialize(context, id)
|
51
|
-
@context = context
|
52
|
-
@id = id
|
53
|
-
end
|
54
|
-
|
55
|
-
def resolve
|
56
|
-
if @id.start_with?('http://open.spotify.com/track/', 'spotify:track:')
|
57
|
-
track = @context[:spotify].track(@id)
|
58
|
-
source = SpotifySource.new(@context, track)
|
59
|
-
else
|
60
|
-
source = PlainSource.new(@context, @id)
|
61
|
-
end
|
62
|
-
ResolvedSong.new(@context, source)
|
63
|
-
end
|
64
|
-
|
65
|
-
def to_s
|
66
|
-
@id
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
class Source
|
71
|
-
def grooveshark_query
|
72
|
-
raise "Not implemented"
|
73
|
-
end
|
74
|
-
|
75
|
-
def to_s
|
76
|
-
raise "Not implemented"
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
class SpotifySource < Source
|
81
|
-
def initialize(context, track)
|
82
|
-
@context = context
|
83
|
-
@track = track
|
84
|
-
end
|
85
|
-
|
86
|
-
def grooveshark_query
|
87
|
-
"artist:\"#{@track.artist}\" title:\"#{@track.name}\""
|
88
|
-
end
|
89
|
-
|
90
|
-
def to_s
|
91
|
-
"#{@track.artist} - #{@track.name}"
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
class PlainSource < Source
|
96
|
-
attr_reader :grooveshark_query, :to_s
|
97
|
-
|
98
|
-
def initialize(context, id)
|
99
|
-
parts = id.split(' - ', 2)
|
100
|
-
if parts.length < 2
|
101
|
-
@grooveshark_query = id
|
102
|
-
@to_s = id
|
103
|
-
else
|
104
|
-
artist, name = parts
|
105
|
-
@grooveshark_query = "artist:\"#{artist}\" title:\"#{name}\""
|
106
|
-
@to_s = "#{artist} - #{name}"
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
class ResolvedSong
|
112
|
-
def initialize(context, source)
|
113
|
-
@context = context
|
114
|
-
@source = source
|
115
|
-
end
|
116
|
-
|
117
|
-
def from_grooveshark
|
118
|
-
GroovesharkSong.new(@context, @source.grooveshark_query)
|
119
|
-
end
|
120
|
-
|
121
|
-
def to_s
|
122
|
-
"#{@source}"
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
class GroovesharkSong
|
127
|
-
def initialize(context, query)
|
128
|
-
@context = context
|
129
|
-
@raw_grooveshark_song = @context[:grooveshark].song(query)
|
130
|
-
end
|
131
|
-
|
132
|
-
def downloaded
|
133
|
-
File.exists?(filename)
|
134
|
-
end
|
135
|
-
|
136
|
-
def download
|
137
|
-
download_url(@context[:grooveshark].song_url(@raw_grooveshark_song))
|
138
|
-
end
|
139
|
-
|
140
|
-
def to_s
|
141
|
-
"#{@raw_grooveshark_song.artist} - #{@raw_grooveshark_song.name}"
|
142
|
-
end
|
143
|
-
|
144
|
-
private
|
145
|
-
|
146
|
-
def filename
|
147
|
-
name = "#{@raw_grooveshark_song.artist} - #{@raw_grooveshark_song.name}.mp3"
|
148
|
-
name.tr('/', '-') # / is not allowed in file names
|
149
|
-
end
|
150
|
-
|
151
|
-
def download_url(url)
|
152
|
-
content = open(url).read
|
153
|
-
file = open(filename, 'wb')
|
154
|
-
file.write(content)
|
155
|
-
file.close
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
class Grooveshark
|
160
|
-
def initialize
|
161
|
-
@client = ::Grooveshark::Client.new
|
162
|
-
end
|
163
|
-
|
164
|
-
def song_url(song)
|
165
|
-
@client.get_song_url(song)
|
166
|
-
end
|
167
|
-
|
168
|
-
def song(query)
|
169
|
-
song = @client.search_songs(query).first
|
170
|
-
raise "Song not found" if song.nil?
|
171
|
-
song
|
172
|
-
end
|
173
|
-
end
|
174
|
-
|
175
|
-
class Spotify
|
176
|
-
def track(uri)
|
177
|
-
content = open('http://ws.spotify.com/lookup/1/.json?uri=' + CGI.escape(uri))
|
178
|
-
json = JSON.parse(content.string)
|
179
|
-
Track.new(json)
|
180
|
-
end
|
181
|
-
|
182
|
-
class Track
|
183
|
-
attr_reader :name, :artist
|
184
|
-
|
185
|
-
def initialize(json)
|
186
|
-
@name = json['track']['name']
|
187
|
-
@artist = json['track']['artists'].first['name']
|
188
|
-
end
|
189
|
-
end
|
190
|
-
end
|
191
|
-
end
|
192
|
-
|
193
|
-
App.new.run
|
7
|
+
SpotifyToMp3::App.new.run
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'grooveshark'
|
3
|
+
require 'open-uri'
|
4
|
+
require 'colorize'
|
5
|
+
require 'rest-client'
|
6
|
+
require 'fileutils'
|
7
|
+
|
8
|
+
require 'spotify_to_mp3/services'
|
9
|
+
require 'spotify_to_mp3/songs'
|
10
|
+
require 'spotify_to_mp3/sources'
|
11
|
+
|
12
|
+
module SpotifyToMp3
|
13
|
+
|
14
|
+
class App
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@context = {
|
18
|
+
:grooveshark => Grooveshark.new,
|
19
|
+
:spotify => Spotify.new
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def run
|
24
|
+
begin
|
25
|
+
file = ARGV.first
|
26
|
+
raise "No songs file specified. Usage: #{$0} file" if file.nil?
|
27
|
+
|
28
|
+
File.open(file).each_line{|song_id|
|
29
|
+
song_id.strip!
|
30
|
+
next if song_id.empty?
|
31
|
+
begin
|
32
|
+
song = UnresolvedSong.new(@context, song_id)
|
33
|
+
print "Resolving \"#{song}\" "
|
34
|
+
song = song.resolve
|
35
|
+
print "-> Searching \"#{song}\" on Grooveshark "
|
36
|
+
song = song.from_grooveshark
|
37
|
+
print "-> Downloading \"#{song}\" "
|
38
|
+
if File.exists? song.filename
|
39
|
+
FileUtils.touch song.filename # To know about songs no longer in download list
|
40
|
+
puts "Already exists, skipping".green
|
41
|
+
else
|
42
|
+
begin
|
43
|
+
song.download
|
44
|
+
puts "Done".green
|
45
|
+
rescue Exception => exception
|
46
|
+
puts exception.message.red
|
47
|
+
end
|
48
|
+
end
|
49
|
+
rescue
|
50
|
+
puts "#{$!}".red
|
51
|
+
end
|
52
|
+
}
|
53
|
+
rescue
|
54
|
+
puts "#{$!}".red
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module SpotifyToMp3
|
4
|
+
|
5
|
+
class Grooveshark
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@client = ::Grooveshark::Client.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def song_url(song)
|
12
|
+
@client.get_song_url(song)
|
13
|
+
end
|
14
|
+
|
15
|
+
def song(query)
|
16
|
+
song = @client.search_songs(query).first
|
17
|
+
raise "Song not found" if song.nil?
|
18
|
+
song
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Spotify
|
23
|
+
|
24
|
+
def track(uri)
|
25
|
+
content = open('http://ws.spotify.com/lookup/1/.json?uri=' + CGI.escape(uri))
|
26
|
+
json = JSON.parse(content.string)
|
27
|
+
Track.new(json)
|
28
|
+
end
|
29
|
+
|
30
|
+
class Track
|
31
|
+
|
32
|
+
attr_reader :name, :artist
|
33
|
+
|
34
|
+
def initialize(json)
|
35
|
+
@name = json['track']['name']
|
36
|
+
@artist = json['track']['artists'].first['name']
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
class UnresolvedSong
|
2
|
+
|
3
|
+
def initialize(context, id)
|
4
|
+
@context = context
|
5
|
+
@id = id
|
6
|
+
end
|
7
|
+
|
8
|
+
def resolve
|
9
|
+
if @id.start_with?('http://open.spotify.com/track/', 'spotify:track:')
|
10
|
+
track = @context[:spotify].track(@id)
|
11
|
+
source = SpotifySource.new(@context, track)
|
12
|
+
else
|
13
|
+
source = PlainSource.new(@context, @id)
|
14
|
+
end
|
15
|
+
ResolvedSong.new(@context, source)
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
@id
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class ResolvedSong
|
24
|
+
|
25
|
+
def initialize(context, source)
|
26
|
+
@context = context
|
27
|
+
@source = source
|
28
|
+
end
|
29
|
+
|
30
|
+
def from_grooveshark
|
31
|
+
GroovesharkSong.new(@context, @source.grooveshark_query)
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
"#{@source}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class GroovesharkSong
|
40
|
+
|
41
|
+
def initialize(context, query)
|
42
|
+
@context = context
|
43
|
+
@raw_grooveshark_song = @context[:grooveshark].song(query)
|
44
|
+
end
|
45
|
+
|
46
|
+
def download
|
47
|
+
download_url(@context[:grooveshark].song_url(@raw_grooveshark_song))
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_s
|
51
|
+
"#{@raw_grooveshark_song.artist} - #{@raw_grooveshark_song.name}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def filename
|
55
|
+
name = "#{@raw_grooveshark_song.artist} - #{@raw_grooveshark_song.name}.mp3"
|
56
|
+
name.tr('/', '-') # / is not allowed in file names
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def download_url(url)
|
62
|
+
file = RestClient::Request.execute(:method => :post, :url => url, :raw_response => true).file
|
63
|
+
FileUtils.mv(file.path, filename)
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class Source
|
2
|
+
|
3
|
+
def grooveshark_query
|
4
|
+
raise "Not implemented"
|
5
|
+
end
|
6
|
+
|
7
|
+
def to_s
|
8
|
+
raise "Not implemented"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class SpotifySource < Source
|
13
|
+
|
14
|
+
def initialize(context, track)
|
15
|
+
@context = context
|
16
|
+
@track = track
|
17
|
+
end
|
18
|
+
|
19
|
+
def grooveshark_query
|
20
|
+
"artist:\"#{@track.artist}\" title:\"#{@track.name}\""
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
"#{@track.artist} - #{@track.name}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class PlainSource < Source
|
29
|
+
|
30
|
+
attr_reader :grooveshark_query, :to_s
|
31
|
+
|
32
|
+
def initialize(context, id)
|
33
|
+
parts = id.split(' - ', 2)
|
34
|
+
if parts.length < 2
|
35
|
+
@grooveshark_query = id
|
36
|
+
@to_s = id
|
37
|
+
else
|
38
|
+
artist, name = parts
|
39
|
+
@grooveshark_query = "artist:\"#{artist}\" title:\"#{name}\""
|
40
|
+
@to_s = "#{artist} - #{name}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/spotify-to-mp3.gemspec
CHANGED
@@ -2,13 +2,14 @@ Gem::Specification.new do |s|
|
|
2
2
|
s.name = 'spotify-to-mp3'
|
3
3
|
s.summary = 'Spotify to MP3'
|
4
4
|
s.description = 'Download MP3 files of Spotify tracks'
|
5
|
-
s.version = '0.4.
|
5
|
+
s.version = '0.4.2'
|
6
6
|
s.author = 'Francesc Rosàs'
|
7
7
|
s.email = 'francescrosasbosque@gmail.com'
|
8
8
|
s.homepage = 'https://github.com/frosas/spotify-to-mp3'
|
9
9
|
|
10
10
|
s.add_dependency 'grooveshark'
|
11
11
|
s.add_dependency 'colorize'
|
12
|
+
s.add_dependency 'rest-client'
|
12
13
|
|
13
14
|
s.files = `git ls-files`.split("\n")
|
14
15
|
s.executables = `git ls-files -- bin/*`.split("\n").map{|f| File.basename(f)}
|
metadata
CHANGED
@@ -1,96 +1,102 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: spotify-to-mp3
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.2
|
5
5
|
prerelease:
|
6
|
-
segments:
|
7
|
-
- 0
|
8
|
-
- 4
|
9
|
-
- 1
|
10
|
-
version: 0.4.1
|
11
6
|
platform: ruby
|
12
|
-
authors:
|
13
|
-
-
|
7
|
+
authors:
|
8
|
+
- Francesc Rosàs
|
14
9
|
autorequire:
|
15
10
|
bindir: bin
|
16
11
|
cert_chain: []
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
- !ruby/object:Gem::Dependency
|
12
|
+
date: 2012-08-19 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
21
15
|
name: grooveshark
|
22
|
-
|
23
|
-
requirement: &id001 !ruby/object:Gem::Requirement
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
24
17
|
none: false
|
25
|
-
requirements:
|
26
|
-
- -
|
27
|
-
- !ruby/object:Gem::Version
|
28
|
-
|
29
|
-
segments:
|
30
|
-
- 0
|
31
|
-
version: "0"
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
32
22
|
type: :runtime
|
33
|
-
|
34
|
-
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
35
31
|
name: colorize
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
36
39
|
prerelease: false
|
37
|
-
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rest-client
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
38
49
|
none: false
|
39
|
-
requirements:
|
40
|
-
- -
|
41
|
-
- !ruby/object:Gem::Version
|
42
|
-
|
43
|
-
segments:
|
44
|
-
- 0
|
45
|
-
version: "0"
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
46
54
|
type: :runtime
|
47
|
-
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
48
62
|
description: Download MP3 files of Spotify tracks
|
49
63
|
email: francescrosasbosque@gmail.com
|
50
|
-
executables:
|
64
|
+
executables:
|
51
65
|
- spotify-to-mp3
|
52
66
|
extensions: []
|
53
|
-
|
54
67
|
extra_rdoc_files: []
|
55
|
-
|
56
|
-
files:
|
68
|
+
files:
|
57
69
|
- Gemfile
|
58
70
|
- Gemfile.lock
|
59
|
-
- README.
|
71
|
+
- README.md
|
60
72
|
- bin/spotify-to-mp3
|
73
|
+
- lib/spotify_to_mp3.rb
|
74
|
+
- lib/spotify_to_mp3/services.rb
|
75
|
+
- lib/spotify_to_mp3/songs.rb
|
76
|
+
- lib/spotify_to_mp3/sources.rb
|
61
77
|
- spotify-to-mp3.gemspec
|
62
78
|
homepage: https://github.com/frosas/spotify-to-mp3
|
63
79
|
licenses: []
|
64
|
-
|
65
80
|
post_install_message:
|
66
81
|
rdoc_options: []
|
67
|
-
|
68
|
-
require_paths:
|
82
|
+
require_paths:
|
69
83
|
- .
|
70
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
71
85
|
none: false
|
72
|
-
requirements:
|
73
|
-
- -
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
|
76
|
-
|
77
|
-
- 0
|
78
|
-
version: "0"
|
79
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ! '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
91
|
none: false
|
81
|
-
requirements:
|
82
|
-
- -
|
83
|
-
- !ruby/object:Gem::Version
|
84
|
-
|
85
|
-
segments:
|
86
|
-
- 0
|
87
|
-
version: "0"
|
92
|
+
requirements:
|
93
|
+
- - ! '>='
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
88
96
|
requirements: []
|
89
|
-
|
90
97
|
rubyforge_project:
|
91
|
-
rubygems_version: 1.8.
|
98
|
+
rubygems_version: 1.8.24
|
92
99
|
signing_key:
|
93
100
|
specification_version: 3
|
94
101
|
summary: Spotify to MP3
|
95
102
|
test_files: []
|
96
|
-
|