Skip to content

Instantly share code, notes, and snippets.

@guymac
Last active September 9, 2024 18:14
Show Gist options
  • Save guymac/1a41a9ff0dca1ded4f42f0f33315ee8c to your computer and use it in GitHub Desktop.
Save guymac/1a41a9ff0dca1ded4f42f0f33315ee8c to your computer and use it in GitHub Desktop.
Given a Spotify playlist URL, prints out a track listing.
import java.io.*;
import java.net.*;
import java.net.http.*;
import java.nio.charset.*;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import javax.swing.text.html.parser.*;
import javax.swing.text.html.*;
import javax.swing.text.*;
/**
* The playlist contains meta tags with name attributes of "music:song", each is a URL
* to a song page where the meta og:title is the track title and music:musician_description contains
* the artist
*/
public class SpotLister
{
private Map <URI, String[]> tracks = new LinkedHashMap <> ();
static int TITLE = 0, ARTIST = 1;
static HttpClient client = HttpClient.newHttpClient();
public SpotLister(URI uri, PrintStream out)
{
try
{
var req = HttpRequest.newBuilder(uri).build();
var res = HttpResponse.BodyHandlers.ofInputStream();
delegate(client.send(req, res));
List <HttpRequest> requests = tracks.keySet().stream()
.map(HttpRequest::newBuilder)
.map(HttpRequest.Builder::build)
.toList();
CompletableFuture.allOf(requests.stream()
.map(request -> client.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())
.thenAccept(response -> tracks.replace(response.request().uri(), delegate(response))))
.toArray(CompletableFuture<?>[]::new))
.join();
}
catch (IOException ex)
{
throw new UncheckedIOException(ex);
}
catch (InterruptedException ex)
{
Thread.currentThread().interrupt();
throw new RuntimeException(ex);
}
var it = tracks.values().iterator();
for (var i = 0 ; it.hasNext() ; i++)
{
String[] track = it.next();
out.format("%d. %s / %s%n", i+1, track[TITLE], track[ARTIST]);
}
}
class Delegate extends HTMLEditorKit.ParserCallback
{
private String[] track = new String[2];
@Override
public void handleSimpleTag(HTML.Tag t, MutableAttributeSet s, int pos)
{
if (!(t.equals(HTML.Tag.META) && s.isDefined(HTML.Attribute.CONTENT))) return;
var content = s.getAttribute(HTML.Attribute.CONTENT).toString();
switch (String.valueOf(s.getAttribute(HTML.Attribute.NAME)))
{
case "music:song":
tracks.put(URI.create(content), track);
return;
case "music:musician_description":
track[ARTIST] = content;
return;
}
var property = String.valueOf(s.getAttribute("property"));
if ("og:title".equals(property)) track[TITLE] = content;
}
}
String[] delegate(HttpResponse <InputStream> res)
{
var delegate = new Delegate();
try
{
new ParserDelegator().parse(new InputStreamReader(res.body(), StandardCharsets.UTF_8), delegate, true);
}
catch (IOException ex)
{
throw new UncheckedIOException(ex);
}
return delegate.track;
}
public static void main(String[] args)
{
try
{
Arrays.stream(args).map(URI::create).forEach(uri -> new SpotLister(uri, System.out));
}
catch (Exception ex)
{
System.err.format("Request failed due to '%s'%n", ex.getMessage());
}
}
}
@guymac
Copy link
Author

guymac commented Dec 7, 2022

Updated for latest Spotify html output, Dec 2022

@guymac
Copy link
Author

guymac commented Jul 27, 2023

Rewritten for new Spotify, July 2023

@guymac
Copy link
Author

guymac commented Oct 23, 2023

Updated, October 2023.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment