Merge pull request #3240 from ByteHamster/combined-search
WIP: Combined podcast search
This commit is contained in:
commit
865cb65470
|
@ -2,7 +2,6 @@ package de.danoeh.antennapod.adapter.itunes;
|
|||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
|
@ -13,18 +12,14 @@ import com.bumptech.glide.Glide;
|
|||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
import com.bumptech.glide.request.RequestOptions;
|
||||
import de.danoeh.antennapod.core.glide.ApGlideSettings;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import de.danoeh.antennapod.discovery.PodcastSearchResult;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import de.danoeh.antennapod.R;
|
||||
import de.danoeh.antennapod.activity.MainActivity;
|
||||
import de.mfietz.fyydlin.SearchHit;
|
||||
|
||||
public class ItunesAdapter extends ArrayAdapter<ItunesAdapter.Podcast> {
|
||||
public class ItunesAdapter extends ArrayAdapter<PodcastSearchResult> {
|
||||
/**
|
||||
* Related Context
|
||||
*/
|
||||
|
@ -33,7 +28,7 @@ public class ItunesAdapter extends ArrayAdapter<ItunesAdapter.Podcast> {
|
|||
/**
|
||||
* List holding the podcasts found in the search
|
||||
*/
|
||||
private final List<Podcast> data;
|
||||
private final List<PodcastSearchResult> data;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
|
@ -41,7 +36,7 @@ public class ItunesAdapter extends ArrayAdapter<ItunesAdapter.Podcast> {
|
|||
* @param context Related context
|
||||
* @param objects Search result
|
||||
*/
|
||||
public ItunesAdapter(Context context, List<Podcast> objects) {
|
||||
public ItunesAdapter(Context context, List<PodcastSearchResult> objects) {
|
||||
super(context, 0, objects);
|
||||
this.data = objects;
|
||||
this.context = context;
|
||||
|
@ -51,7 +46,7 @@ public class ItunesAdapter extends ArrayAdapter<ItunesAdapter.Podcast> {
|
|||
@Override
|
||||
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
|
||||
//Current podcast
|
||||
Podcast podcast = data.get(position);
|
||||
PodcastSearchResult podcast = data.get(position);
|
||||
|
||||
//ViewHolder
|
||||
PodcastViewHolder viewHolder;
|
||||
|
@ -93,75 +88,6 @@ public class ItunesAdapter extends ArrayAdapter<ItunesAdapter.Podcast> {
|
|||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an individual podcast on the iTunes Store.
|
||||
*/
|
||||
public static class Podcast { //TODO: Move this out eventually. Possibly to core.itunes.model
|
||||
|
||||
/**
|
||||
* The name of the podcast
|
||||
*/
|
||||
public final String title;
|
||||
|
||||
/**
|
||||
* URL of the podcast image
|
||||
*/
|
||||
@Nullable
|
||||
public final String imageUrl;
|
||||
/**
|
||||
* URL of the podcast feed
|
||||
*/
|
||||
@Nullable
|
||||
public final String feedUrl;
|
||||
|
||||
|
||||
private Podcast(String title, @Nullable String imageUrl, @Nullable String feedUrl) {
|
||||
this.title = title;
|
||||
this.imageUrl = imageUrl;
|
||||
this.feedUrl = feedUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a Podcast instance from a iTunes search result
|
||||
*
|
||||
* @param json object holding the podcast information
|
||||
* @throws JSONException
|
||||
*/
|
||||
public static Podcast fromSearch(JSONObject json) {
|
||||
String title = json.optString("collectionName", "");
|
||||
String imageUrl = json.optString("artworkUrl100", null);
|
||||
String feedUrl = json.optString("feedUrl", null);
|
||||
return new Podcast(title, imageUrl, feedUrl);
|
||||
}
|
||||
|
||||
public static Podcast fromSearch(SearchHit searchHit) {
|
||||
return new Podcast(searchHit.getTitle(), searchHit.getThumbImageURL(), searchHit.getXmlUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a Podcast instance from iTunes toplist entry
|
||||
*
|
||||
* @param json object holding the podcast information
|
||||
* @throws JSONException
|
||||
*/
|
||||
public static Podcast fromToplist(JSONObject json) throws JSONException {
|
||||
String title = json.getJSONObject("title").getString("label");
|
||||
String imageUrl = null;
|
||||
JSONArray images = json.getJSONArray("im:image");
|
||||
for(int i=0; imageUrl == null && i < images.length(); i++) {
|
||||
JSONObject image = images.getJSONObject(i);
|
||||
String height = image.getJSONObject("attributes").getString("height");
|
||||
if(Integer.parseInt(height) >= 100) {
|
||||
imageUrl = image.getString("label");
|
||||
}
|
||||
}
|
||||
String feedUrl = "https://itunes.apple.com/lookup?id=" +
|
||||
json.getJSONObject("id").getJSONObject("attributes").getString("im:id");
|
||||
return new Podcast(title, imageUrl, feedUrl);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* View holder object for the GridView
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
package de.danoeh.antennapod.discovery;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.SingleOnSubscribe;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
public class CombinedSearcher implements PodcastSearcher {
|
||||
private static final String TAG = "CombinedSearcher";
|
||||
|
||||
private final List<Pair<PodcastSearcher, Float>> searchProviders = new ArrayList<>();
|
||||
|
||||
public CombinedSearcher(Context context) {
|
||||
addProvider(new FyydPodcastSearcher(), 1.f);
|
||||
addProvider(new ItunesPodcastSearcher(context), 1.f);
|
||||
addProvider(new GpodnetPodcastSearcher(), 0.6f);
|
||||
}
|
||||
|
||||
private void addProvider(PodcastSearcher provider, float priority) {
|
||||
searchProviders.add(new Pair<>(provider, priority));
|
||||
}
|
||||
|
||||
public Single<List<PodcastSearchResult>> search(String query) {
|
||||
ArrayList<Disposable> disposables = new ArrayList<>();
|
||||
List<List<PodcastSearchResult>> singleResults = new ArrayList<>(Collections.nCopies(searchProviders.size(), null));
|
||||
CountDownLatch latch = new CountDownLatch(searchProviders.size());
|
||||
for (int i = 0; i < searchProviders.size(); i++) {
|
||||
Pair<PodcastSearcher, Float> searchProviderInfo = searchProviders.get(i);
|
||||
PodcastSearcher searcher = searchProviderInfo.first;
|
||||
final int index = i;
|
||||
disposables.add(searcher.search(query).subscribe(e -> {
|
||||
singleResults.set(index, e);
|
||||
latch.countDown();
|
||||
}, throwable -> {
|
||||
Log.d(TAG, Log.getStackTraceString(throwable));
|
||||
latch.countDown();
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
return Single.create((SingleOnSubscribe<List<PodcastSearchResult>>) subscriber -> {
|
||||
latch.await();
|
||||
List<PodcastSearchResult> results = weightSearchResults(singleResults);
|
||||
subscriber.onSuccess(results);
|
||||
})
|
||||
.doOnDispose(() -> {
|
||||
for (Disposable disposable : disposables) {
|
||||
disposable.dispose();
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
private List<PodcastSearchResult> weightSearchResults(List<List<PodcastSearchResult>> singleResults) {
|
||||
HashMap<String, Float> resultRanking = new HashMap<>();
|
||||
HashMap<String, PodcastSearchResult> urlToResult = new HashMap<>();
|
||||
for (int i = 0; i < singleResults.size(); i++) {
|
||||
float providerPriority = searchProviders.get(i).second;
|
||||
List<PodcastSearchResult> providerResults = singleResults.get(i);
|
||||
if (providerResults == null) {
|
||||
continue;
|
||||
}
|
||||
for (int position = 0; position < providerResults.size(); position++) {
|
||||
PodcastSearchResult result = providerResults.get(position);
|
||||
urlToResult.put(result.feedUrl, result);
|
||||
|
||||
float ranking = 0;
|
||||
if (resultRanking.containsKey(result.feedUrl)) {
|
||||
ranking = resultRanking.get(result.feedUrl);
|
||||
}
|
||||
ranking += 1.f / (position + 1.f);
|
||||
resultRanking.put(result.feedUrl, ranking * providerPriority);
|
||||
}
|
||||
}
|
||||
List<Map.Entry<String, Float>> sortedResults = new ArrayList<>(resultRanking.entrySet());
|
||||
Collections.sort(sortedResults, (o1, o2) -> Double.compare(o2.getValue(), o1.getValue()));
|
||||
|
||||
List<PodcastSearchResult> results = new ArrayList<>();
|
||||
for (Map.Entry<String, Float> res : sortedResults) {
|
||||
results.add(urlToResult.get(res.getKey()));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package de.danoeh.antennapod.discovery;
|
||||
|
||||
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
|
||||
import de.mfietz.fyydlin.FyydClient;
|
||||
import de.mfietz.fyydlin.FyydResponse;
|
||||
import de.mfietz.fyydlin.SearchHit;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.SingleOnSubscribe;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class FyydPodcastSearcher implements PodcastSearcher {
|
||||
private final FyydClient client = new FyydClient(AntennapodHttpClient.getHttpClient());
|
||||
|
||||
public Single<List<PodcastSearchResult>> search(String query) {
|
||||
return Single.create((SingleOnSubscribe<List<PodcastSearchResult>>) subscriber -> {
|
||||
FyydResponse response = client.searchPodcasts(query, 10)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.blockingGet();
|
||||
|
||||
ArrayList<PodcastSearchResult> searchResults = new ArrayList<>();
|
||||
|
||||
if (!response.getData().isEmpty()) {
|
||||
for (SearchHit searchHit : response.getData()) {
|
||||
PodcastSearchResult podcast = PodcastSearchResult.fromFyyd(searchHit);
|
||||
searchResults.add(podcast);
|
||||
}
|
||||
}
|
||||
|
||||
subscriber.onSuccess(searchResults);
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package de.danoeh.antennapod.discovery;
|
||||
|
||||
import de.danoeh.antennapod.core.gpoddernet.GpodnetService;
|
||||
import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException;
|
||||
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.SingleOnSubscribe;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class GpodnetPodcastSearcher implements PodcastSearcher {
|
||||
public Single<List<PodcastSearchResult>> search(String query) {
|
||||
return Single.create((SingleOnSubscribe<List<PodcastSearchResult>>) subscriber -> {
|
||||
GpodnetService service = null;
|
||||
try {
|
||||
service = new GpodnetService();
|
||||
List<GpodnetPodcast> gpodnetPodcasts = service.searchPodcasts(query, 0);
|
||||
List<PodcastSearchResult> results = new ArrayList<>();
|
||||
for (GpodnetPodcast podcast : gpodnetPodcasts) {
|
||||
results.add(PodcastSearchResult.fromGpodder(podcast));
|
||||
}
|
||||
subscriber.onSuccess(results);
|
||||
} catch (GpodnetServiceException e) {
|
||||
e.printStackTrace();
|
||||
subscriber.onError(e);
|
||||
} finally {
|
||||
if (service != null) {
|
||||
service.shutdown();
|
||||
}
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package de.danoeh.antennapod.discovery;
|
||||
|
||||
import android.content.Context;
|
||||
import de.danoeh.antennapod.R;
|
||||
import de.danoeh.antennapod.core.ClientConfig;
|
||||
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.SingleOnSubscribe;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ItunesPodcastSearcher implements PodcastSearcher {
|
||||
private static final String ITUNES_API_URL = "https://itunes.apple.com/search?media=podcast&term=%s";
|
||||
private final Context context;
|
||||
|
||||
public ItunesPodcastSearcher(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public Single<List<PodcastSearchResult>> search(String query) {
|
||||
return Single.create((SingleOnSubscribe<List<PodcastSearchResult>>) subscriber -> {
|
||||
String encodedQuery;
|
||||
try {
|
||||
encodedQuery = URLEncoder.encode(query, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// this won't ever be thrown
|
||||
encodedQuery = query;
|
||||
}
|
||||
|
||||
String formattedUrl = String.format(ITUNES_API_URL, encodedQuery);
|
||||
|
||||
OkHttpClient client = AntennapodHttpClient.getHttpClient();
|
||||
Request.Builder httpReq = new Request.Builder()
|
||||
.url(formattedUrl)
|
||||
.header("User-Agent", ClientConfig.USER_AGENT);
|
||||
List<PodcastSearchResult> podcasts = new ArrayList<>();
|
||||
try {
|
||||
Response response = client.newCall(httpReq.build()).execute();
|
||||
|
||||
if (response.isSuccessful()) {
|
||||
String resultString = response.body().string();
|
||||
JSONObject result = new JSONObject(resultString);
|
||||
JSONArray j = result.getJSONArray("results");
|
||||
|
||||
for (int i = 0; i < j.length(); i++) {
|
||||
JSONObject podcastJson = j.getJSONObject(i);
|
||||
PodcastSearchResult podcast = PodcastSearchResult.fromItunes(podcastJson);
|
||||
podcasts.add(podcast);
|
||||
}
|
||||
} else {
|
||||
String prefix = context.getString(R.string.error_msg_prefix);
|
||||
subscriber.onError(new IOException(prefix + response));
|
||||
}
|
||||
} catch (IOException | JSONException e) {
|
||||
subscriber.onError(e);
|
||||
}
|
||||
subscriber.onSuccess(podcasts);
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package de.danoeh.antennapod.discovery;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast;
|
||||
import de.mfietz.fyydlin.SearchHit;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class PodcastSearchResult {
|
||||
|
||||
/**
|
||||
* The name of the podcast
|
||||
*/
|
||||
public final String title;
|
||||
|
||||
/**
|
||||
* URL of the podcast image
|
||||
*/
|
||||
@Nullable
|
||||
public final String imageUrl;
|
||||
/**
|
||||
* URL of the podcast feed
|
||||
*/
|
||||
@Nullable
|
||||
public final String feedUrl;
|
||||
|
||||
|
||||
private PodcastSearchResult(String title, @Nullable String imageUrl, @Nullable String feedUrl) {
|
||||
this.title = title;
|
||||
this.imageUrl = imageUrl;
|
||||
this.feedUrl = feedUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a Podcast instance from a iTunes search result
|
||||
*
|
||||
* @param json object holding the podcast information
|
||||
* @throws JSONException
|
||||
*/
|
||||
public static PodcastSearchResult fromItunes(JSONObject json) {
|
||||
String title = json.optString("collectionName", "");
|
||||
String imageUrl = json.optString("artworkUrl100", null);
|
||||
String feedUrl = json.optString("feedUrl", null);
|
||||
return new PodcastSearchResult(title, imageUrl, feedUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a Podcast instance from iTunes toplist entry
|
||||
*
|
||||
* @param json object holding the podcast information
|
||||
* @throws JSONException
|
||||
*/
|
||||
public static PodcastSearchResult fromItunesToplist(JSONObject json) throws JSONException {
|
||||
String title = json.getJSONObject("title").getString("label");
|
||||
String imageUrl = null;
|
||||
JSONArray images = json.getJSONArray("im:image");
|
||||
for(int i=0; imageUrl == null && i < images.length(); i++) {
|
||||
JSONObject image = images.getJSONObject(i);
|
||||
String height = image.getJSONObject("attributes").getString("height");
|
||||
if(Integer.parseInt(height) >= 100) {
|
||||
imageUrl = image.getString("label");
|
||||
}
|
||||
}
|
||||
String feedUrl = "https://itunes.apple.com/lookup?id=" +
|
||||
json.getJSONObject("id").getJSONObject("attributes").getString("im:id");
|
||||
return new PodcastSearchResult(title, imageUrl, feedUrl);
|
||||
}
|
||||
|
||||
public static PodcastSearchResult fromFyyd(SearchHit searchHit) {
|
||||
return new PodcastSearchResult(searchHit.getTitle(), searchHit.getThumbImageURL(), searchHit.getXmlUrl());
|
||||
}
|
||||
|
||||
public static PodcastSearchResult fromGpodder(GpodnetPodcast searchHit) {
|
||||
return new PodcastSearchResult(searchHit.getTitle(), searchHit.getLogoUrl(), searchHit.getUrl());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package de.danoeh.antennapod.discovery;
|
||||
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import java.util.List;
|
||||
|
||||
public interface PodcastSearcher {
|
||||
Single<List<PodcastSearchResult>> search(String query);
|
||||
}
|
|
@ -42,6 +42,7 @@ public class AddFeedFragment extends Fragment {
|
|||
Button butSearchITunes = root.findViewById(R.id.butSearchItunes);
|
||||
Button butBrowserGpoddernet = root.findViewById(R.id.butBrowseGpoddernet);
|
||||
Button butSearchFyyd = root.findViewById(R.id.butSearchFyyd);
|
||||
Button butSearchCombined = root.findViewById(R.id.butSearchCombined);
|
||||
Button butOpmlImport = root.findViewById(R.id.butOpmlImport);
|
||||
Button butConfirm = root.findViewById(R.id.butConfirm);
|
||||
|
||||
|
@ -54,6 +55,8 @@ public class AddFeedFragment extends Fragment {
|
|||
|
||||
butSearchFyyd.setOnClickListener(v -> activity.loadChildFragment(new FyydSearchFragment()));
|
||||
|
||||
butSearchCombined.setOnClickListener(v -> activity.loadChildFragment(new CombinedSearchFragment()));
|
||||
|
||||
butOpmlImport.setOnClickListener(v -> startActivity(new Intent(getActivity(),
|
||||
OpmlImportFromPathActivity.class)));
|
||||
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
package de.danoeh.antennapod.fragment;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.view.MenuItemCompat;
|
||||
import android.support.v7.widget.SearchView;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.GridView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import de.danoeh.antennapod.R;
|
||||
import de.danoeh.antennapod.activity.OnlineFeedViewActivity;
|
||||
import de.danoeh.antennapod.adapter.itunes.ItunesAdapter;
|
||||
import de.danoeh.antennapod.discovery.CombinedSearcher;
|
||||
import de.danoeh.antennapod.discovery.PodcastSearchResult;
|
||||
import de.danoeh.antennapod.menuhandler.MenuItemUtils;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class CombinedSearchFragment extends Fragment {
|
||||
|
||||
private static final String TAG = "CombinedSearchFragment";
|
||||
|
||||
/**
|
||||
* Adapter responsible with the search results
|
||||
*/
|
||||
private ItunesAdapter adapter;
|
||||
private GridView gridView;
|
||||
private ProgressBar progressBar;
|
||||
private TextView txtvError;
|
||||
private Button butRetry;
|
||||
private TextView txtvEmpty;
|
||||
|
||||
/**
|
||||
* List of podcasts retreived from the search
|
||||
*/
|
||||
private List<PodcastSearchResult> searchResults = new ArrayList<>();
|
||||
private Disposable disposable;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public CombinedSearchFragment() {
|
||||
// Required empty public constructor
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
// Inflate the layout for this fragment
|
||||
View root = inflater.inflate(R.layout.fragment_itunes_search, container, false);
|
||||
gridView = root.findViewById(R.id.gridView);
|
||||
adapter = new ItunesAdapter(getActivity(), new ArrayList<>());
|
||||
gridView.setAdapter(adapter);
|
||||
|
||||
//Show information about the podcast when the list item is clicked
|
||||
gridView.setOnItemClickListener((parent, view1, position, id) -> {
|
||||
PodcastSearchResult podcast = searchResults.get(position);
|
||||
Intent intent = new Intent(getActivity(), OnlineFeedViewActivity.class);
|
||||
intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, podcast.feedUrl);
|
||||
intent.putExtra(OnlineFeedViewActivity.ARG_TITLE, podcast.title);
|
||||
startActivity(intent);
|
||||
});
|
||||
progressBar = root.findViewById(R.id.progressBar);
|
||||
txtvError = root.findViewById(R.id.txtvError);
|
||||
butRetry = root.findViewById(R.id.butRetry);
|
||||
txtvEmpty = root.findViewById(android.R.id.empty);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (disposable != null) {
|
||||
disposable.dispose();
|
||||
}
|
||||
adapter = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
inflater.inflate(R.menu.itunes_search, menu);
|
||||
MenuItem searchItem = menu.findItem(R.id.action_search);
|
||||
final SearchView sv = (SearchView) MenuItemCompat.getActionView(searchItem);
|
||||
MenuItemUtils.adjustTextColor(getActivity(), sv);
|
||||
sv.setQueryHint(getString(R.string.search_label));
|
||||
sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(String s) {
|
||||
sv.clearFocus();
|
||||
search(s);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextChange(String s) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
MenuItemCompat.setOnActionExpandListener(searchItem, new MenuItemCompat.OnActionExpandListener() {
|
||||
@Override
|
||||
public boolean onMenuItemActionExpand(MenuItem item) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemActionCollapse(MenuItem item) {
|
||||
getActivity().getSupportFragmentManager().popBackStack();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
MenuItemCompat.expandActionView(searchItem);
|
||||
}
|
||||
|
||||
private void search(String query) {
|
||||
if (disposable != null) {
|
||||
disposable.dispose();
|
||||
}
|
||||
|
||||
showOnlyProgressBar();
|
||||
|
||||
CombinedSearcher searcher = new CombinedSearcher(getContext());
|
||||
disposable = searcher.search(query).subscribe(result -> {
|
||||
searchResults = result;
|
||||
progressBar.setVisibility(View.GONE);
|
||||
|
||||
adapter.clear();
|
||||
adapter.addAll(searchResults);
|
||||
adapter.notifyDataSetInvalidated();
|
||||
gridView.setVisibility(!searchResults.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
txtvEmpty.setVisibility(searchResults.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
|
||||
}, error -> {
|
||||
Log.e(TAG, Log.getStackTraceString(error));
|
||||
progressBar.setVisibility(View.GONE);
|
||||
txtvError.setText(error.toString());
|
||||
txtvError.setVisibility(View.VISIBLE);
|
||||
butRetry.setOnClickListener(v -> search(query));
|
||||
butRetry.setVisibility(View.VISIBLE);
|
||||
});
|
||||
}
|
||||
|
||||
private void showOnlyProgressBar() {
|
||||
gridView.setVisibility(View.GONE);
|
||||
txtvError.setVisibility(View.GONE);
|
||||
butRetry.setVisibility(View.GONE);
|
||||
txtvEmpty.setVisibility(View.GONE);
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
|
@ -16,24 +16,16 @@ import android.widget.Button;
|
|||
import android.widget.GridView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.danoeh.antennapod.R;
|
||||
import de.danoeh.antennapod.activity.OnlineFeedViewActivity;
|
||||
import de.danoeh.antennapod.adapter.itunes.ItunesAdapter;
|
||||
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
|
||||
import de.danoeh.antennapod.discovery.FyydPodcastSearcher;
|
||||
import de.danoeh.antennapod.discovery.PodcastSearchResult;
|
||||
import de.danoeh.antennapod.menuhandler.MenuItemUtils;
|
||||
import de.mfietz.fyydlin.FyydClient;
|
||||
import de.mfietz.fyydlin.FyydResponse;
|
||||
import de.mfietz.fyydlin.SearchHit;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static de.danoeh.antennapod.adapter.itunes.ItunesAdapter.Podcast;
|
||||
import static java.util.Collections.emptyList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class FyydSearchFragment extends Fragment {
|
||||
|
||||
|
@ -49,12 +41,10 @@ public class FyydSearchFragment extends Fragment {
|
|||
private Button butRetry;
|
||||
private TextView txtvEmpty;
|
||||
|
||||
private final FyydClient client = new FyydClient(AntennapodHttpClient.getHttpClient());
|
||||
|
||||
/**
|
||||
* List of podcasts retreived from the search
|
||||
*/
|
||||
private List<Podcast> searchResults;
|
||||
private List<PodcastSearchResult> searchResults;
|
||||
private Disposable disposable;
|
||||
|
||||
/**
|
||||
|
@ -81,7 +71,7 @@ public class FyydSearchFragment extends Fragment {
|
|||
|
||||
//Show information about the podcast when the list item is clicked
|
||||
gridView.setOnItemClickListener((parent, view1, position, id) -> {
|
||||
Podcast podcast = searchResults.get(position);
|
||||
PodcastSearchResult podcast = searchResults.get(position);
|
||||
Intent intent = new Intent(getActivity(), OnlineFeedViewActivity.class);
|
||||
intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, podcast.feedUrl);
|
||||
intent.putExtra(OnlineFeedViewActivity.ARG_TITLE, podcast.title);
|
||||
|
@ -145,20 +135,26 @@ public class FyydSearchFragment extends Fragment {
|
|||
disposable.dispose();
|
||||
}
|
||||
showOnlyProgressBar();
|
||||
disposable = client.searchPodcasts(query, 10)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(result -> {
|
||||
progressBar.setVisibility(View.GONE);
|
||||
processSearchResult(result);
|
||||
}, error -> {
|
||||
Log.e(TAG, Log.getStackTraceString(error));
|
||||
progressBar.setVisibility(View.GONE);
|
||||
txtvError.setText(error.toString());
|
||||
txtvError.setVisibility(View.VISIBLE);
|
||||
butRetry.setOnClickListener(v -> search(query));
|
||||
butRetry.setVisibility(View.VISIBLE);
|
||||
});
|
||||
|
||||
FyydPodcastSearcher searcher = new FyydPodcastSearcher();
|
||||
disposable = searcher.search(query).subscribe(result -> {
|
||||
searchResults = result;
|
||||
progressBar.setVisibility(View.GONE);
|
||||
|
||||
adapter.clear();
|
||||
adapter.addAll(searchResults);
|
||||
adapter.notifyDataSetInvalidated();
|
||||
gridView.setVisibility(!searchResults.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
txtvEmpty.setVisibility(searchResults.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
|
||||
}, error -> {
|
||||
Log.e(TAG, Log.getStackTraceString(error));
|
||||
progressBar.setVisibility(View.GONE);
|
||||
txtvError.setText(error.toString());
|
||||
txtvError.setVisibility(View.VISIBLE);
|
||||
butRetry.setOnClickListener(v -> search(query));
|
||||
butRetry.setVisibility(View.VISIBLE);
|
||||
});
|
||||
}
|
||||
|
||||
private void showOnlyProgressBar() {
|
||||
|
@ -168,25 +164,4 @@ public class FyydSearchFragment extends Fragment {
|
|||
txtvEmpty.setVisibility(View.GONE);
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private void processSearchResult(FyydResponse response) {
|
||||
adapter.clear();
|
||||
if (!response.getData().isEmpty()) {
|
||||
adapter.clear();
|
||||
searchResults = new ArrayList<>();
|
||||
for (SearchHit searchHit : response.getData()) {
|
||||
Podcast podcast = Podcast.fromSearch(searchHit);
|
||||
searchResults.add(podcast);
|
||||
}
|
||||
} else {
|
||||
searchResults = emptyList();
|
||||
}
|
||||
for(Podcast podcast : searchResults) {
|
||||
adapter.add(podcast);
|
||||
}
|
||||
adapter.notifyDataSetInvalidated();
|
||||
gridView.setVisibility(!searchResults.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
txtvEmpty.setVisibility(searchResults.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -20,13 +20,13 @@ import android.widget.TextView;
|
|||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
|
||||
import de.danoeh.antennapod.discovery.ItunesPodcastSearcher;
|
||||
import de.danoeh.antennapod.discovery.PodcastSearchResult;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
@ -46,15 +46,11 @@ import okhttp3.OkHttpClient;
|
|||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
import static de.danoeh.antennapod.adapter.itunes.ItunesAdapter.Podcast;
|
||||
|
||||
//Searches iTunes store for given string and displays results in a list
|
||||
public class ItunesSearchFragment extends Fragment {
|
||||
|
||||
private static final String TAG = "ItunesSearchFragment";
|
||||
|
||||
private static final String API_URL = "https://itunes.apple.com/search?media=podcast&term=%s";
|
||||
|
||||
|
||||
/**
|
||||
* Adapter responsible with the search results
|
||||
|
@ -69,21 +65,21 @@ public class ItunesSearchFragment extends Fragment {
|
|||
/**
|
||||
* List of podcasts retreived from the search
|
||||
*/
|
||||
private List<Podcast> searchResults;
|
||||
private List<Podcast> topList;
|
||||
private List<PodcastSearchResult> searchResults;
|
||||
private List<PodcastSearchResult> topList;
|
||||
private Disposable disposable;
|
||||
|
||||
/**
|
||||
* Replace adapter data with provided search results from SearchTask.
|
||||
* @param result List of Podcast objects containing search results
|
||||
*/
|
||||
private void updateData(List<Podcast> result) {
|
||||
private void updateData(List<PodcastSearchResult> result) {
|
||||
this.searchResults = result;
|
||||
adapter.clear();
|
||||
if (result != null && result.size() > 0) {
|
||||
gridView.setVisibility(View.VISIBLE);
|
||||
txtvEmpty.setVisibility(View.GONE);
|
||||
for (Podcast p : result) {
|
||||
for (PodcastSearchResult p : result) {
|
||||
adapter.add(p);
|
||||
}
|
||||
adapter.notifyDataSetInvalidated();
|
||||
|
@ -117,7 +113,7 @@ public class ItunesSearchFragment extends Fragment {
|
|||
|
||||
//Show information about the podcast when the list item is clicked
|
||||
gridView.setOnItemClickListener((parent, view1, position, id) -> {
|
||||
Podcast podcast = searchResults.get(position);
|
||||
PodcastSearchResult podcast = searchResults.get(position);
|
||||
if(podcast.feedUrl == null) {
|
||||
return;
|
||||
}
|
||||
|
@ -239,7 +235,7 @@ public class ItunesSearchFragment extends Fragment {
|
|||
butRetry.setVisibility(View.GONE);
|
||||
txtvEmpty.setVisibility(View.GONE);
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
disposable = Single.create((SingleOnSubscribe<List<Podcast>>) emitter -> {
|
||||
disposable = Single.create((SingleOnSubscribe<List<PodcastSearchResult>>) emitter -> {
|
||||
String lang = Locale.getDefault().getLanguage();
|
||||
OkHttpClient client = AntennapodHttpClient.getHttpClient();
|
||||
String feedString;
|
||||
|
@ -249,7 +245,7 @@ public class ItunesSearchFragment extends Fragment {
|
|||
} catch (IOException e) {
|
||||
feedString = getTopListFeed(client, "us");
|
||||
}
|
||||
List<Podcast> podcasts = parseFeed(feedString);
|
||||
List<PodcastSearchResult> podcasts = parseFeed(feedString);
|
||||
emitter.onSuccess(podcasts);
|
||||
} catch (IOException | JSONException e) {
|
||||
if (!disposable.isDisposed()) {
|
||||
|
@ -288,15 +284,15 @@ public class ItunesSearchFragment extends Fragment {
|
|||
}
|
||||
}
|
||||
|
||||
private List<Podcast> parseFeed(String jsonString) throws JSONException {
|
||||
private List<PodcastSearchResult> parseFeed(String jsonString) throws JSONException {
|
||||
JSONObject result = new JSONObject(jsonString);
|
||||
JSONObject feed = result.getJSONObject("feed");
|
||||
JSONArray entries = feed.getJSONArray("entry");
|
||||
|
||||
List<Podcast> results = new ArrayList<>();
|
||||
List<PodcastSearchResult> results = new ArrayList<>();
|
||||
for (int i=0; i < entries.length(); i++) {
|
||||
JSONObject json = entries.getJSONObject(i);
|
||||
results.add(Podcast.fromToplist(json));
|
||||
results.add(PodcastSearchResult.fromItunesToplist(json));
|
||||
}
|
||||
|
||||
return results;
|
||||
|
@ -311,60 +307,19 @@ public class ItunesSearchFragment extends Fragment {
|
|||
butRetry.setVisibility(View.GONE);
|
||||
txtvEmpty.setVisibility(View.GONE);
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
disposable = Single.create((SingleOnSubscribe<List<Podcast>>) subscriber -> {
|
||||
String encodedQuery = null;
|
||||
try {
|
||||
encodedQuery = URLEncoder.encode(query, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// this won't ever be thrown
|
||||
}
|
||||
if (encodedQuery == null) {
|
||||
encodedQuery = query; // failsafe
|
||||
}
|
||||
|
||||
String formattedUrl = String.format(API_URL, encodedQuery);
|
||||
|
||||
OkHttpClient client = AntennapodHttpClient.getHttpClient();
|
||||
Request.Builder httpReq = new Request.Builder()
|
||||
.url(formattedUrl)
|
||||
.header("User-Agent", ClientConfig.USER_AGENT);
|
||||
List<Podcast> podcasts = new ArrayList<>();
|
||||
try {
|
||||
Response response = client.newCall(httpReq.build()).execute();
|
||||
|
||||
if(response.isSuccessful()) {
|
||||
String resultString = response.body().string();
|
||||
JSONObject result = new JSONObject(resultString);
|
||||
JSONArray j = result.getJSONArray("results");
|
||||
|
||||
for (int i = 0; i < j.length(); i++) {
|
||||
JSONObject podcastJson = j.getJSONObject(i);
|
||||
Podcast podcast = Podcast.fromSearch(podcastJson);
|
||||
podcasts.add(podcast);
|
||||
}
|
||||
}
|
||||
else {
|
||||
String prefix = getString(R.string.error_msg_prefix);
|
||||
subscriber.onError(new IOException(prefix + response));
|
||||
}
|
||||
} catch (IOException | JSONException e) {
|
||||
subscriber.onError(e);
|
||||
}
|
||||
subscriber.onSuccess(podcasts);
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(podcasts -> {
|
||||
progressBar.setVisibility(View.GONE);
|
||||
updateData(podcasts);
|
||||
}, error -> {
|
||||
Log.e(TAG, Log.getStackTraceString(error));
|
||||
progressBar.setVisibility(View.GONE);
|
||||
txtvError.setText(error.toString());
|
||||
txtvError.setVisibility(View.VISIBLE);
|
||||
butRetry.setOnClickListener(v -> search(query));
|
||||
butRetry.setVisibility(View.VISIBLE);
|
||||
});
|
||||
ItunesPodcastSearcher searcher = new ItunesPodcastSearcher(getContext());
|
||||
disposable = searcher.search(query).subscribe(podcasts -> {
|
||||
progressBar.setVisibility(View.GONE);
|
||||
updateData(podcasts);
|
||||
}, error -> {
|
||||
Log.e(TAG, Log.getStackTraceString(error));
|
||||
progressBar.setVisibility(View.GONE);
|
||||
txtvError.setText(error.toString());
|
||||
txtvError.setVisibility(View.VISIBLE);
|
||||
butRetry.setOnClickListener(v -> search(query));
|
||||
butRetry.setVisibility(View.VISIBLE);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -29,10 +29,16 @@
|
|||
android:textSize="@dimen/text_size_medium"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/butSearchItunes"
|
||||
android:id="@+id/butSearchCombined"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="Search all providers"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/butSearchItunes"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/search_itunes_label"/>
|
||||
|
||||
<Button
|
||||
|
|
Loading…
Reference in New Issue