/* == This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
*
* Copyright 2013, Enno Gottschalk <mrmaffen@googlemail.com>
*
* Tomahawk 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.
*
* Tomahawk 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 Tomahawk. If not, see <http://www.gnu.org/licenses/>.
*/
package org.tomahawk.libtomahawk.resolver;
import org.tomahawk.libtomahawk.collection.Album;
import org.tomahawk.libtomahawk.collection.AlphaComparable;
import org.tomahawk.libtomahawk.collection.Artist;
import org.tomahawk.libtomahawk.collection.ArtistAlphaComparable;
import org.tomahawk.libtomahawk.collection.Cacheable;
import org.tomahawk.libtomahawk.collection.Image;
import org.tomahawk.libtomahawk.collection.Playlist;
import org.tomahawk.libtomahawk.collection.Track;
import org.tomahawk.tomahawk_android.R;
import org.tomahawk.tomahawk_android.TomahawkApp;
import org.tomahawk.tomahawk_android.utils.IdGenerator;
import android.text.TextUtils;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;
/**
* This class represents a query which is passed to a resolver. It contains all the information
* needed to enable the Resolver to resolve the results.
*/
public class Query extends Cacheable implements AlphaComparable, ArtistAlphaComparable {
public static final String TAG = Query.class.getSimpleName();
private static final HashSet<String> sBlacklistedResults = new HashSet<>();
private Track mBasicTrack;
private String mResultHint;
private String mFullTextQuery;
private final boolean mIsFullTextQuery;
private final boolean mIsOnlyLocal;
private boolean mIsFetchedViaHatchet;
private final ConcurrentSkipListSet<Result> mTrackResults
= new ConcurrentSkipListSet<>(new ResultComparator());
private final ConcurrentHashMap<Result, Float> mTrackResultScores
= new ConcurrentHashMap<>();
public class ResultComparator implements Comparator<Result> {
/**
* The actual comparison method
*
* @param r1 First {@link org.tomahawk.libtomahawk.resolver.Result} object
* @param r2 Second {@link org.tomahawk.libtomahawk.resolver.Result} Object
* @return int containing comparison score
*/
public int compare(Result r1, Result r2) {
if (mResultHint != null) {
// We have a result hint. If the cacheKey matches we automatically put the matching
// Result at the top of the sorted list.
if (r1.getCacheKey().equals(mResultHint)) {
return -1;
} else if (r2.getCacheKey().equals(mResultHint)) {
return 1;
}
}
if (r1 == r2) {
return 0;
}
Float score1 = mTrackResultScores.get(r1);
Float score2 = mTrackResultScores.get(r2);
int scoreResult = score2.compareTo(score1);
if (scoreResult > 0) {
return 1;
} else if (scoreResult < 0) {
return -1;
} else {
// We have two identical trackScores.
// Now we take the Resolver's weight into account.
Integer weight1 = r1.getResolvedBy().getWeight();
Integer weight2 = r2.getResolvedBy().getWeight();
int weightResult = weight2.compareTo(weight1);
if (weightResult > 0) {
return 1;
} else if (weightResult < 0) {
return -1;
} else {
// We have two identical trackScores and Resolver weights.
Integer hashCode1 = r1.hashCode();
Integer hashCode2 = r2.hashCode();
int hashCodeResult = hashCode1.compareTo(hashCode2);
if (hashCodeResult > 0) {
return 1;
} else if (hashCodeResult < 0) {
return -1;
} else {
// should never happen
return 0;
}
}
}
}
}
/**
* Constructs a new Query.
*
* @param fullTextQuery fulltext-query String to construct this Query with
* @param onlyLocal whether or not this query should be resolved locally
*/
private Query(String fullTextQuery, boolean onlyLocal) {
super(Query.class, getCacheKey(fullTextQuery, onlyLocal));
mFullTextQuery = fullTextQuery != null ? fullTextQuery : "";
mIsFullTextQuery = true;
mIsOnlyLocal = onlyLocal;
}
/**
* Constructs a new Query.
*
* @param trackName track's name String
* @param artistName artist's name String
* @param albumName album's name String
* @param resultHint resultHint's name String
* @param onlyLocal whether or not this query should be resolved locally
* @param isFetchedViaHatchet whether or not this query has been fetched via the Hatchet API
*/
private Query(String trackName, String albumName, String artistName, String resultHint,
boolean onlyLocal, boolean isFetchedViaHatchet) {
super(Query.class, getCacheKey(trackName, albumName, artistName, resultHint, onlyLocal));
Artist artist = Artist.get(artistName);
Album album = Album.get(albumName, artist);
mBasicTrack = Track.get(trackName, album, artist);
mResultHint = resultHint != null ? resultHint : "";
mIsFullTextQuery = false;
mIsOnlyLocal = onlyLocal;
mIsFetchedViaHatchet = isFetchedViaHatchet;
}
/**
* Static builder method which constructs a query or fetches it from the cache, resulting in
* Queries being unique by trackname/artistname/albumname/resulthint/onlyLocal
*/
public static Query get(String fullTextQuery, boolean onlyLocal) {
Cacheable cacheable = get(Query.class, getCacheKey(fullTextQuery, onlyLocal));
return cacheable != null ? (Query) cacheable : new Query(fullTextQuery, onlyLocal);
}
/**
* Static builder method which constructs a query or fetches it from the cache, resulting in
* Queries being unique by trackname/artistname/albumname/resulthint/onlyLocal
*/
public static Query get(String trackName, String albumName, String artistName,
String resultHint, boolean onlyLocal, boolean isFetchedViaHatchet) {
Cacheable cacheable = get(Query.class,
getCacheKey(trackName, albumName, artistName, resultHint, onlyLocal));
return cacheable != null ? (Query) cacheable :
new Query(trackName, albumName, artistName, resultHint, onlyLocal,
isFetchedViaHatchet);
}
/**
* Static builder method which constructs a query or fetches it from the cache, resulting in
* Queries being unique by trackname/artistname/albumname/resulthint/onlyLocal
*/
public static Query get(String trackName, String albumName, String artistName,
boolean onlyLocal) {
return get(trackName, albumName, artistName, null, onlyLocal, false);
}
/**
* Static builder method which constructs a query or fetches it from the cache, resulting in
* Queries being unique by trackname/artistname/albumname/resulthint/onlyLocal
*/
public static Query get(String trackName, String albumName, String artistName,
boolean onlyLocal, boolean isFetchedViaHatchet) {
return get(trackName, albumName, artistName, null, onlyLocal, isFetchedViaHatchet);
}
/**
* Static builder method which constructs a query or fetches it from the cache, resulting in
* Queries being unique by trackname/artistname/albumname/resulthint
*/
public static Query get(Track track, boolean onlyLocal) {
return get(track.getName(), track.getAlbum().getName(), track.getArtist().getName(), null,
onlyLocal, false);
}
/**
* Static builder method which constructs a query or fetches it from the cache, resulting in
* Queries being unique by trackname/artistname/albumname/resulthint
*/
public static Query get(Result result, boolean onlyLocal) {
return get(result.getTrack().getName(), result.getTrack().getAlbum().getName(),
result.getTrack().getArtist().getName(), result.getCacheKey(), onlyLocal, false);
}
public static Query getByKey(String cacheKey) {
return (Query) get(Query.class, cacheKey);
}
public Class getMediaPlayerClass() {
if (getPreferredTrackResult() != null) {
return getPreferredTrackResult().getMediaPlayerClass();
} else {
return null;
}
}
public Track getBasicTrack() {
return mBasicTrack;
}
public static HashSet<String> getBlacklistedResults() {
return sBlacklistedResults;
}
/**
* @return An ArrayList<Query> which contains all tracks in the resultList, sorted by score.
* Given as queries.
*/
public Playlist getResultPlaylist() {
ArrayList<Query> queries = new ArrayList<>();
for (Result result : mTrackResults) {
if (!isOnlyLocal() || result.isLocal()) {
Query query = Query.get(result, isOnlyLocal());
query.addTrackResult(result, mTrackResultScores.get(result));
queries.add(query);
}
}
Playlist playlist = Playlist.fromQueryList(IdGenerator.getSessionUniqueStringId(),
mFullTextQuery, "", queries);
playlist.setFilled(true);
return playlist;
}
public Result getPreferredTrackResult() {
for (Result trackResult : mTrackResults) {
if (trackResult.getResolvedBy().isEnabled()) {
return trackResult;
}
}
return null;
}
public Track getPreferredTrack() {
Result result = getPreferredTrackResult();
if (result != null) {
return result.getTrack();
}
return mBasicTrack;
}
/**
* Add a {@link Result} to this {@link Query}.
*
* @param result The {@link Result} which should be added
* @param trackScore the trackScore for the given {@link Result}
*/
public void addTrackResult(Result result, float trackScore) {
String cacheKey = result.getCacheKey();
if (!sBlacklistedResults.contains(cacheKey)) {
mTrackResultScores.put(result, trackScore);
mTrackResults.add(result);
}
}
public void blacklistTrackResult(Result result) {
sBlacklistedResults.add(result.getCacheKey());
if (result.getCacheKey().equals(mResultHint)) {
mResultHint = null;
}
mTrackResults.remove(result);
mTrackResultScores.remove(result);
}
public String getResultHint() {
return mResultHint;
}
public String getTopTrackResultKey() {
Result result = getPreferredTrackResult();
if (result != null) {
return getPreferredTrackResult().getCacheKey();
}
return null;
}
public String getFullTextQuery() {
return mFullTextQuery;
}
public boolean isFullTextQuery() {
return mIsFullTextQuery;
}
public boolean isOnlyLocal() {
return mIsOnlyLocal;
}
public boolean isPlayable() {
return getPreferredTrackResult() != null;
}
public boolean isSolved() {
Result result = getPreferredTrackResult();
return result != null && result.getCacheKey().equals(mResultHint);
}
public boolean isFetchedViaHatchet() {
return mIsFetchedViaHatchet;
}
/**
* This method determines how similar the given result is to the search string.
*/
public float howSimilar(Result r) {
String resultArtistName = ResultScoring.cleanUpString(r.getArtist().getName(), false);
String resultAlbumName = ResultScoring.cleanUpString(r.getAlbum().getName(), false);
String resultTrackName = ResultScoring.cleanUpString(r.getTrack().getName(), false);
if (isFullTextQuery()) {
String fullTextQuery = ResultScoring.cleanUpString(mFullTextQuery, true);
float maxResult = 0f;
maxResult = Math.max(maxResult, ResultScoring.calculateScore(
resultTrackName + " " + resultAlbumName + " " + resultArtistName,
fullTextQuery));
maxResult = Math.max(maxResult, ResultScoring.calculateScore(
resultTrackName + " " + resultArtistName + " " + resultAlbumName,
fullTextQuery));
maxResult = Math.max(maxResult, ResultScoring.calculateScore(
resultArtistName + " " + resultTrackName + " " + resultAlbumName,
fullTextQuery));
maxResult = Math.max(maxResult, ResultScoring.calculateScore(
resultArtistName + " " + resultAlbumName + " " + resultTrackName,
fullTextQuery));
maxResult = Math.max(maxResult, ResultScoring.calculateScore(
resultAlbumName + " " + resultArtistName + " " + resultTrackName,
fullTextQuery));
maxResult = Math.max(maxResult, ResultScoring.calculateScore(
resultAlbumName + " " + resultTrackName + " " + resultArtistName,
fullTextQuery));
return maxResult;
} else {
String queryArtistName =
ResultScoring.cleanUpString(mBasicTrack.getArtist().getName(), false);
float artistScore = ResultScoring.calculateScore(resultArtistName, queryArtistName);
String queryTrackName =
ResultScoring.cleanUpString(mBasicTrack.getName(), false);
float trackScore = ResultScoring.calculateScore(resultTrackName, queryTrackName);
String queryAlbumName =
ResultScoring.cleanUpString(mBasicTrack.getAlbum().getName(), false);
float albumScore;
if (queryAlbumName.isEmpty()) {
return (artistScore + trackScore) / 2;
} else {
albumScore = ResultScoring.calculateScore(resultAlbumName, queryAlbumName);
return (artistScore * 3 + albumScore + trackScore * 4) / 8;
}
}
}
public String getName() {
if (isFullTextQuery()) {
return mFullTextQuery;
}
return getPreferredTrack().getName();
}
/**
* @return the name that should be displayed
*/
public String getPrettyName() {
return getName().isEmpty() ?
TomahawkApp.getContext().getResources().getString(R.string.unknown)
: getName();
}
public Artist getArtist() {
return getPreferredTrack().getArtist();
}
public Album getAlbum() {
if (mIsFetchedViaHatchet && !mBasicTrack.getAlbum().getName().isEmpty()) {
return mBasicTrack.getAlbum();
}
return getPreferredTrack().getAlbum();
}
public Image getImage() {
if (getAlbum().getImage() != null && !TextUtils
.isEmpty(getAlbum().getImage().getImagePath())) {
return getAlbum().getImage();
} else {
return getArtist().getImage();
}
}
public boolean hasArtistImage() {
return (getAlbum().getImage() == null
|| TextUtils.isEmpty(getAlbum().getImage().getImagePath()))
&& getArtist().getImage() != null;
}
public String toShortString() {
String desc;
if (mIsFullTextQuery) {
desc = "fullTextQuery: '" + mFullTextQuery + "'";
} else {
desc = "basic: " + mBasicTrack.toShortString()
+ " - preferred: " + getPreferredTrack().toShortString();
}
return desc + ", resultCount: " + mTrackResults.size();
}
@Override
public String toString() {
return getClass().getSimpleName() + "( " + toShortString() + " )@"
+ Integer.toHexString(hashCode());
}
}