[
  {
    "path": ".gitignore",
    "content": "*.gem\n*.rbc\n.bundle\n.config\nInstalledFiles\ncoverage\nlib/bundler/man\npkg\nplaylists/\nrdoc\nspec/reports\nspotify-cache.db\ntest/tmp\ntest/version_tmp\ntmp\n\n# YARD artifacts\n.yardoc\n_yardoc\ndoc/\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "#### 1.5\n* Added support for playlists comprised of Song Links in addition to Spotify URIs in order to solve a common mistake.\n* Updated gem versions.\n* Retroactively added a 1.4 CHANGELOG entry.\n\n#### 1.4\n* Updated to the new Spotify API and response format (contributed by Stephen Fuller).\n* Refactored tests for the latest version of RSpec and fixed a few that were broken.\n* Deprecated support for Ruby 1.9.3.\n* README cleanup and reformatting.\n* Updated gem versions.\n\n#### 1.3\n* A lovely progress bar is now included.\n* Spotify API errors are handled more gracefully:\n  * \"502 Bad Gateway\" responses will no longer cause the program to crash.\n  * Failed requests are automatically retried after five seconds.\n\n#### 1.2\n* Added support for playlists that contain Local Files (contributed by Christopher Nguyen).\n* Changed the way that the SQLite database is handled:\n  * Your personal track cache is now ignored by git.\n  * 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.\n  * **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).\n\n#### 1.1\n* Requests are now made using the [Spotify Web API](http://developer.spotify.com/technologies/web-api/):\n  * Spotify URIs have replaced HTTP Links. **Please see the updated instructions.**\n  * This also, unfortunately, will invalidate your existing cache. But it's worth it!\n* Bandwidth usage has been drastically reduced:\n  * 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.\n* The 404 issue (where some tracks were coming back blank) is resolved!\n* The JSON API does not return HTML entities, so that gem dependency has been removed.\n* Tracks with multiple artists are now exported properly. Rap fans, I got you.\n\n#### 1.0.1\n* Fixed a bug where track names containing the word 'by' were not properly exported.\n\n#### 1.0\n* Complete rewrite.\n* Added Gemfile for Bundler support.\n* Added RSpec tests.\n* 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!**\n\n#### 0.2\n* Added support for album names.\n\n#### 0.1\n* Initial release.\n"
  },
  {
    "path": "Gemfile",
    "content": "source \"https://rubygems.org\"\n\ngem 'activerecord'\ngem 'rspec'\ngem 'ruby-progressbar'\ngem 'sqlite3'\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2012-2013 Joshua Lund\n\nPermission 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:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE 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.\n"
  },
  {
    "path": "README.md",
    "content": "spotify-export\n==============\n\nDescription\n-----------\nLet's convert a Spotify playlist into plain text!\n\n1. Open Spotify and go to the playlist that you want to export.\n2. Select the tracks that you want to export (Ctrl-A or Cmd-A to Select All).\n3. Right-click on the selected tracks and choose \"Copy Spotify URI\" from the menu.\n4. Go to the text editor of your choice and Paste.\n5. Save the file.\n6. Run `./bin/spotify-export.rb your-filename.txt`.\n\nRunning the command on the included `spec/support/multiple-tracks.txt` test file will produce the following output:\n\n    1. Illusions -- Shout Out Louds -- Optica (Bonus Track Version)\n    2. My Number -- Foals -- Holy Fire\n    3. Love to Get Used -- Matt Pond -- The Lives Inside The Lines In Your Hand\n    4. Clouds -- Rangleklods -- Beekeeper (incl. Home EP)\n    5. Kelly -- When Saints Go Machine -- Konkylie\n\nListening to the songs might be fun too.\n\nEnjoy!\n\n\nFeatures\n--------\n* Lookups are performed using the super-efficient [Spotify Web API](https://developer.spotify.com/web-api/).\n* 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.\n\n\nRequirements\n------------\n* [Ruby](http://www.ruby-lang.org/en/) 2.1 or higher\n* [Bundler](http://gembundler.com/)\n* [ActiveRecord](https://github.com/rails/rails/tree/master/activerecord)\n* [RSpec](http://rspec.info/)\n* [Ruby/ProgressBar](https://github.com/jfelchner/ruby-progressbar)\n* [SQLite3](https://github.com/luislavena/sqlite3-ruby) and a working [SQLite](http://www.sqlite.org/) binary\n\n\nSetup\n-----\n* `bundle install`\n\n\nAcknowledgments\n---------------\nThis 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.\n"
  },
  {
    "path": "bin/spotify-export.rb",
    "content": "#!/usr/bin/env ruby\n\nrequire 'bundler/setup'\nrequire 'fileutils'\nrequire 'ruby-progressbar'\nrequire_relative '../lib/spotify-playlist'\n\n# Copy the template SQLite file for new users, unless it\n# already exists\nunless File.exist?(\"#{ ROOT }/db/spotify-cache.db\")\n  FileUtils.cp(\"#{ ROOT }/db/spotify-cache-template.db\",\n               \"#{ ROOT }/db/spotify-cache.db\")\nend\n\noutput      = String.new\nplaylist    = SpotifyPlaylist.new(ARGV.first)\nprogressbar = ProgressBar.create(format: \"%t: %c/%C |%B|\",\n                                 total: playlist.tracks.size)\n\nplaylist.tracks.each_with_index do |track, count|\n  # Sanity check\n  unless track.nil?\n    output << \"#{count + 1}. #{ track.name } -- #{ track.artist } -- #{ track.album }\\n\"\n    progressbar.increment\n  end\nend\n\nputs output\n"
  },
  {
    "path": "lib/spotify-cache.rb",
    "content": "require 'active_record'\n\n# So excited for __dir__ in Ruby 2.0!\nROOT = \"#{ File.dirname(File.expand_path(__FILE__)) }/..\"\n\nclass SpotifyCache < ActiveRecord::Base\n  establish_connection(\n    adapter: \"sqlite3\",\n    database: \"#{ ROOT }/db/spotify-cache.db\"\n  )\nend\n"
  },
  {
    "path": "lib/spotify-playlist.rb",
    "content": "require_relative 'spotify-track'\n\nclass SpotifyPlaylist\n  class MissingFileError < StandardError; end\n\n  attr_reader :filename\n\n  def initialize(filename=nil)\n    raise MissingFileError, \"Usage: spotify-export.rb <filename>\" unless filename\n    raise MissingFileError, \"That file does not exist\" unless File.exist?(filename)\n\n    @filename = filename\n  end\n\n  def export_targets\n    @export_targets ||= File.read(filename).split\n  end\n\n  def tracks\n    export_targets.map{ |track_uri| SpotifyTrack.new(track_uri) }\n  end\nend\n"
  },
  {
    "path": "lib/spotify-track.rb",
    "content": "require 'net/https'\nrequire 'json'\nrequire_relative 'spotify-cache'\n\nclass SpotifyTrack\n  attr_reader :local, :uri\n\n  def initialize(uri)\n    @local = uri.include? ':local:'\n    @uri   = uri\n  end\n\n  def album\n    attributes[:album]\n  end\n\n  def artist\n    attributes[:artist]\n  end\n\n  def name\n    attributes[:name]\n  end\n\n  private\n\n  def attributes\n    @attributes ||= begin\n      cache = SpotifyCache.where(uri: uri).first\n\n      if cache.blank?\n        get_track_attributes\n      else\n        { name: cache[:name], artist: cache[:artist], album: cache[:album] } \n      end\n    end\n  end\n\n  def cache_track(cache_name, cache_artist, cache_album)\n    SpotifyCache.create(uri: uri,\n                        name: cache_name,\n                        artist: cache_artist,\n                        album: cache_album)\n  end\n\n  def format_artists(artists)\n    artist_list = []\n\n    artists.each do |artist|\n      artist_list << artist[\"name\"]\n    end\n\n    artist_list.join(\", \")\n  end\n\n  def get_track_attributes\n    if local\n      local_array = uri.split(':')\n\n      # The array should be length 6\n      # [\"spotify\", \"local\", \"artist\", \"album\", \"song title\", \"duration\"]\n      name   = URI.decode(local_array[4].gsub('+', ' '))\n      album  = URI.decode(local_array[3].gsub('+', ' '))\n      artist = URI.decode(local_array[2].gsub('+', ' '))\n    else\n      track_id = uri[/track(:|\\/)(.+)/, 2]\n      target  = URI.parse(\"https://api.spotify.com/v1/tracks/#{ track_id }\")\n      http    = Net::HTTP.new(target.host, target.port)\n      http.use_ssl = true\n      request = Net::HTTP::Get.new(target.request_uri)\n\n      begin\n        response = http.request(request)\n        json     = JSON.parse(response.body)\n      rescue Errno::ECONNREFUSED, JSON::ParserError\n        puts \"Spotify API error. Retrying in five seconds...\"\n        sleep 5\n        retry\n      end\n\n      name   =  json[\"name\"]\n      artist =  format_artists( json[\"artists\"] )\n      album  =  json[\"album\"][\"name\"]\n      cache_track(name, artist, album) if response.code == \"200\"\n    end\n\n    { name: name, artist: artist, album: album } \n  end\n\nend\n"
  },
  {
    "path": "spec/spec_helper.rb",
    "content": "require 'rspec'\nrequire_relative '../lib/spotify-playlist'\n"
  },
  {
    "path": "spec/spotify-playlist_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe SpotifyPlaylist do\n\n  let(:filename) { 'spec/support/single-track.txt' }\n  let(:playlist) { SpotifyPlaylist.new(filename) }\n\n  describe \".new\" do\n    context \"if no filename is provided\" do\n      it \"raises a MissingFileError\" do\n        expect { SpotifyPlaylist.new }.to raise_error(SpotifyPlaylist::MissingFileError)\n      end\n    end\n\n    context \"if the specified file does not exist\" do\n      it \"raises a MissingFileError\" do\n        expect { SpotifyPlaylist.new('spec/support/asdf.txt') }.to raise_error(SpotifyPlaylist::MissingFileError)\n      end\n    end\n  end\n\n  describe \"#tracks\" do\n    it \"returns an array\" do\n      expect(playlist.tracks.class).to equal Array\n    end\n\n    it \"contains SpotifyTrack objects\" do\n      expect(playlist.tracks.first.class).to equal SpotifyTrack\n    end\n  end\n\nend\n"
  },
  {
    "path": "spec/spotify-track_spec.rb",
    "content": "# encoding: utf-8\nrequire 'spec_helper'\n\ndescribe SpotifyTrack do\n\n  let(:filename) { 'spec/support/single-track.txt' }\n  let(:playlist) { SpotifyPlaylist.new(filename) }\n  let(:track)    { playlist.tracks.first }\n\n  describe \"#name\" do\n    it \"should return the track name\" do\n      expect(track.name).to eq(\"Fineshrine\")\n    end\n\n    it \"properly handles UTF-8 characters\" do\n      track = SpotifyPlaylist.new('spec/support/utf8-track.txt').tracks.first\n      expect(track.name).to eq(\"Gangnam Style (강남스타일)\")\n    end\n\n    it \"properly handles local files\" do\n      track = SpotifyPlaylist.new('spec/support/local-track.txt').tracks.first\n      expect(track.name).to eq(\"Life On Mars?\")\n    end\n\n    it \"properly handles Song Link URLs (e.g. non-Spotify-URI tracks)\" do\n      track = SpotifyPlaylist.new('spec/support/song-link-track.txt').tracks.first\n      expect(track.name).to eq(\"8 (circle)\")\n    end\n\n    it \"properly retrieves track names that contain the word 'by'\" do\n      track = SpotifyPlaylist.new('spec/support/by-track.txt').tracks.first\n      expect(track.name).to eq(\"Play by Play\")\n    end\n  end\n\n  describe \"#artist\" do\n    it \"should return the track artist\" do\n      expect(track.artist).to eq(\"Purity Ring\")\n    end\n\n    it \"properly handles HTML entities\" do\n      track = SpotifyPlaylist.new('spec/support/html-entities-track.txt').tracks.first\n      expect(track.artist).to eq(\"Niki & The Dove\")\n    end\n\n    it \"should return all track artists if there are several\" do\n      track = SpotifyPlaylist.new('spec/support/multiple-artist-track.txt').tracks.first\n      expect(track.artist).to eq(\"Big Boi, B.o.B, Wavves\")\n    end\n  end\n\n  describe \"#album\" do\n    it \"should return the track album\" do\n      expect(track.album).to eq(\"Shrines\")\n    end\n  end\n\nend\n"
  },
  {
    "path": "spec/support/by-track.txt",
    "content": "spotify:track:59ZIrPBaCOmLrweJOGHZOD\n"
  },
  {
    "path": "spec/support/html-entities-track.txt",
    "content": "spotify:track:3XJrdK0Okwfi7waIcUQkca\n"
  },
  {
    "path": "spec/support/local-track.txt",
    "content": "spotify:local:Seu+Jorge:Life+Aquatic+Studio+Sessions:Life+On+Mars%3f:209\n"
  },
  {
    "path": "spec/support/multiple-artist-track.txt",
    "content": "spotify:track:225gqtTcsE8cfFtfKNTVjt\n"
  },
  {
    "path": "spec/support/multiple-tracks.txt",
    "content": "spotify:track:2IWrMpJi3Om3MexLwcNUXf spotify:track:4c9WmjVlQMr0s1IjbYO52Z spotify:track:7rpBFDqUmFrMhiToJKORnY spotify:track:3bzYcthGKdW0gTL3N2YPBF spotify:track:1LZGkZfx4uAFeFZQ4uADc3\n"
  },
  {
    "path": "spec/support/single-track.txt",
    "content": "spotify:track:5KeyVNymqfqac1wLDseK8v\n"
  },
  {
    "path": "spec/support/song-link-track.txt",
    "content": "https://open.spotify.com/track/47IklCMgkgWvI4jpkdrop0\n"
  },
  {
    "path": "spec/support/utf8-track.txt",
    "content": "spotify:track:03UrZgTINDqvnUMbbIMhql\n"
  }
]