/* == 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.Collection;
import org.tomahawk.libtomahawk.collection.CollectionManager;
import org.tomahawk.libtomahawk.collection.DbCollection;
import org.tomahawk.libtomahawk.collection.UserCollection;
import org.tomahawk.libtomahawk.resolver.models.ScriptResolverUrlResult;
import org.tomahawk.tomahawk_android.TomahawkApp;
import org.tomahawk.tomahawk_android.utils.ThreadManager;
import org.tomahawk.tomahawk_android.utils.TomahawkRunnable;
import android.text.TextUtils;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import de.greenrobot.event.EventBus;
/**
* The {@link PipeLine} is being used to provide all the resolving functionality. All {@link
* Resolver}s are stored and invoked here. Callbacks which report the found {@link Result}s are also
* included in this class.
*/
public class PipeLine {
private final static String TAG = PipeLine.class.getSimpleName();
public static final int URL_TYPE_PLAYLIST = 1;
public static final int URL_TYPE_TRACK = 2;
public static final int URL_TYPE_ALBUM = 3;
public static final int URL_TYPE_ARTIST = 4;
public static final int URL_TYPE_XSPFURL = 5;
private static final float MINSCORE = 0.5f;
private static final float FULLTEXT_MINSCORE = 0f;
private static class Holder {
private static final PipeLine instance = new PipeLine();
}
public static class ResultsEvent {
public Query mQuery;
}
public static class UrlResultsEvent {
public Resolver mResolver;
public ScriptResolverUrlResult mResult;
}
public static class ResolversChangedEvent {
public ScriptResolver mScriptResolver;
public boolean mManuallyAdded;
}
private final Set<ScriptAccount> mScriptAccounts =
Collections.newSetFromMap(new ConcurrentHashMap<ScriptAccount, Boolean>());
private final Set<ScriptAccount> mManualScriptAccounts =
Collections.newSetFromMap(new ConcurrentHashMap<ScriptAccount, Boolean>());
private final Set<ScriptResolver> mResolvers =
Collections.newSetFromMap(new ConcurrentHashMap<ScriptResolver, Boolean>());
private final Set<String> mWaitingUrlLookups =
Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
private final Set<Query> mWaitingQueries =
Collections.newSetFromMap(new ConcurrentHashMap<Query, Boolean>());
private final Set<ScriptAccount> mLoadingPlugins =
Collections.newSetFromMap(new ConcurrentHashMap<ScriptAccount, Boolean>());
private PipeLine() {
try {
String[] plugins = TomahawkApp.getContext().getAssets().list("js/resolvers");
for (String plugin : plugins) {
String path = "/js/resolvers/" + plugin;
ScriptAccount account = new ScriptAccount(path, false);
mScriptAccounts.add(account);
mLoadingPlugins.add(account);
}
String manualResolverDirPath = TomahawkApp.getContext().getFilesDir().getAbsolutePath()
+ File.separator + "manualresolvers";
File manualResolverDir = new File(manualResolverDirPath);
plugins = manualResolverDir.list();
if (plugins != null) {
for (String plugin : plugins) {
if (!plugin.equals(".temp")) {
String pluginPath = manualResolverDirPath + File.separator + plugin;
File pluginFile = new File(pluginPath);
if (pluginFile.isDirectory()) {
ScriptAccount account = new ScriptAccount(pluginPath, true);
mScriptAccounts.add(account);
mLoadingPlugins.add(account);
}
}
}
}
} catch (IOException e) {
Log.e(TAG, "PipeLine<init>: " + e.getClass() + ": " + e.getLocalizedMessage());
}
}
public static PipeLine get() {
return Holder.instance;
}
public void onPluginLoaded(ScriptAccount account) {
mLoadingPlugins.remove(account);
if (mLoadingPlugins.isEmpty()) {
Log.d(TAG, "All plugins loaded. Resolving "
+ mWaitingQueries.size() + " waiting queries. Looking up "
+ mWaitingUrlLookups.size() + " waiting URLs.");
for (Query query : mWaitingQueries) {
resolve(query);
}
mWaitingQueries.clear();
for (String url : mWaitingUrlLookups) {
lookupUrl(url);
}
mWaitingUrlLookups.clear();
}
}
public void addScriptAccount(ScriptAccount scriptAccount) {
mManualScriptAccounts.add(scriptAccount);
mScriptAccounts.add(scriptAccount);
mLoadingPlugins.add(scriptAccount);
}
public void addResolver(ScriptResolver resolver) {
mResolvers.add(resolver);
ResolversChangedEvent event = new ResolversChangedEvent();
event.mScriptResolver = resolver;
event.mManuallyAdded = mManualScriptAccounts.contains(resolver.getScriptAccount());
EventBus.getDefault().post(event);
}
public void removeResolver(ScriptResolver resolver) {
mResolvers.remove(resolver);
EventBus.getDefault().post(new ResolversChangedEvent());
}
/**
* Get the {@link ScriptResolver} with the given id, null if not found
*/
public ScriptResolver getResolver(String id) {
for (ScriptResolver resolver : mResolvers) {
if (resolver.getId().equals(id)) {
return resolver;
}
}
return null;
}
/**
* Get the ArrayList of all {@link org.tomahawk.libtomahawk.resolver.ScriptResolver}s
*/
public ArrayList<ScriptResolver> getScriptResolvers() {
ArrayList<ScriptResolver> scriptResolvers = new ArrayList<>();
for (Resolver resolver : mResolvers) {
if (resolver instanceof ScriptResolver) {
scriptResolvers.add((ScriptResolver) resolver);
}
}
return scriptResolvers;
}
/**
* This will invoke every {@link Resolver} to resolve the given fullTextQuery. If there already
* is a {@link Query} with the same fullTextQuery, the old resultList will be reported.
*/
public Query resolve(String fullTextQuery) {
return resolve(fullTextQuery, false);
}
/**
* This will invoke every {@link Resolver} to resolve the given fullTextQuery. If there already
* is a {@link Query} with the same fullTextQuery, the old resultList will be reported.
*/
public Query resolve(String fullTextQuery, boolean forceOnlyLocal) {
if (fullTextQuery != null && !TextUtils.isEmpty(fullTextQuery)) {
Query q = Query.get(fullTextQuery, forceOnlyLocal);
resolve(q, forceOnlyLocal);
return q;
}
return null;
}
/**
* This will invoke every {@link Resolver} to resolve the given {@link Query}.
*/
public Query resolve(Query q) {
return resolve(q, false);
}
/**
* This will invoke every {@link Resolver} to resolve the given {@link Query}.
*/
public Query resolve(final Query q, final boolean forceOnlyLocal) {
final TomahawkRunnable r = new TomahawkRunnable(TomahawkRunnable.PRIORITY_IS_RESOLVING) {
@Override
public void run() {
if (!mLoadingPlugins.isEmpty()) {
mWaitingQueries.add(q);
} else {
for (ScriptResolver resolver : mResolvers) {
if (shouldResolve(resolver, q, forceOnlyLocal)) {
resolver.resolve(q);
}
}
for (Collection collection : CollectionManager.get().getCollections()) {
if (!(collection instanceof UserCollection)
&& shouldResolve(collection, q, forceOnlyLocal)) {
((DbCollection) collection).resolve(q);
}
}
}
if (shouldResolve(CollectionManager.get().getUserCollection(), q, forceOnlyLocal)) {
CollectionManager.get().getUserCollection().resolve(q);
}
}
};
ThreadManager.get().execute(r, q);
return q;
}
/**
* Method to determine if a given Resolver should resolve the query or not
*/
public boolean shouldResolve(Collection collection, Query q, boolean forceOnlyLocal) {
return !forceOnlyLocal && !q.isOnlyLocal() && collection instanceof DbCollection
|| collection instanceof UserCollection;
}
/**
* Method to determine if a given Resolver should resolve the query or not
*/
public boolean shouldResolve(Resolver resolver, Query q, boolean forceOnlyLocal) {
return !forceOnlyLocal && !q.isOnlyLocal() && resolver.isEnabled();
}
/**
* Resolve the given ArrayList of {@link org.tomahawk.libtomahawk.resolver.Query}s and return a
* HashSet containing all query ids
*/
public HashSet<Query> resolve(Set<Query> queries) {
return resolve(queries, false);
}
/**
* Resolve the given ArrayList of {@link org.tomahawk.libtomahawk.resolver.Query}s and return a
* HashSet containing all query keys
*/
public HashSet<Query> resolve(Set<Query> queries, boolean forceOnlyLocal) {
HashSet<Query> queryKeys = new HashSet<>();
if (queries != null) {
for (Query query : queries) {
queryKeys.add(resolve(query, forceOnlyLocal));
}
}
return queryKeys;
}
/**
* If the {@link ScriptResolver} has resolved the {@link Query}, this method will be called.
* This method will then calculate a score and assign it to every {@link Result}. If the score
* is higher than MINSCORE the {@link Result} is added to the output resultList.
*
* @param query the {@link Query} that results are being reported for
* @param results the unfiltered {@link ArrayList} of {@link Result}s
*/
public void reportResults(final Query query, final ArrayList<Result> results,
final String resolverId) {
int priority;
if (TomahawkApp.PLUGINNAME_USERCOLLECTION.equals(resolverId)) {
priority = TomahawkRunnable.PRIORITY_IS_REPORTING_LOCALSOURCE;
} else if (TomahawkApp.PLUGINNAME_SPOTIFY.equals(resolverId)
|| TomahawkApp.PLUGINNAME_DEEZER.equals(resolverId)
|| TomahawkApp.PLUGINNAME_BEATSMUSIC.equals(resolverId)) {
priority = TomahawkRunnable.PRIORITY_IS_REPORTING_SUBSCRIPTION;
} else {
priority = TomahawkRunnable.PRIORITY_IS_REPORTING;
}
ThreadManager.get().execute(
new TomahawkRunnable(priority) {
@Override
public void run() {
if (query != null) {
boolean shouldReport = query.isFullTextQuery();
for (Result r : results) {
if (r != null) {
float trackScore = query.howSimilar(r);
float goalScore = query.isFullTextQuery()
? FULLTEXT_MINSCORE : MINSCORE;
if (trackScore > goalScore) {
Result before = query.getPreferredTrackResult();
query.addTrackResult(r, trackScore);
if (before != query.getPreferredTrackResult()) {
shouldReport = true;
}
}
}
}
if (shouldReport) {
ResultsEvent event = new ResultsEvent();
event.mQuery = query;
EventBus.getDefault().post(event);
}
}
}
}
);
}
public void lookupUrl(final String url) {
Log.d(TAG, "lookupUrl - looking up url: " + url);
if (!mLoadingPlugins.isEmpty()) {
mWaitingUrlLookups.add(url);
} else {
for (Resolver resolver : mResolvers) {
if (resolver instanceof ScriptResolver) {
ScriptResolver scriptResolver = (ScriptResolver) resolver;
scriptResolver.lookupUrl(url);
}
}
}
}
}