/**
* Copyright (C) 2013 Johannes Schnatterer
*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This file is part of nusic.
*
* nusic is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* nusic is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with nusic. If not, see <http://www.gnu.org/licenses/>.
*/
package info.schnatterer.nusic.core.impl;
import fm.last.musicbrainz.coverart.CoverArt;
import fm.last.musicbrainz.coverart.CoverArtArchiveClient;
import fm.last.musicbrainz.coverart.CoverArtImage;
import fm.last.musicbrainz.coverart.impl.DefaultCoverArtArchiveClient;
import info.schnatterer.nusic.core.RemoteMusicDatabaseService;
import info.schnatterer.nusic.core.ServiceException;
import info.schnatterer.nusic.core.i18n.CoreMessageKey;
import info.schnatterer.nusic.data.DatabaseException;
import info.schnatterer.nusic.data.dao.ArtworkDao;
import info.schnatterer.nusic.data.dao.ArtworkDao.ArtworkType;
import info.schnatterer.nusic.data.model.Artist;
import info.schnatterer.nusic.data.model.Release;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import javax.inject.Inject;
import org.musicbrainz.MBWS2Exception;
import org.musicbrainz.model.ArtistCreditWs2;
import org.musicbrainz.model.NameCreditWs2;
import org.musicbrainz.model.entity.ReleaseWs2;
import org.musicbrainz.model.searchresult.ReleaseResultWs2;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.util.concurrent.RateLimiter;
import com.google.inject.BindingAnnotation;
/**
* {@link RemoteMusicDatabaseService} that queries information from MusicBrainz.
*
* @author schnatterer
*
*/
public class RemoteMusicDatabaseServiceMusicBrainz implements
RemoteMusicDatabaseService {
private static final Logger LOG = LoggerFactory
.getLogger(RemoteMusicDatabaseServiceMusicBrainz.class);
/**
* See http://musicbrainz.org/doc/Development/XML_Web_Service/Version_2#
* Release_Type_and_Status
*/
private static final String SEARCH_BASE = "type:album";
private static final String SEARCH_DATE_BASE = " AND date:[";
private static final String SEARCH_DATE_TO = " TO ";
private static final String SEARCH_DATE_OPEN_END = "?";
private static final String SEARCH_DATE_FINAL = "]";
private static final String SEARCH_ARTIST_1 = " AND artist:\"";
private static final String SEARCH_ARTIST_2 = "\"";
/**
* MusicBrainz allows at max 22 requests in 20 seconds. However, we still
* get 503s then. Try 1 request per second.
*/
private static final double PERMITS_PER_SECOND = 1.0;
private final RateLimiter rateLimiter = RateLimiter
.create(PERMITS_PER_SECOND);
private CoverArtArchiveClient client = new DefaultCoverArtArchiveClient();
/** Application name used in user agent string of request. */
private String appName;
/** Application version used in user agent string of request. */
private String appVersion;
/**
* Contact URL or author email used in user agent string of request.
*/
private String appContact;
@Inject
private ArtworkDao artworkDao;
/**
* Creates a service instance for finding releases.
*
* @param appName
* application name used in user agent string of request
*
* @param appVersion
* application version used in user agent string of request
*
* @param appContact
* contact URL or author email used in user agent string of
* request
*/
@Inject
public RemoteMusicDatabaseServiceMusicBrainz(
@ApplicationName String appName,
@ApplicationVersion String appVersion,
@ApplicationContact String appContact) {
this.appName = appName;
this.appVersion = appVersion;
this.appContact = appContact;
}
private DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
@Override
public Artist findReleases(Artist artist, Date fromDate, Date endDate)
throws ServiceException {
if (artist == null || artist.getArtistName() == null) {
return null;
}
String artistName = artist.getArtistName();
Map<String, Release> releases = new HashMap<String, Release>();
try {
// List<ReleaseResultWs2> releaseResults = findReleases();
org.musicbrainz.controller.Release releaseSearch = createReleaseSearch(
appName, appVersion, appContact);
releaseSearch.search(appendDate(fromDate, endDate,
new StringBuffer(SEARCH_BASE)).append(SEARCH_ARTIST_1)
.append(artistName).append(SEARCH_ARTIST_2).toString());
// Limit request rate to avoid server bans
rateLimiter.acquire();
processReleaseResults(artistName, artist, releases,
releaseSearch.getFirstSearchResultPage());
while (releaseSearch.hasMore()) {
// TODO check if internet connection still there?
// Limit request rate to avoid server bans
rateLimiter.acquire();
processReleaseResults(artistName, artist, releases,
releaseSearch.getNextSearchResultPage());
}
} catch (MBWS2Exception mBWS2Exception) {
throw new AndroidServiceException(
CoreMessageKey.ERROR_QUERYING_MUSIC_BRAINZ, mBWS2Exception,
artistName);
} catch (SecurityException securityException) {
throw securityException;
} catch (Exception e) {
throw new AndroidServiceException(
CoreMessageKey.ERROR_FINDING_RELEASE_ARTIST, e, artistName);
}
return artist;
}
public StringBuffer appendDate(Date startDate, Date endDate,
StringBuffer stringBuffer) {
if (startDate == null && endDate == null) {
// Don't append anything
return stringBuffer;
}
stringBuffer.append(SEARCH_DATE_BASE);
if (startDate != null) {
stringBuffer.append(dateFormat.format(startDate));
} else {
stringBuffer.append("0");
}
stringBuffer.append(SEARCH_DATE_TO);
if (endDate != null) {
stringBuffer.append(dateFormat.format(endDate));
} else {
stringBuffer.append(SEARCH_DATE_OPEN_END);
}
stringBuffer.append(SEARCH_DATE_FINAL);
return stringBuffer;
}
/**
* Creates an instance of the release search object.
*
* @param userAgentName
* custom application name used in user agent string. If
* <code>null</code>, the default user agent string is used.
* @param userAgentVersion
* custom application version used in user agent string
* @param userAgentContact
* contact URL or author email used in user agent string
*
* @return a new instance of the web service implementation.
*/
protected org.musicbrainz.controller.Release createReleaseSearch(
String userAgentName, String userAgentVersion,
String userAgentContact) {
return new org.musicbrainz.controller.Release(userAgentName,
userAgentVersion, userAgentContact);
}
/**
* Iterates over the results of a MusicBrainz query and converts them to
* nusic entities. In addition, tries to download artwork for each release
* group.
*
* @param artistName
* @param artist
* @param releases
* @param releaseResults
*/
protected void processReleaseResults(String artistName, Artist artist,
Map<String, Release> releases, List<ReleaseResultWs2> releaseResults) {
for (ReleaseResultWs2 releaseResultWs2 : releaseResults) {
// Make sure not to add other artists albums
ReleaseWs2 releaseResult = releaseResultWs2.getRelease();
if (releaseResult.getArtistCredit().getArtistCreditString().trim()
.equalsIgnoreCase(artistName.trim())) {
if (artist.getMusicBrainzId() == null
|| artist.getMusicBrainzId().isEmpty()) {
artist.setMusicBrainzId(getMusicBrainzId(releaseResult
.getArtistCredit()));
}
// Use only the release with the "oldest" date of a release
// group
String releaseGroupId = releaseResult.getReleaseGroup().getId()
.trim();
Release existingRelease = releases.get(releaseGroupId);
Date newDate = releaseResult.getDate();
if (existingRelease == null) {
Release release = new Release();
release.setArtist(artist);
release.setReleaseName(releaseResult.getTitle());
release.setReleaseDate(newDate);
release.setMusicBrainzId(releaseGroupId);
// LOG.debug("Release: " + artist.getArtistName()
// + "-" + releaseResult.getTitle() + "-"
// + releaseGroupId);
try {
downloadFrontCover(release);
} catch (IOException e) {
LOG.warn("Unable to download cover", e);
} catch (DatabaseException e) {
LOG.warn("Unable to store cover", e);
}
// TODO store all release dates and their countries?
artist.getReleases().add(release);
releases.put(releaseGroupId, release);
} else {
if (existingRelease.getReleaseDate() == null
|| (newDate != null && existingRelease
.getReleaseDate().after(newDate))) {
// Change date of existing release
existingRelease.setReleaseDate(newDate);
}
}
}
}
}
/**
* Downloads the front cover of the release and persists it.
*
* @param release
* @throws IOException
* error downloading the artwork
* @throws DatabaseException
*/
private void downloadFrontCover(Release release) throws IOException,
DatabaseException {
CoverArt coverArt = null;
UUID mbid = UUID.fromString(release.getMusicBrainzId());
coverArt = client.getReleaseGroupByMbid(mbid);
if (coverArt != null && coverArt.getImages() != null) {
for (CoverArtImage coverArtImage : coverArt.getImages()) {
if (coverArtImage.isFront()) {
/*
* TODO load large thumbnail for certain screen resolutions?
*/
if (!artworkDao.exists(release, ArtworkType.SMALL)) {
InputStream smallThumbnail = coverArtImage
.getSmallThumbnail();
/*
* As transactions are not used yet, the cover that is
* persisted here won't be deleted, if the corresponding
* release could not be saved.
*
* This is ignored here as the app will try it again. As
* the artwork is needed anyway we might as well keep
* it.
*/
artworkDao.save(release, ArtworkType.SMALL,
smallThumbnail);
release.setCoverartArchiveId(coverArtImage.getId());
// LOG.debug(
// "Cover: " + artist.getArtistName() + "-"
// + release.getReleaseName() + "_"
// + release.getMusicBrainzId() + "_"
// + coverArtImage.getId() + ". Size: "
// + output.length());
/*
* We successfully downloaded the cover! Stop trying to
* get another one
*/
break;
}
}
}
}
}
private String getMusicBrainzId(ArtistCreditWs2 artistCredit) {
String musicBrainzId = null;
List<NameCreditWs2> nameCredits = artistCredit.getNameCredits();
if (nameCredits.size() > 0 && nameCredits.get(0).getArtist() != null) {
musicBrainzId = nameCredits.get(0).getArtist().getId();
}
return musicBrainzId;
}
/**
* Application name used in user agent string of request
*
* @author schnatterer
*
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.PARAMETER })
@BindingAnnotation
public @interface ApplicationName {
}
/**
* Application version used in user agent string of request
*
* @author schnatterer
*
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.PARAMETER })
@BindingAnnotation
public @interface ApplicationVersion {
}
/**
* Contact URL or author email used in user agent string of request
*
* @author schnatterer
*
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.PARAMETER })
@BindingAnnotation
public @interface ApplicationContact {
}
}