spotify-to-mp3 0.4.2 → 0.5

Sign up to get free protection for your applications and to get access to all the features.
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color --format nested
data/README.md CHANGED
@@ -42,6 +42,10 @@ Make sure you have rubygems installed and configured:
42
42
 
43
43
  ## Changelog
44
44
 
45
+ 2012-08-20
46
+
47
+ - Set filename artist and title from Grooveshark
48
+
45
49
  2012-01-11
46
50
 
47
51
  - Touch already downloaded songs. This way songs no more in the download list can be spotted easily.
@@ -57,7 +61,8 @@ Make sure you have rubygems installed and configured:
57
61
  ## TODO
58
62
 
59
63
  - Consider multiple artists songs
60
- - Distinguish internal errors from user errors
61
64
  - Filter Grooveshark results by artist, title and length
62
- - Split code in multiple files
63
65
  - It seems Grooveshark API fails after hundreds of calls
66
+ - Accept tracks from stdin (instead of from a file)
67
+ - Test downloads
68
+ - Test App class
data/bin/spotify-to-mp3 CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
+ # To run it in development: ruby -Ilib bin/spotify-to-mp3
4
4
 
5
5
  require 'spotify_to_mp3'
6
6
 
7
- SpotifyToMp3::App.new.run
7
+ SpotifyToMp3::DependencyInjection.new.app.run
@@ -1,58 +1,16 @@
1
- require 'rubygems'
2
1
  require 'grooveshark'
3
2
  require 'open-uri'
4
3
  require 'colorize'
5
4
  require 'rest-client'
6
5
  require 'fileutils'
