Пример интеграции Last.FM API и VK.COM API для обновления содержимого своего плеера

в 16:02, , рубрики: Без рубрики

Всем привет!

Думаю, большинство из вас знакомы с социальными сетями "Вконтакте" и "Last.fm". В данной статье мы рассмотрим маленький пример интеграции между API обоих сервисов. Если быть точнее, будем получать список любимых треков с last.fm, искать бинарный контент в ВК и сохранять на HDD.
Статья будет скорее интересна новичкам, чем профессионалам, тем не менее, требует определенной технической подготовки. Тестовый проект реализован средствами Java.

Если читателю интересно, что из этого получилось, прошу пожаловать под кат.

Дисклаймер

В наше страшное время, когда корпорации пытаются отсудить друг у друга миллиард за несколько строк кода. Когда получить проблему можно на «раз-два», я просто вынужден сказать следующее.
Программная реализация в рамках данной статьи имеет сугубо ознакомительные побуждения и не преследует никаких коммерческих мотивов.

Intro

Не буду долго затягивать. Определимся с целями.

  1. Получить список любимых треков на last.fm
  2. Найти треки на серверах vk.com
  3. Скачать треки на плеер

Но прежде всего, необходимо получить доступ к API обоих сервисов.
На Хабре уже были статьи, например, эта. Не буду описывать все подробно, поэтому сразу ссылки:
API ключ last.fm
API ключ vk.com
Далее, внимательно изучаем спецификацию (last.fm, vk.com) и определяемся, чего же именно мы хотим.
Собственно, нам нужны методы audio.search и user.getLovedTracks.

Тестовые запросы

После того, как приложение уже создано, необходимо проверить с чем мы будем работать.
С last.fm все просто. Делаем обычный html запрос в строке браузера:
http://ws.audioscrobbler.com/2.0/?method=user.getlovedtracks&user=USER&api_key=API_KEY
Если все параметры были введены верно, то можно будет увидеть xml'ку, в которой помимо названия трека, а также его авторов, можно встретить всякую дополнительную информацию.
С vk.com все практически также, за тем лишь исключением, что без параметра access_token мы не сможем получить доступ к аудиозаписям.
Делаем запрос:
http://api.vk.com/oauth/authorize?client_id=ID&redirect_uri=http://api.vk.com/blank.html&scope=audio&display=page&response_type=token
В итоге, редирект отправит нас на ссылку вида:
http://api.vk.com/blank.html#access_token=XYZ&expires_in=86400&user_id=ID
XYZ — это, что нам нужно.
Теперь запрос вида:
https://api.vk.com/method/audio.search.xml?q=AC/DC%20-%20Highway%20to%20Hell&access_token=XYZ должен выдать xml'ку, где можно увидеть физический адрес бинарного музыкально файла.
Если все «взлетело», можем идти дальше.

Реализация ПО

Заведем себе файлик Configuration.properties, где будут храниться текущие настройки.
last_fm_api=
last_fm_user=
last_fm_ws_address=http://ws.audioscrobbler.com/2.0/
vk_com_access_token=
vk_com_ws_address=https://api.vk.com/method/

Для простоты, решил на GUI на заморачиваться. Данные удобно хранить в коллекции POJO бинов такого вида:

public class LovedTrackBean {
	
	public LovedTrackBean(String trackName, String trackArtist) {
		this.trackName = trackName;
		this.trackArtist = trackArtist;
	}
	
	//GETTERS & SETTERS

    public String toString() {
		return trackArtist + " - " + trackName;
	}
	
	private String trackName = null;
	private String trackArtist = null;
	private String trackURL = null;
}

Теперь нам нужно воспользоваться API last.fm и получить список любимых треков.
Примерно, вот так:

public class TrackLover {

	public TrackLover() {
		StringBuffer buff = new StringBuffer();
		buff.append(PropertyLoader.getProperties().get(PropertyLoader.LAST_FM_WS_ADDRESS));
		buff.append("?method=");
		buff.append(LAST_FM_API_METHOD);
		buff.append("&user=");
		buff.append(PropertyLoader.getProperties().get(PropertyLoader.LAST_FM_USER));
		buff.append("&api_key=");
		buff.append(PropertyLoader.getProperties().get(PropertyLoader.LAST_FM_API));
		connectToUrl = buff.toString();
	}
	
	public List<LovedTrackBean> getLovedTracks() throws IOException, ParserConfigurationException, SAXException {
		if(lovedTracks == null) {
				URL url = new URL(connectToUrl);
				URLConnection conn = url.openConnection();
				
				DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
				DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
				Document doc = docBuilder.parse(conn.getInputStream());
				doc.getDocumentElement().normalize();
				
				NodeList listOfLovedTracks = doc.getElementsByTagName("track");
				int tracksTotal = listOfLovedTracks.getLength();
				lovedTracks = new ArrayList<LovedTrackBean>(tracksTotal);
				
//...
				//Парсим содержимое
//...
				
				System.out.println("######### AFTER LAST.FM RESPONSE #########");
				for(LovedTrackBean track: lovedTracks) {
					System.out.println(track.toString());
				}
				
			return lovedTracks;
		} else {
			lovedTracks = new ArrayList<LovedTrackBean>(1);
			lovedTracks.add(new LovedTrackBean("Wish you were here", "Pink Floyd"));
			return lovedTracks;
		}
	}
	
