Combined podcast search

This commit is contained in:
ByteHamster 2019-06-26 08:27:19 +02:00
parent 1580e93b94
commit 2d91292937
11 changed files with 566 additions and 200 deletions

View File

@ -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
*/

View File

@ -0,0 +1,45 @@
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.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.List;
public class FyydPodcastSearcher implements PodcastSearcher {
private final String query;
private final FyydClient client = new FyydClient(AntennapodHttpClient.getHttpClient());
public FyydPodcastSearcher(String query) {
this.query = query;
}
public Disposable search(Consumer<? super List<PodcastSearchResult>> successHandler, Consumer<? super Throwable> errorHandler) {
return client.searchPodcasts(query, 10)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
ArrayList<PodcastSearchResult> results = processSearchResult(result);
successHandler.accept(results);
}, errorHandler);
}
private ArrayList<PodcastSearchResult> processSearchResult(FyydResponse response) {
ArrayList<PodcastSearchResult> searchResults = new ArrayList<>();
if (!response.getData().isEmpty()) {
for (SearchHit searchHit : response.getData()) {
PodcastSearchResult podcast = PodcastSearchResult.fromFyyd(searchHit);
searchResults.add(podcast);
}
}
return searchResults;
}
}

View File

@ -0,0 +1,47 @@
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.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.List;
public class GpodnetPodcastSearcher implements PodcastSearcher {
private final String query;
public GpodnetPodcastSearcher(String query) {
this.query = query;
}
public Disposable search(Consumer<? super List<PodcastSearchResult>> successHandler, Consumer<? super Throwable> errorHandler) {
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())
.subscribe(successHandler, errorHandler);
}
}

View File

@ -0,0 +1,81 @@
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.disposables.Disposable;
import io.reactivex.functions.Consumer;
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;
private final String query;
public ItunesPodcastSearcher(Context context, String query) {
this.context = context;
this.query = query;
}
public Disposable search(Consumer<? super List<PodcastSearchResult>> successHandler, Consumer<? super Throwable> errorHandler) {
return Single.create((SingleOnSubscribe<List<PodcastSearchResult>>) 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(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())
.subscribe(successHandler, errorHandler);
}
}

View File

@ -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());
}
}

View File

@ -0,0 +1,9 @@
package de.danoeh.antennapod.discovery;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import java.util.List;
public interface PodcastSearcher {
Disposable search(Consumer<? super List<PodcastSearchResult>> successHandler, Consumer<? super Throwable> errorHandler);
}

View File

@ -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)));

View File

@ -0,0 +1,242 @@
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 android.widget.Toast;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.OnlineFeedViewActivity;
import de.danoeh.antennapod.adapter.itunes.ItunesAdapter;
import de.danoeh.antennapod.core.gpoddernet.GpodnetService;
import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast;
import de.danoeh.antennapod.discovery.FyydPodcastSearcher;
import de.danoeh.antennapod.discovery.GpodnetPodcastSearcher;
import de.danoeh.antennapod.discovery.ItunesPodcastSearcher;
import de.danoeh.antennapod.discovery.PodcastSearchResult;
import de.danoeh.antennapod.discovery.PodcastSearcher;
import de.danoeh.antennapod.menuhandler.MenuItemUtils;
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.io.Serializable;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
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 List<Disposable> disposables = new ArrayList<>();
/**
* 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();
disposeAll();
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) {
disposeAll();
showOnlyProgressBar();
List<PodcastSearcher> searchProviders = new ArrayList<>();
searchProviders.add(new FyydPodcastSearcher(query));
searchProviders.add(new ItunesPodcastSearcher(getContext(), query));
searchProviders.add(new GpodnetPodcastSearcher(query));
List<List<PodcastSearchResult>> singleResults = new ArrayList<>();
CountDownLatch latch = new CountDownLatch(searchProviders.size());
for (PodcastSearcher searchProvider : searchProviders) {
disposables.add(searchProvider.search(e -> {
singleResults.add(e);
latch.countDown();
}, throwable -> {
Toast.makeText(getContext(), throwable.getLocalizedMessage(), Toast.LENGTH_LONG).show();
latch.countDown();
}
));
}
disposables.add(Single.create((SingleOnSubscribe<List<PodcastSearchResult>>) subscriber -> {
latch.await();
HashMap<String, Float> resultRanking = new HashMap<>();
HashMap<String, PodcastSearchResult> urlToResult = new HashMap<>();
for (List<PodcastSearchResult> providerResults : singleResults) {
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);
}
}
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()));
}
subscriber.onSuccess(results);
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.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 disposeAll() {
for (Disposable d : disposables) {
d.dispose();
}
disposables.clear();
}
private void showOnlyProgressBar() {
gridView.setVisibility(View.GONE);
txtvError.setVisibility(View.GONE);
butRetry.setVisibility(View.GONE);
txtvEmpty.setVisibility(View.GONE);
progressBar.setVisibility(View.VISIBLE);
}
public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K, V>> comparingByValue() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getValue().compareTo(c2.getValue());
}
}

View File

@ -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(query);
disposable = searcher.search(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);
}
}

View File

@ -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(), query);
disposable = searcher.search(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);
});
}
}

View File

@ -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