6
+ require 'cgi'
7
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
8
+ require 'spotify_to_mp3/app'
9
+ require 'spotify_to_mp3/app/file_track_ids'
10
+ require 'spotify_to_mp3/dependency_injection'
11
+ require 'spotify_to_mp3/spotify'
12
+ require 'spotify_to_mp3/spotify/track'
13
+ require 'spotify_to_mp3/track_id_resolver'
14
+ require 'spotify_to_mp3/track'
15
+ require 'spotify_to_mp3/grooveshark'
16
+ require 'spotify_to_mp3/grooveshark/track'
@@ -0,0 +1,35 @@
1
+ module SpotifyToMp3
2
+ class App
3
+ def initialize(track_id_resolver, grooveshark)
4
+ @track_id_resolver = track_id_resolver
5
+ @grooveshark = grooveshark
6
+ end
7
+
8
+ def run
9
+ file = ARGV.first or raise "No songs file specified. Usage: #{$0} file"
10
+ FileTrackIds.new(file).each do |track_id|
11
+ begin
12
+ puts "Resolving \"#{track_id}\" ".blue
13
+ track = @track_id_resolver.resolve(track_id)
14
+
15
+ puts "Searching \"#{track}\" on Grooveshark ".blue
16
+ grooveshark_track = @grooveshark.get_track(track.grooveshark_query)
17
+
18
+ puts "Downloading \"#{grooveshark_track}\" ".blue
19
+ if File.exists? grooveshark_track.filename
20
+ FileUtils.touch grooveshark_track.filename # To know about songs no longer in download list
21
+ puts "Already exists, skipping".green
22
+ else
23
+ @grooveshark.download(grooveshark_track)
24
+ puts "Done".green
25
+ end
26
+ rescue Exception => exception # For some reason without the "Exception" it is ignored
27
+ puts exception.message.red
28
+ # Continue with the next track
29
+ end
30
+ end
31
+ rescue
32
+ puts "#{$!}".red
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,21 @@
1
+ module SpotifyToMp3
2
+ class App
3
+ class FileTrackIds
4
+ include Enumerable
5
+
6
+ def initialize(file)
7
+ @file = file
8
+ end
9
+
10
+ def each
11
+ File.open(@file) do |file|
12
+ file.each do |track_id|
13
+ track_id.strip!
14
+ next if track_id.empty?
15
+ yield track_id
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ module SpotifyToMp3
2
+ class DependencyInjection
3
+ def track_id_resolver
4
+ @track_id_resolver ||= TrackIdResolver.new(Spotify.new)
5
+ end
6
+
7
+ def grooveshark
8
+ @grooveshark ||= Grooveshark.new(::Grooveshark::Client.new)
9
+ end
10
+
11
+ def app
12
+ @app ||= App.new(track_id_resolver, grooveshark)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ module SpotifyToMp3
2
+ class Grooveshark
3
+ def initialize(client)
4
+ @client = client
5
+ end
6
+
7
+ def get_track(query)
8
+ client_track = @client.search_songs(query).first or raise "Track not found"
9
+ Track.new(client_track)
10
+ end
11
+
12
+ def download(track)
13
+ url = @client.get_song_url(track.client_track)
14
+ file = RestClient::Request.execute(:method => :post, :url => url, :raw_response => true).file
15
+ FileUtils.mv(file.path, track.filename)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ module SpotifyToMp3
2
+ class Grooveshark
3
+ class Track
4
+ attr_reader :client_track
5
+
6
+ def initialize(client_track)
7
+ @client_track = client_track
8
+ end
9
+
10
+ def to_s
11
+ "#{@client_track.artist} - #{@client_track.name}"
12
+ end
13
+
14
+ def filename
15
+ "#{self}.mp3".tr('/', '-') # / is not allowed in file names
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ module SpotifyToMp3
2
+ class Spotify
3
+ def get_track(uri)
4
+ content = open('http://ws.spotify.com/lookup/1/.json?uri=' + CGI.escape(uri))
5
+ json = JSON.parse(content.string)
6
+ Track.new(json)
7
+ end
8
+
9
+ def resolvable_uri?(uri)
10
+ uri.start_with?('http://open.spotify.com/track/', 'spotify:track:')
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ module SpotifyToMp3
2
+ class Spotify
3
+ class Track
4
+ attr_reader :name, :artist
5
+
6
+ def initialize(json)
7
+ @name = json['track']['name']
8
+ @artist = json['track']['artists'].first['name']
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ module SpotifyToMp3
2
+ class Track
3
+ attr_reader :grooveshark_query
4
+
5
+ def initialize(description, grooveshark_query = nil)
6
+ @description = description
7
+ @grooveshark_query = grooveshark_query || @description
8
+ end
9
+
10
+ def to_s
11
+ @description
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,26 @@
1
+ module SpotifyToMp3
2
+ class TrackIdResolver
3
+ def initialize(spotify)
4
+ @spotify = spotify
5
+ end
6
+
7
+ def resolve(track_id)
8
+ resolve_spotify_track(track_id) || resolve_plain_track(track_id)
9
+ end
10
+
11
+ private
12
+
13
+ def resolve_spotify_track(id)
14
+ if @spotify.resolvable_uri?(id)
15
+ spotify_track = @spotify.get_track(id)
16
+ description = "#{spotify_track.artist} - #{spotify_track.name}"
17
+ grooveshark_query = "artist:\"#{spotify_track.artist}\" title:\"#{spotify_track.name}\""
18
+ Track.new(description, grooveshark_query)
19
+ end
20
+ end
21
+
22
+ def resolve_plain_track(id)
23
+ Track.new(id)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,32 @@
1
+ require 'spotify_to_mp3'
2
+ require 'tempfile'
3
+
4
+ module SpotifyToMp3
5
+ describe App::FileTrackIds do
6
+ it "reads lines" do
7
+ open_test_file("1\n2\n3") do |ids|
8
+ ids.count.should == 3
9
+ end
10
+ end
11
+
12
+ it "trims spaces" do
13
+ open_test_file(" 1 ") do |ids|
14
+ ids.each do |id| id.should == "1" end
15
+ end
16
+ end
17
+
18
+ it "ignores empty lines" do
19
+ open_test_file("1\n\n2\n") do |ids|
20
+ ids.count.should == 2
21
+ end
22
+ end
23
+
24
+ def open_test_file(content)
25
+ Tempfile.open('tracks') do |file|
26
+ file.write(content)
27
+ file.rewind
28
+ yield App::FileTrackIds.new(file)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,23 @@
1
+ require 'spotify_to_mp3'
2
+
3
+ module SpotifyToMp3
4
+ describe Grooveshark do
5
+ context "#get_track" do
6
+ before(:each) do
7
+ @grooveshark = DependencyInjection.new.grooveshark
8
+ end
9
+
10
+ it "finds by plain query" do
11
+ @grooveshark.get_track("Cake - I Will Survive").should be
12
+ end
13
+
14
+ it "finds by artist and title" do
15
+ @grooveshark.get_track('artist:"Cake" title:"I Will Survive"').should be
16
+ end
17
+
18
+ it "fails with nonexistent tracks" do
19
+ expect { @grooveshark.get_track('XXXXXXXXXXXXXXX') }.to raise_error
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,41 @@
1
+ require 'spotify_to_mp3'
2
+
3
+ module SpotifyToMp3
4
+ describe Spotify do
5
+ before(:each) do
6
+ @spotify = SpotifyToMp3::Spotify.new
7
+ end
8
+
9
+ context '#get_track' do
10
+ it "resolves HTTP URLs" do
11
+ track = @spotify.get_track('http://open.spotify.com/track/0qgiFuYhYuwtFXEwYakddE')
12
+ track.name.should == "I Will Survive"
13
+ track.artist.should == "Cake"
14
+ end
15
+
16
+ it "resolves Spotify URIs" do
17
+ track = @spotify.get_track('spotify:track:0qgiFuYhYuwtFXEwYakddE')
18
+ track.name.should == "I Will Survive"
19
+ track.artist.should == "Cake"
20
+ end
21
+
22
+ it "fails on nonexistent URI" do
23
+ expect { @spotify.get_track('spotify:track:not-found') }.to raise_error
24
+ end
25
+ end
26
+
27
+ context "#resolvable_uri?" do
28
+ it "accepts HTTP URLs" do
29
+ @spotify.resolvable_uri?('http://open.spotify.com/track/0qgiFuYhYuwtFXEwYakddE').should be_true
30
+ end
31
+
32
+ it "accepts Spotify URIs" do
33
+ @spotify.resolvable_uri?('spotify:track:0qgiFuYhYuwtFXEwYakddE').should be_true
34
+ end
35
+
36
+ it "refuses other URIs" do
37
+ @spotify.resolvable_uri?('123').should be_false
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,24 @@
1
+ require 'spotify_to_mp3'
2
+
3
+ module SpotifyToMp3
4
+ describe TrackIdResolver do
5
+ before(:each) do
6
+ @resolver = DependencyInjection.new.track_id_resolver
7
+ end
8
+
9
+ it "resolves Spotify HTTP URLs" do
10
+ track = @resolver.resolve('http://open.spotify.com/track/0qgiFuYhYuwtFXEwYakddE')
11
+ track.to_s.should == "Cake - I Will Survive"
12
+ end
13
+
14
+ it "resolves Spotify URIs" do
15
+ track = @resolver.resolve('spotify:track:0qgiFuYhYuwtFXEwYakddE')
16
+ track.to_s.should == "Cake - I Will Survive"
17
+ end
18
+
19
+ it "resolves track titles" do
20
+ track = @resolver.resolve('Cake - I Will Survive')
21
+ track.to_s.should == "Cake - I Will Survive"
22
+ end
23
+ end
24
+ end
@@ -1,17 +1,17 @@
1
- Gem::Specification.new do |s|
2
- s.name = 'spotify-to-mp3'
3
- s.summary = 'Spotify to MP3'
4
- s.description = 'Download MP3 files of Spotify tracks'
5
- s.version = '0.4.2'
6
- s.author = 'Francesc Rosàs'
7
- s.email = 'francescrosasbosque@gmail.com'
8
- s.homepage = 'https://github.com/frosas/spotify-to-mp3'
1
+ Gem::Specification.new do |gem|
2
+ gem.name = 'spotify-to-mp3'
3
+ gem.summary = 'Spotify to MP3'
4
+ gem.description = 'Download Spotify tracks as MP3 files from Grooveshark'
5
+ gem.version = '0.5'
6
+ gem.author = 'Francesc Rosàs'
7
+ gem.email = 'francescrosasbosque@gmail.com'
8
+ gem.homepage = 'https://github.com/frosas/spotify-to-mp3'
9
9
 
