spotify-to-mp3 0.4.2 → 0.5

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/.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