	private String connectToUrl = null;
	private List<LovedTrackBean> lovedTracks = null;
	
	public static final String LAST_FM_API_METHOD = "user.getlovedtracks";
}

Данный код должен вернуть нам коллекцию с любимыми треками.
Следующая остановка — vk.com.
Попытаемся по парам «Автор — Композиция» получить аудиозапись. Для простоты, возьмем первую попавшуюся.

public class TrackURLFinder {
	public TrackURLFinder(LovedTrackBean lovedTrack) {
		this.lovedTrack = lovedTrack;
		StringBuffer buff = new StringBuffer();
		buff.append(PropertyLoader.getProperties().get(PropertyLoader.VK_COM_WS_ADDRESS));
		buff.append(VK_COM_API_METHOD);
		buff.append("?q=");
		buff.append(lovedTrack.toString());
		buff.append("&");
		buff.append("access_token=");
		buff.append(PropertyLoader.getProperties().get(PropertyLoader.VK_COM_ACCESS_TOKEN));
		connectToUrl = buff.toString();
		connectToUrl = connectToUrl.replaceAll(" ", "%20");
		System.out.println(connectToUrl);
	}
	
	public void addUrlToLovedTrack() throws SAXException, IOException, ParserConfigurationException {
		if(lovedTrack != null) {
				URL url = new URL(connectToUrl);
				URLConnection conn = url.openConnection();
				
				DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
				DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
				Document doc = docBuilder.parse(conn.getInputStream());
				doc.getDocumentElement().normalize();
				
				NodeList audioList = doc.getElementsByTagName("audio");
				Node currentNode = audioList.item(0);
				
				Element currentElement = (Element) currentNode;
				
				NodeList urlList = currentElement.getElementsByTagName("url");
				Element urlElement = (Element) urlList.item(0);
				NodeList urlFormattedList = urlElement.getChildNodes();
				
				String urlAddressOfTheMP3File = urlFormattedList.item(0).getNodeValue().trim();
				
				lovedTrack.setTrackURL(urlAddressOfTheMP3File);
				System.out.println(urlAddressOfTheMP3File);
		}
	}
	
	private String connectToUrl = null;
	private LovedTrackBean lovedTrack = null;
	
	public static final String VK_COM_API_METHOD = "audio.search.xml";
}

И, наконец, получаем контент:

public class MP3MusicContentDownloader {
	
	public MP3MusicContentDownloader(String urlAddress, String fileName) throws MalformedURLException {
		this.fileName = fileName;
		url = new URL(urlAddress);
	}

	public void download() throws IOException {
			conn = url.openConnection();
			
			System.out.println("Started downloading of " + fileName);
			
			String contentType = conn.getContentType();
			int contentLength = conn.getContentLength();
			if (contentType.startsWith("text/") || contentLength == -1) {
				throw new IOException("This is not a binary file.");
			}
			
			File folder = new File(DEFAULT_DIRECTORY_NAME);
			if (!folder.exists() || !folder.isDirectory()) {
				folder.mkdir();
			}
			
			File binaryFile = new File(DEFAULT_DIRECTORY_NAME + File.separator + fileName);
			if (!binaryFile.exists()) {

				BufferedInputStream in = new BufferedInputStream(conn.getInputStream());
				byte[] data = new byte[contentLength];
				int bytesRead = 0;
				int offset = 0;
				while (offset < contentLength) {
					bytesRead = in.read(data, offset, data.length - offset);
					if (bytesRead == -1)
						break;
					offset += bytesRead;
				}
				in.close();

				if (offset != contentLength) {
					throw new IOException("Only read " + offset + " bytes; Expected " + contentLength + " bytes");
				}
				FileOutputStream out = new FileOutputStream(binaryFile);
				out.write(data);
				out.flush();
				out.close();
			} else {
				return;
			}
	}

	private String fileName = null;
	private URL url = null;
	private URLConnection conn = null;

	private static final String DEFAULT_DIRECTORY_NAME = "music";
}

На этом все. Запустить, надеюсь, не составит труда.

TODOs

Так как это всего лишь пробный пример, который, кстати, был выполнен в целях самообучения, почти уверен, что содержит некоторое количество багов/недочетов. Но тем не менее, пример на моих треках отработал. Конкретно в данном случае, можно было бы реализовать многопоточность, или, например, полное сканирование всех композиций избранного (сейчас собираются первые 50), или, например, проверка на валидность access_token'ов (вдруг, истек срок действия). Да много чего. Но, если вы уже поняли, смысл статьи был не в этом :)

Всем спасибо, у кого хватило терпения дойти до этих строк!

Автор: KriegeR


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js