10
- s.add_dependency 'grooveshark'
11
- s.add_dependency 'colorize'
12
- s.add_dependency 'rest-client'
10
+ gem.add_dependency 'grooveshark'
11
+ gem.add_dependency 'colorize'
12
+ gem.add_dependency 'rest-client'
13
13
 
14
- s.files = `git ls-files`.split("\n")
15
- s.executables = `git ls-files -- bin/*`.split("\n").map{|f| File.basename(f)}
16
- s.require_paths = ['.']
14
+ gem.files = `git ls-files`.split("\n")
15
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
16
+ gem.require_paths = ['lib']
17
17
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spotify-to-mp3
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: '0.5'
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-08-19 00:00:00.000000000 Z
12
+ date: 2012-08-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: grooveshark
@@ -59,28 +59,39 @@ dependencies:
59
59
  - - ! '>='
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0'
62
- description: Download MP3 files of Spotify tracks
62
+ description: Download Spotify tracks as MP3 files from Grooveshark
63
63
  email: francescrosasbosque@gmail.com
64
64
  executables:
65
65
  - spotify-to-mp3
66
66
  extensions: []
67
67
  extra_rdoc_files: []
68
68
  files:
69
+ - .rspec
69
70
  - Gemfile
70
71
  - Gemfile.lock
71
72
  - README.md
72
73
  - bin/spotify-to-mp3
73
74
  - 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
75
+ - lib/spotify_to_mp3/app.rb
76
+ - lib/spotify_to_mp3/app/file_track_ids.rb
77
+ - lib/spotify_to_mp3/dependency_injection.rb
78
+ - lib/spotify_to_mp3/grooveshark.rb
79
+ - lib/spotify_to_mp3/grooveshark/track.rb
80
+ - lib/spotify_to_mp3/spotify.rb
81
+ - lib/spotify_to_mp3/spotify/track.rb
82
+ - lib/spotify_to_mp3/track.rb
83
+ - lib/spotify_to_mp3/track_id_resolver.rb
84
+ - spec/app/file_track_ids_spec.rb
85
+ - spec/grooveshark_spec.rb
86
+ - spec/spotify_spec.rb
87
+ - spec/track_id_resolver_spec.rb
77
88
  - spotify-to-mp3.gemspec
78
89
  homepage: https://github.com/frosas/spotify-to-mp3
79
90
  licenses: []
80
91
  post_install_message:
81
92
  rdoc_options: []
82
93
  require_paths:
83
- - .
94
+ - lib
84
95
  required_ruby_version: !ruby/object:Gem::Requirement
85
96
  none: false
86
97
  requirements:
@@ -1,40 +0,0 @@
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
@@ -1,65 +0,0 @@
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
@@ -1,43 +0,0 @@
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