Repository: jlund/spotify-export Branch: master Commit: 210183c5103c Files: 20 Total size: 12.5 KB Directory structure: gitextract_gy971w70/ ├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── bin/ │ └── spotify-export.rb ├── lib/ │ ├── spotify-cache.rb │ ├── spotify-playlist.rb │ └── spotify-track.rb └── spec/ ├── spec_helper.rb ├── spotify-playlist_spec.rb ├── spotify-track_spec.rb └── support/ ├── by-track.txt ├── html-entities-track.txt ├── local-track.txt ├── multiple-artist-track.txt ├── multiple-tracks.txt ├── single-track.txt ├── song-link-track.txt └── utf8-track.txt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.gem *.rbc .bundle .config InstalledFiles coverage lib/bundler/man pkg playlists/ rdoc spec/reports spotify-cache.db test/tmp test/version_tmp tmp # YARD artifacts .yardoc _yardoc doc/ ================================================ FILE: CHANGELOG.md ================================================ #### 1.5 * Added support for playlists comprised of Song Links in addition to Spotify URIs in order to solve a common mistake. * Updated gem versions. * Retroactively added a 1.4 CHANGELOG entry. #### 1.4 * Updated to the new Spotify API and response format (contributed by Stephen Fuller). * Refactored tests for the latest version of RSpec and fixed a few that were broken. * Deprecated support for Ruby 1.9.3. * README cleanup and reformatting. * Updated gem versions. #### 1.3 * A lovely progress bar is now included. * Spotify API errors are handled more gracefully: * "502 Bad Gateway" responses will no longer cause the program to crash. * Failed requests are automatically retried after five seconds. #### 1.2 * Added support for playlists that contain Local Files (contributed by Christopher Nguyen). * Changed the way that the SQLite database is handled: * Your personal track cache is now ignored by git. * For new users, the repository contains a database template file that spotify-export will copy to the proper location if it doesn't already exist. * **Current users who would like to merge these changes will need to temporarily move their SQLite cache file elsewhere, checkout the original version, pull the latest commits, and then move their database back.** Git will otherwise complain about merge conflicts. I sincerely apologize for the hassle; I really should have done it this way from the very beginning (thanks to Christopher Nguyen for this insight as well). #### 1.1 * Requests are now made using the [Spotify Web API](http://developer.spotify.com/technologies/web-api/): * Spotify URIs have replaced HTTP Links. **Please see the updated instructions.** * This also, unfortunately, will invalidate your existing cache. But it's worth it! * Bandwidth usage has been drastically reduced: * The JSON response for one of the test tracks is only 486 bytes, compared to 19,753 bytes for the HTML version of the same track. * The 404 issue (where some tracks were coming back blank) is resolved! * The JSON API does not return HTML entities, so that gem dependency has been removed. * Tracks with multiple artists are now exported properly. Rap fans, I got you. #### 1.0.1 * Fixed a bug where track names containing the word 'by' were not properly exported. #### 1.0 * Complete rewrite. * Added Gemfile for Bundler support. * Added RSpec tests. * Implemented caching of track information using SQLite for dramatically better performance. **Now you can regularly back up your constantly-growing Starred playlist, even if it contains hundreds and hundreds of tracks!** #### 0.2 * Added support for album names. #### 0.1 * Initial release. ================================================ FILE: Gemfile ================================================ source "https://rubygems.org" gem 'activerecord' gem 'rspec' gem 'ruby-progressbar' gem 'sqlite3' ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2012-2013 Joshua Lund Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ spotify-export ============== Description ----------- Let's convert a Spotify playlist into plain text! 1. Open Spotify and go to the playlist that you want to export. 2. Select the tracks that you want to export (Ctrl-A or Cmd-A to Select All). 3. Right-click on the selected tracks and choose "Copy Spotify URI" from the menu. 4. Go to the text editor of your choice and Paste. 5. Save the file. 6. Run `./bin/spotify-export.rb your-filename.txt`. Running the command on the included `spec/support/multiple-tracks.txt` test file will produce the following output: 1. Illusions -- Shout Out Louds -- Optica (Bonus Track Version) 2. My Number -- Foals -- Holy Fire 3. Love to Get Used -- Matt Pond -- The Lives Inside The Lines In Your Hand 4. Clouds -- Rangleklods -- Beekeeper (incl. Home EP) 5. Kelly -- When Saints Go Machine -- Konkylie Listening to the songs might be fun too. Enjoy! Features -------- * Lookups are performed using the super-efficient [Spotify Web API](https://developer.spotify.com/web-api/). * SQLite is used as a caching layer so that information about each track will only be requested once, which allows you to regularly back up large playlists. Requirements ------------ * [Ruby](http://www.ruby-lang.org/en/) 2.1 or higher * [Bundler](http://gembundler.com/) * [ActiveRecord](https://github.com/rails/rails/tree/master/activerecord) * [RSpec](http://rspec.info/) * [Ruby/ProgressBar](https://github.com/jfelchner/ruby-progressbar) * [SQLite3](https://github.com/luislavena/sqlite3-ruby) and a working [SQLite](http://www.sqlite.org/) binary Setup ----- * `bundle install` Acknowledgments --------------- This product uses a SPOTIFY API but is not endorsed, certified or otherwise approved in any way by Spotify. Spotify is the registered trade mark of the Spotify Group. ================================================ FILE: bin/spotify-export.rb ================================================ #!/usr/bin/env ruby require 'bundler/setup' require 'fileutils' require 'ruby-progressbar' require_relative '../lib/spotify-playlist' # Copy the template SQLite file for new users, unless it # already exists unless File.exist?("#{ ROOT }/db/spotify-cache.db") FileUtils.cp("#{ ROOT }/db/spotify-cache-template.db", "#{ ROOT }/db/spotify-cache.db") end output = String.new playlist = SpotifyPlaylist.new(ARGV.first) progressbar = ProgressBar.create(format: "%t: %c/%C |%B|", total: playlist.tracks.size) playlist.tracks.each_with_index do |track, count| # Sanity check unless track.nil? output << "#{count + 1}. #{ track.name } -- #{ track.artist } -- #{ track.album }\n" progressbar.increment end end puts output ================================================ FILE: lib/spotify-cache.rb ================================================ require 'active_record' # So excited for __dir__ in Ruby 2.0! ROOT = "#{ File.dirname(File.expand_path(__FILE__)) }/.." class SpotifyCache < ActiveRecord::Base establish_connection( adapter: "sqlite3", database: "#{ ROOT }/db/spotify-cache.db" ) end ================================================ FILE: lib/spotify-playlist.rb ================================================ require_relative 'spotify-track' class SpotifyPlaylist class MissingFileError < StandardError; end attr_reader :filename def initialize(filename=nil) raise MissingFileError, "Usage: spotify-export.rb " unless filename raise MissingFileError, "That file does not exist" unless File.exist?(filename) @filename = filename end def export_targets @export_targets ||= File.read(filename).split end def tracks export_targets.map{ |track_uri| SpotifyTrack.new(track_uri) } end end ================================================ FILE: lib/spotify-track.rb ================================================ require 'net/https' require 'json' require_relative 'spotify-cache' class SpotifyTrack attr_reader :local, :uri def initialize(uri) @local = uri.include? ':local:' @uri = uri end def album attributes[:album] end def artist attributes[:artist] end def name attributes[:name] end private def attributes @attributes ||= begin cache = SpotifyCache.where(uri: uri).first if cache.blank? get_track_attributes else { name: cache[:name], artist: cache[:artist], album: cache[:album] } end end end def cache_track(cache_name, cache_artist, cache_album) SpotifyCache.create(uri: uri, name: cache_name, artist: cache_artist, album: cache_album) end def format_artists(artists) artist_list = [] artists.each do |artist| artist_list << artist["name"] end artist_list.join(", ") end def get_track_attributes if local local_array = uri.split(':') # The array should be length 6 # ["spotify", "local", "artist", "album", "song title", "duration"] name = URI.decode(local_array[4].gsub('+', ' ')) album = URI.decode(local_array[3].gsub('+', ' ')) artist = URI.decode(local_array[2].gsub('+', ' ')) else track_id = uri[/track(:|\/)(.+)/, 2] target = URI.parse("https://api.spotify.com/v1/tracks/#{ track_id }") http = Net::HTTP.new(target.host, target.port) http.use_ssl = true request = Net::HTTP::Get.new(target.request_uri) begin response = http.request(request) json = JSON.parse(response.body) rescue Errno::ECONNREFUSED, JSON::ParserError puts "Spotify API error. Retrying in five seconds..." sleep 5 retry end name = json["name"] artist = format_artists( json["artists"] ) album = json["album"]["name"] cache_track(name, artist, album) if response.code == "200" end { name: name, artist: artist, album: album } end end ================================================ FILE: spec/spec_helper.rb ================================================ require 'rspec' require_relative '../lib/spotify-playlist' ================================================ FILE: spec/spotify-playlist_spec.rb ================================================ require 'spec_helper' describe SpotifyPlaylist do let(:filename) { 'spec/support/single-track.txt' } let(:playlist) { SpotifyPlaylist.new(filename) } describe ".new" do context "if no filename is provided" do it "raises a MissingFileError" do expect { SpotifyPlaylist.new }.to raise_error(SpotifyPlaylist::MissingFileError) end end context "if the specified file does not exist" do it "raises a MissingFileError" do expect { SpotifyPlaylist.new('spec/support/asdf.txt') }.to raise_error(SpotifyPlaylist::MissingFileError) end end end describe "#tracks" do it "returns an array" do expect(playlist.tracks.class).to equal Array end it "contains SpotifyTrack objects" do expect(playlist.tracks.first.class).to equal SpotifyTrack end end end ================================================ FILE: spec/spotify-track_spec.rb ================================================ # encoding: utf-8 require 'spec_helper' describe SpotifyTrack do let(:filename) { 'spec/support/single-track.txt' } let(:playlist) { SpotifyPlaylist.new(filename) } let(:track) { playlist.tracks.first } describe "#name" do it "should return the track name" do expect(track.name).to eq("Fineshrine") end it "properly handles UTF-8 characters" do track = SpotifyPlaylist.new('spec/support/utf8-track.txt').tracks.first expect(track.name).to eq("Gangnam Style (강남스타일)") end it "properly handles local files" do track = SpotifyPlaylist.new('spec/support/local-track.txt').tracks.first expect(track.name).to eq("Life On Mars?") end it "properly handles Song Link URLs (e.g. non-Spotify-URI tracks)" do track = SpotifyPlaylist.new('spec/support/song-link-track.txt').tracks.first expect(track.name).to eq("8 (circle)") end it "properly retrieves track names that contain the word 'by'" do track = SpotifyPlaylist.new('spec/support/by-track.txt').tracks.first expect(track.name).to eq("Play by Play") end end describe "#artist" do it "should return the track artist" do expect(track.artist).to eq("Purity Ring") end it "properly handles HTML entities" do track = SpotifyPlaylist.new('spec/support/html-entities-track.txt').tracks.first expect(track.artist).to eq("Niki & The Dove") end it "should return all track artists if there are several" do track = SpotifyPlaylist.new('spec/support/multiple-artist-track.txt').tracks.first expect(track.artist).to eq("Big Boi, B.o.B, Wavves") end end describe "#album" do it "should return the track album" do expect(track.album).to eq("Shrines") end end end ================================================ FILE: spec/support/by-track.txt ================================================ spotify:track:59ZIrPBaCOmLrweJOGHZOD ================================================ FILE: spec/support/html-entities-track.txt ================================================ spotify:track:3XJrdK0Okwfi7waIcUQkca ================================================ FILE: spec/support/local-track.txt ================================================ spotify:local:Seu+Jorge:Life+Aquatic+Studio+Sessions:Life+On+Mars%3f:209 ================================================ FILE: spec/support/multiple-artist-track.txt ================================================ spotify:track:225gqtTcsE8cfFtfKNTVjt ================================================ FILE: spec/support/multiple-tracks.txt ================================================ spotify:track:2IWrMpJi3Om3MexLwcNUXf spotify:track:4c9WmjVlQMr0s1IjbYO52Z spotify:track:7rpBFDqUmFrMhiToJKORnY spotify:track:3bzYcthGKdW0gTL3N2YPBF spotify:track:1LZGkZfx4uAFeFZQ4uADc3 ================================================ FILE: spec/support/single-track.txt ================================================ spotify:track:5KeyVNymqfqac1wLDseK8v ================================================ FILE: spec/support/song-link-track.txt ================================================ https://open.spotify.com/track/47IklCMgkgWvI4jpkdrop0 ================================================ FILE: spec/support/utf8-track.txt ================================================ spotify:track:03UrZgTINDqvnUMbbIMhql