Skip to content

Instantly share code, notes, and snippets.

@cheshire137
Last active June 29, 2020 08:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save cheshire137/69844669fe791beed2fa to your computer and use it in GitHub Desktop.
Save cheshire137/69844669fe791beed2fa to your computer and use it in GitHub Desktop.
Ruby script to create iTunes playlists from your Spotify playlists. Requires a Spotify API app.
#!/usr/bin/env ruby
require 'uri'
require 'json'
require 'net/https'
require 'time'
require 'cgi'
require 'csv'
# You need a Spotify API app to have a client ID and client secret. Create
# one at https://developer.spotify.com/my-applications/#!/applications/create
# For a redirect URI, you can use your Github profile URL, e.g.,
# https://github.com/moneypenny
class WebApi
def get uri, headers={}
puts "GET #{uri}"
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
request = Net::HTTP::Get.new(uri.request_uri, headers)
http.request(request)
end
def post uri, body={}, headers={}
puts "POST #{uri}"
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
request = Net::HTTP::Post.new(uri.request_uri, headers)
request.set_form_data(body)
http.request(request)
end
end
class SpotifyApi < WebApi
attr_reader :client_id, :client_secret, :redirect_uri, :code, :token,
:user_id, :playlist_id
def initialize attrs={}
attrs.each do |key, value|
instance_variable_set "@#{key}", value
end
end
def auth_url
"https://accounts.spotify.com/authorize?client_id=#@client_id" +
"&response_type=code&redirect_uri=#@redirect_uri" +
'&scope=playlist-read-private'
end
def get_user_id
return @user_id if @user_id
me_uri = URI.parse('https://api.spotify.com/v1/me')
response = get(me_uri, auth_headers)
unless response.is_a? Net::HTTPOK
puts "Failed to get Spotify user info: #{response.class.name}\n" +
"#{response.body}"
exit
end
json = JSON.parse(response.body)
@user_id = json['id']
end
def get_playlists
# TODO: get multiple pages of playlists
playlist_uri = URI.parse('https://api.spotify.com/v1/users/' +
"#@user_id/playlists?limit=50")
response = get(playlist_uri, auth_headers)
unless response.is_a? Net::HTTPOK
puts "Failed to get Spotify playlists: #{response.class.name}\n" +
"#{response.body}"
exit
end
json = JSON.parse(response.body)
json['items']
end
def get_tracks
# TODO: get multiple pages of tracks in a playlist
fields = 'items(track(name,album(name),artists(name)))'
tracks_uri = URI.parse("https://api.spotify.com/v1/users/#@user_id/" +
"playlists/#@playlist_id/tracks?fields=#{fields}")
response = get(tracks_uri, auth_headers)
unless response.is_a? Net::HTTPOK
puts "Failed to get Spotify playlist tracks: #{response.class.name}\n" +
"#{response.body}"
exit
end
json = JSON.parse(response.body)
json['items']
end
def get_token
token_uri = URI.parse('https://accounts.spotify.com/api/token')
body = {'grant_type' => 'authorization_code', 'code' => @code,
'redirect_uri' => @redirect_uri, 'client_id' => @client_id,
'client_secret' => @client_secret}
response = post(token_uri, body)
unless response.is_a? Net::HTTPOK
puts "Failed to authenticate with Spotify: #{response.class.name}\n" +
"#{response.body}"
exit
end
json = JSON.parse(response.body)
@token = json['access_token']
end
private
def auth_headers
{'Authorization' => "Bearer #@token"}
end
end
class ItunesApi < WebApi
attr_reader :artists, :track, :album
def initialize attrs={}
attrs.each do |key, value|
instance_variable_set "@#{key}", value
end
@track = clean_str(@track) if @track
@artists = @artists.map {|a| clean_str(a) } if @artists
@album = clean_str(@album, true) if @album
end
def get_track
artists_query = CGI.escape(@artists.join(' '))
track_query = CGI.escape(@track)
album_query = CGI.escape(@album)
query = [artists_query, track_query, album_query].
reject {|str| str.strip.length < 1 }.join('+')
uri = URI.parse("https://itunes.apple.com/search?term=#{query}" +
'&entity=musicTrack&media=music&limit=1')
response = get(uri)
unless response.is_a? Net::HTTPOK
puts "Failed to find #@track by #{@artists.join(', ')} on album " +
"#@album: #{response.class.name}\n#{response.body}"
return false
end
json = JSON.parse(response.body)
result_count = json['resultCount'].to_i
return false if result_count < 1
json['results'][0]
end
private
def clean_str str, strip_parens=false
# 'Big Boi Presents... Got Purp? Vol. 2' =>
# 'Big Boi Presents Got Purp Vol. 2'
str = str.gsub(/\.\.\./, '').gsub(/\?/, '')
feat_index = str.downcase.index('feat.')
if feat_index
# 'Kryptonite - feat. Big Boi' => 'Kryptonite - '
str = str[0...feat_index]
end
hyphen_index = str.index(' - ')
if hyphen_index
# 'Kryptonite - ' => 'Kryptonite'
str = str[0...hyphen_index]
end
ellipsis_index = str.index('...')
if strip_parens
open_index = str.index('(')
if open_index
close_index = str.index(')', open_index)
if close_index
# 'Take Care (Explicit Deluxee)' => 'Take Care '
str = str[0...open_index] + str[close_index+1...str.length]
end
end
end
# 'Take Care ' => 'Take Care'
str.strip
end
end
client_id = ENV['CLIENT_ID']
client_secret = ENV['CLIENT_SECRET']
redirect_uri = ENV['REDIRECT_URI']
code = ENV['CODE']
token = ENV['TOKEN']
playlist_id = ENV['PLAYLIST_ID']
user_id = ENV['USER_ID']
api = SpotifyApi.new(client_id: client_id, client_secret: client_secret,
redirect_uri: redirect_uri, code: code, token: token,
playlist_id: playlist_id, user_id: user_id)
if api.token && api.playlist_id && api.user_id
puts '# Step 4'
spotify_tracks = api.get_tracks
itunes_tracks = []
spotify_tracks.each do |json|
spotify_track = json['track']
artist_names = spotify_track['artists'].map {|artist| artist['name'] }
album_name = spotify_track['album']['name']
track_name = spotify_track['name']
itunes_api = ItunesApi.new(artists: artist_names, track: track_name,
album: album_name)
itunes_track = itunes_api.get_track
if itunes_track
puts "\tFound on iTunes: #{itunes_track['trackName']}\t\t" +
"#{itunes_track['artistName']}\t\t#{itunes_track['collectionName']}"
itunes_tracks << itunes_track
else
puts "\tNo match found on iTunes"
end
end
itunes_playlist_file = "itunes-playlist-#{api.playlist_id}.txt"
CSV.open(itunes_playlist_file, 'wb', col_sep: "\t") do |csv|
csv << ['Name', 'Artist', 'Composer', 'Album', 'Grouping', 'Genre', 'Size',
'Time', 'Disc Number', 'Disc Count', 'Track Number', 'Track Count',
'Year', 'Date Modified', 'Date Added', 'Bit Rate', 'Sample Rate',
'Volume Adjustment', 'Kind', 'Equalizer', 'Comments', 'Plays',
'Last Played', 'Skips', 'Last Skipped', 'My Rating', 'Location']
itunes_tracks.each do |itunes_track|
name = itunes_track['trackName']
artist = itunes_track['artistName']
album = itunes_track['collectionCensoredName'] # seems to work best
genre = nil#itunes_track['primaryGenreName']
time = 255 # denotes streaming file
disc_number = nil#(itunes_track['discNumber'] || '').to_s
disc_count = nil#(itunes_track['discCount'] || '').to_s
track_number = nil#(itunes_track['trackNumber'] || '').to_s
track_count = nil#(itunes_track['trackCount'] || '').to_s
year = nil
kind = 'AAC audio file'
begin
release_date = DateTime.parse(itunes_track['releaseDate'])
year = release_date.year
rescue ArgumentError
end
csv << [name, artist, nil, album, nil, genre, nil, time, disc_number,
disc_count, track_number, track_count, year, nil, nil, nil, nil,
nil, kind, nil, nil, nil, nil, nil, nil, nil, nil]
end
end
File.open(itunes_playlist_file, 'ab') {|f| f.puts '' } # end with new line
puts "Wrote iTunes playlist to file: #{itunes_playlist_file}"
elsif api.token
user_id = api.get_user_id
puts '# Step 3'
puts "Your Spotify user ID: #{user_id}"
playlists = api.get_playlists
puts 'Your Spotify playlists (ID then name):'
playlists.each do |json|
puts "#{json['id']}\t#{json['name']}"
end
puts "\nRun this script again with TOKEN, USER_ID, and PLAYLIST_ID " +
"\nenvironment variables to get an iTunes playlist for the specified " +
"\nSpotify playlist."
elsif api.code && api.client_secret && api.client_id && api.redirect_uri
token = api.get_token
puts '# Step 2'
puts "Spotify access token: #{token}"
puts "\nRun this script again with TOKEN environment variable."
elsif api.client_id && api.redirect_uri
puts '# Step 1'
puts 'Go to this URL, copy the code parameter after you are redirected:'
puts "\t#{api.auth_url}"
puts "\nRun this script again with CODE, CLIENT_ID, CLIENT_SECRET, and\n" +
'REDIRECT_URI environment variables.'
else
puts 'Example use:'
puts 'CLIENT_ID=yourSpotifyClientId REDIRECT_URI=yourSpotifyRedirectUri ' +
__FILE__
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment