Created
November 13, 2012 16:49
-
-
Save sagacity/4066915 to your computer and use it in GitHub Desktop.
M3U to Spotify tracks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<packages> | |
<package id="Newtonsoft.Json" version="4.5.10" targetFramework="net45" /> | |
<package id="taglib" version="2.1.0.0" targetFramework="net45" /> | |
</packages> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Newtonsoft.Json; | |
using System; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Linq; | |
using System.Net; | |
using System.Text; | |
using System.Threading.Tasks; | |
using System.Web; | |
namespace SpotifyImporter | |
{ | |
public class SongInfo | |
{ | |
public string Artist { get; set; } | |
public string Title { get; set; } | |
public double Duration { get; set; } | |
public override string ToString() | |
{ | |
return Artist + " - " + Title; | |
} | |
} | |
class Program | |
{ | |
public static List<string> invalid = new List<string>(); | |
public static HashSet<SongInfo> songs = new HashSet<SongInfo>(); | |
static void Main(string[] args) | |
{ | |
var lines = File.ReadAllLines("playlist.m3u"); | |
foreach (var line in lines) | |
{ | |
if (line.StartsWith("#")) continue; | |
if (String.IsNullOrWhiteSpace(line)) continue; | |
if (File.Exists(line)) | |
{ | |
using (var file = TagLib.File.Create(line)) | |
{ | |
var filename = Path.GetFileNameWithoutExtension(line); | |
var split = filename.Split('-'); | |
var title = file.Tag.Title; | |
if (String.IsNullOrWhiteSpace(title)) | |
{ | |
title = null; | |
if (split.Length >= 2) title = split[1].Trim(); | |
if (String.IsNullOrWhiteSpace(title)) | |
{ | |
invalid.Add(filename); | |
continue; | |
} | |
} | |
var artist = file.Tag.Performers.FirstOrDefault(); | |
if (String.IsNullOrWhiteSpace(artist)) | |
{ | |
artist = file.Tag.AlbumArtists.FirstOrDefault(); | |
if (String.IsNullOrWhiteSpace(artist)) | |
{ | |
artist = split[0].Trim(); | |
if (String.IsNullOrWhiteSpace(artist)) | |
{ | |
invalid.Add(filename); | |
continue; | |
} | |
} | |
} | |
var song = new SongInfo | |
{ | |
Artist = artist, | |
Title = title, | |
Duration = file.Properties.Duration.TotalSeconds | |
}; | |
Console.WriteLine(song); | |
songs.Add(song); | |
} | |
} | |
} | |
PrintInvalid("Could not determine metadata for:"); | |
Console.WriteLine(); | |
invalid.Clear(); | |
var output = new List<string>(); | |
foreach (var song in songs) | |
{ | |
var response = Get<Response>("search", "track", song.Artist + " " + song.Title); | |
var orderedTracks = response.Tracks.OrderBy(x => LevenshteinDistance.Compute(x.Artists.First().Name, song.Artist)).ThenBy(x => Math.Abs(x.Length - song.Duration)); | |
var bestMatch = orderedTracks.FirstOrDefault(); | |
if (bestMatch == null) | |
{ | |
invalid.Add(song.ToString()); | |
continue; | |
} | |
output.Add(bestMatch.Href); | |
System.Threading.Thread.Sleep(100); // limit API rate | |
} | |
PrintInvalid("Could not find tracks for:"); | |
File.WriteAllLines("notfound.txt", invalid); | |
Console.WriteLine("Done! Generated output.txt"); | |
File.WriteAllLines("output.txt", output); | |
Console.ReadLine(); | |
} | |
public static void PrintInvalid(string msg) | |
{ | |
if (invalid.Any()) | |
{ | |
Console.WriteLine(); | |
Console.WriteLine(msg); | |
foreach (var file in invalid) | |
{ | |
Console.WriteLine("- " + file); | |
} | |
} | |
} | |
public class Response | |
{ | |
public class ResponseInfo | |
{ | |
[JsonProperty("num_results")] | |
public int NumResults { get; set; } | |
public int Limit { get; set; } | |
public int Offset { get; set; } | |
public int Page { get; set; } | |
} | |
public class ArtistInfo | |
{ | |
public string Name { get; set; } | |
} | |
public class TrackInfo | |
{ | |
public string Name { get; set; } | |
public string Href { get; set; } | |
public double Length { get; set; } | |
public IEnumerable<ArtistInfo> Artists { get; set; } | |
} | |
public ResponseInfo Info { get; set; } | |
public IEnumerable<TrackInfo> Tracks { get; set; } | |
} | |
static T Get<T>(string service, string method, string parameters = null) where T : class | |
{ | |
var url = String.Format("http://ws.spotify.com/{0}/1/{1}", service, method); | |
if (!String.IsNullOrWhiteSpace(parameters)) | |
{ | |
url += "?q=" + HttpUtility.UrlEncode(parameters); | |
} | |
string text = null; | |
bool done = false; | |
while (!done) | |
{ | |
try | |
{ | |
Console.WriteLine("Requesting: " + url); | |
var request = (HttpWebRequest)HttpWebRequest.Create(url); | |
request.Accept = "application/json"; | |
var response = (HttpWebResponse)request.GetResponse(); | |
using (var sr = new StreamReader(response.GetResponseStream())) | |
{ | |
text = sr.ReadToEnd(); | |
} | |
done = true; | |
} | |
catch (WebException ex) | |
{ | |
Console.WriteLine("Exception: " + ex.Message); | |
Console.WriteLine("Retrying in 1 second..."); | |
System.Threading.Thread.Sleep(1000); | |
} | |
} | |
if (String.IsNullOrEmpty(text)) throw new InvalidOperationException(); | |
return JsonConvert.DeserializeObject<T>(text); | |
} | |
// http://www.dotnetperls.com/levenshtein | |
static class LevenshteinDistance | |
{ | |
/// <summary> | |
/// Compute the distance between two strings. | |
/// </summary> | |
public static int Compute(string s, string t) | |
{ | |
int n = s.Length; | |
int m = t.Length; | |
int[,] d = new int[n + 1, m + 1]; | |
// Step 1 | |
if (n == 0) | |
{ | |
return m; | |
} | |
if (m == 0) | |
{ | |
return n; | |
} | |
// Step 2 | |
for (int i = 0; i <= n; d[i, 0] = i++) | |
{ | |
} | |
for (int j = 0; j <= m; d[0, j] = j++) | |
{ | |
} | |
// Step 3 | |
for (int i = 1; i <= n; i++) | |
{ | |
//Step 4 | |
for (int j = 1; j <= m; j++) | |
{ | |
// Step 5 | |
int cost = (t[j - 1] == s[i - 1]) ? 0 : 1; | |
// Step 6 | |
d[i, j] = Math.Min( | |
Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), | |
d[i - 1, j - 1] + cost); | |
} | |
} | |
// Step 7 | |
return d[n, m]; | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment