Merge pull request #3240 from ByteHamster/combined-search

WIP: Combined podcast search
This commit is contained in:
H. Lehmann 2019-07-22 19:05:21 +02:00 committed by GitHub
commit 865cb65470
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 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.content.Context;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
@ -13,18 +12,14 @@ import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.RequestOptions;
import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.discovery.PodcastSearchResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.List; import java.util.List;
import de.danoeh.antennapod.R; import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity; 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 * Related Context
*/ */
@ -33,7 +28,7 @@ public class ItunesAdapter extends ArrayAdapter<ItunesAdapter.Podcast> {
/** /**
* List holding the podcasts found in the search * List holding the podcasts found in the search
*/ */
private final List<Podcast> data; private final List<PodcastSearchResult> data;
/** /**
* Constructor. * Constructor.
@ -41,7 +36,7 @@ public class ItunesAdapter extends ArrayAdapter<ItunesAdapter.Podcast> {
* @param context Related context * @param context Related context
* @param objects Search result * @param objects Search result
*/ */
public ItunesAdapter(Context context, List<Podcast> objects) { public ItunesAdapter(Context context, List<PodcastSearchResult> objects) {
super(context, 0, objects); super(context, 0, objects);
this.data = objects; this.data = objects;
this.context = context; this.context = context;
@ -51,7 +46,7 @@ public class ItunesAdapter extends ArrayAdapter<ItunesAdapter.Podcast> {
@Override @Override
public View getView(int position, View convertView, @NonNull ViewGroup parent) { public View getView(int position, View convertView, @NonNull ViewGroup parent) {
//Current podcast //Current podcast
Podcast podcast = data.get(position); PodcastSearchResult podcast = data.get(position);
//ViewHolder //ViewHolder
PodcastViewHolder viewHolder; PodcastViewHolder viewHolder;
@ -93,75 +88,6 @@ public class ItunesAdapter extends ArrayAdapter<ItunesAdapter.Podcast> {
return view; 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 holder object for the GridView
*/ */

View File

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

View File

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

View File

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

View File

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

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,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);
}

View File

@ -42,6 +42,7 @@ public class AddFeedFragment extends Fragment {
Button butSearchITunes = root.findViewById(R.id.butSearchItunes); Button butSearchITunes = root.findViewById(R.id.butSearchItunes);
Button butBrowserGpoddernet = root.findViewById(R.id.butBrowseGpoddernet); Button butBrowserGpoddernet = root.findViewById(R.id.butBrowseGpoddernet);
Button butSearchFyyd = root.findViewById(R.id.butSearchFyyd); Button butSearchFyyd = root.findViewById(R.id.butSearchFyyd);
Button butSearchCombined = root.findViewById(R.id.butSearchCombined);
Button butOpmlImport = root.findViewById(R.id.butOpmlImport); Button butOpmlImport = root.findViewById(R.id.butOpmlImport);
Button butConfirm = root.findViewById(R.id.butConfirm); Button butConfirm = root.findViewById(R.id.butConfirm);
@ -54,6 +55,8 @@ public class AddFeedFragment extends Fragment {
butSearchFyyd.setOnClickListener(v -> activity.loadChildFragment(new FyydSearchFragment())); butSearchFyyd.setOnClickListener(v -> activity.loadChildFragment(new FyydSearchFragment()));
butSearchCombined.setOnClickListener(v -> activity.loadChildFragment(new CombinedSearchFragment()));
butOpmlImport.setOnClickListener(v -> startActivity(new Intent(getActivity(), butOpmlImport.setOnClickListener(v -> startActivity(new Intent(getActivity(),
OpmlImportFromPathActivity.class))); OpmlImportFromPathActivity.class)));

View File

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

View File

@ -16,24 +16,16 @@ import android.widget.Button;
import android.widget.GridView; import android.widget.GridView;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
import de.danoeh.antennapod.R; import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.OnlineFeedViewActivity; import de.danoeh.antennapod.activity.OnlineFeedViewActivity;
import de.danoeh.antennapod.adapter.itunes.ItunesAdapter; 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.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.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import static de.danoeh.antennapod.adapter.itunes.ItunesAdapter.Podcast; import java.util.ArrayList;
import static java.util.Collections.emptyList; import java.util.List;
public class FyydSearchFragment extends Fragment { public class FyydSearchFragment extends Fragment {
@ -49,12 +41,10 @@ public class FyydSearchFragment extends Fragment {
private Button butRetry; private Button butRetry;
private TextView txtvEmpty; private TextView txtvEmpty;
private final FyydClient client = new FyydClient(AntennapodHttpClient.getHttpClient());
/** /**
* List of podcasts retreived from the search * List of podcasts retreived from the search
*/ */
private List<Podcast> searchResults; private List<PodcastSearchResult> searchResults;
private Disposable disposable; private Disposable disposable;
/** /**
@ -81,7 +71,7 @@ public class FyydSearchFragment extends Fragment {
//Show information about the podcast when the list item is clicked //Show information about the podcast when the list item is clicked
gridView.setOnItemClickListener((parent, view1, position, id) -> { gridView.setOnItemClickListener((parent, view1, position, id) -> {
Podcast podcast = searchResults.get(position); PodcastSearchResult podcast = searchResults.get(position);
Intent intent = new Intent(getActivity(), OnlineFeedViewActivity.class); Intent intent = new Intent(getActivity(), OnlineFeedViewActivity.class);
intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, podcast.feedUrl); intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, podcast.feedUrl);
intent.putExtra(OnlineFeedViewActivity.ARG_TITLE, podcast.title); intent.putExtra(OnlineFeedViewActivity.ARG_TITLE, podcast.title);
@ -145,20 +135,26 @@ public class FyydSearchFragment extends Fragment {
disposable.dispose(); disposable.dispose();
} }
showOnlyProgressBar(); showOnlyProgressBar();
disposable = client.searchPodcasts(query, 10)
.subscribeOn(Schedulers.io()) FyydPodcastSearcher searcher = new FyydPodcastSearcher();
.observeOn(AndroidSchedulers.mainThread()) disposable = searcher.search(query).subscribe(result -> {
.subscribe(result -> { searchResults = result;
progressBar.setVisibility(View.GONE); progressBar.setVisibility(View.GONE);
processSearchResult(result);
}, error -> { adapter.clear();
Log.e(TAG, Log.getStackTraceString(error)); adapter.addAll(searchResults);
progressBar.setVisibility(View.GONE); adapter.notifyDataSetInvalidated();
txtvError.setText(error.toString()); gridView.setVisibility(!searchResults.isEmpty() ? View.VISIBLE : View.GONE);
txtvError.setVisibility(View.VISIBLE); txtvEmpty.setVisibility(searchResults.isEmpty() ? View.VISIBLE : View.GONE);
butRetry.setOnClickListener(v -> search(query));
butRetry.setVisibility(View.VISIBLE); }, 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() { private void showOnlyProgressBar() {
@ -168,25 +164,4 @@ public class FyydSearchFragment extends Fragment {
txtvEmpty.setVisibility(View.GONE); txtvEmpty.setVisibility(View.GONE);
progressBar.setVisibility(View.VISIBLE); 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 com.afollestad.materialdialogs.MaterialDialog;
import de.danoeh.antennapod.discovery.ItunesPodcastSearcher;
import de.danoeh.antennapod.discovery.PodcastSearchResult;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@ -46,15 +46,11 @@ import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import static de.danoeh.antennapod.adapter.itunes.ItunesAdapter.Podcast;
//Searches iTunes store for given string and displays results in a list //Searches iTunes store for given string and displays results in a list
public class ItunesSearchFragment extends Fragment { public class ItunesSearchFragment extends Fragment {
private static final String TAG = "ItunesSearchFragment"; 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 * Adapter responsible with the search results
@ -69,21 +65,21 @@ public class ItunesSearchFragment extends Fragment {
/** /**
* List of podcasts retreived from the search * List of podcasts retreived from the search
*/ */
private List<Podcast> searchResults; private List<PodcastSearchResult> searchResults;
private List<Podcast> topList; private List<PodcastSearchResult> topList;
private Disposable disposable; private Disposable disposable;
/** /**
* Replace adapter data with provided search results from SearchTask. * Replace adapter data with provided search results from SearchTask.
* @param result List of Podcast objects containing search results * @param result List of Podcast objects containing search results
*/ */
private void updateData(List<Podcast> result) { private void updateData(List<PodcastSearchResult> result) {
this.searchResults = result; this.searchResults = result;
adapter.clear(); adapter.clear();
if (result != null && result.size() > 0) { if (result != null && result.size() > 0) {
gridView.setVisibility(View.VISIBLE); gridView.setVisibility(View.VISIBLE);
txtvEmpty.setVisibility(View.GONE); txtvEmpty.setVisibility(View.GONE);
for (Podcast p : result) { for (PodcastSearchResult p : result) {
adapter.add(p); adapter.add(p);
} }
adapter.notifyDataSetInvalidated(); adapter.notifyDataSetInvalidated();
@ -117,7 +113,7 @@ public class ItunesSearchFragment extends Fragment {
//Show information about the podcast when the list item is clicked //Show information about the podcast when the list item is clicked
gridView.setOnItemClickListener((parent, view1, position, id) -> { gridView.setOnItemClickListener((parent, view1, position, id) -> {
Podcast podcast = searchResults.get(position); PodcastSearchResult podcast = searchResults.get(position);
if(podcast.feedUrl == null) { if(podcast.feedUrl == null) {
return; return;
} }
@ -239,7 +235,7 @@ public class ItunesSearchFragment extends Fragment {
butRetry.setVisibility(View.GONE); butRetry.setVisibility(View.GONE);
txtvEmpty.setVisibility(View.GONE); txtvEmpty.setVisibility(View.GONE);
progressBar.setVisibility(View.VISIBLE); progressBar.setVisibility(View.VISIBLE);
disposable = Single.create((SingleOnSubscribe<List<Podcast>>) emitter -> { disposable = Single.create((SingleOnSubscribe<List<PodcastSearchResult>>) emitter -> {
String lang = Locale.getDefault().getLanguage(); String lang = Locale.getDefault().getLanguage();
OkHttpClient client = AntennapodHttpClient.getHttpClient(); OkHttpClient client = AntennapodHttpClient.getHttpClient();
String feedString; String feedString;
@ -249,7 +245,7 @@ public class ItunesSearchFragment extends Fragment {
} catch (IOException e) { } catch (IOException e) {
feedString = getTopListFeed(client, "us"); feedString = getTopListFeed(client, "us");
} }
List<Podcast> podcasts = parseFeed(feedString); List<PodcastSearchResult> podcasts = parseFeed(feedString);
emitter.onSuccess(podcasts); emitter.onSuccess(podcasts);
} catch (IOException | JSONException e) { } catch (IOException | JSONException e) {
if (!disposable.isDisposed()) { 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 result = new JSONObject(jsonString);
JSONObject feed = result.getJSONObject("feed"); JSONObject feed = result.getJSONObject("feed");
JSONArray entries = feed.getJSONArray("entry"); JSONArray entries = feed.getJSONArray("entry");
List<Podcast> results = new ArrayList<>(); List<PodcastSearchResult> results = new ArrayList<>();
for (int i=0; i < entries.length(); i++) { for (int i=0; i < entries.length(); i++) {
JSONObject json = entries.getJSONObject(i); JSONObject json = entries.getJSONObject(i);
results.add(Podcast.fromToplist(json)); results.add(PodcastSearchResult.fromItunesToplist(json));
} }
return results; return results;
@ -311,60 +307,19 @@ public class ItunesSearchFragment extends Fragment {
butRetry.setVisibility(View.GONE); butRetry.setVisibility(View.GONE);
txtvEmpty.setVisibility(View.GONE); txtvEmpty.setVisibility(View.GONE);
progressBar.setVisibility(View.VISIBLE); 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); ItunesPodcastSearcher searcher = new ItunesPodcastSearcher(getContext());
disposable = searcher.search(query).subscribe(podcasts -> {
OkHttpClient client = AntennapodHttpClient.getHttpClient(); progressBar.setVisibility(View.GONE);
Request.Builder httpReq = new Request.Builder() updateData(podcasts);
.url(formattedUrl) }, error -> {
.header("User-Agent", ClientConfig.USER_AGENT); Log.e(TAG, Log.getStackTraceString(error));
List<Podcast> podcasts = new ArrayList<>(); progressBar.setVisibility(View.GONE);
try { txtvError.setText(error.toString());
Response response = client.newCall(httpReq.build()).execute(); txtvError.setVisibility(View.VISIBLE);
butRetry.setOnClickListener(v -> search(query));
if(response.isSuccessful()) { butRetry.setVisibility(View.VISIBLE);
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);
});
} }
} }

View File

@ -29,10 +29,16 @@
android:textSize="@dimen/text_size_medium"/> android:textSize="@dimen/text_size_medium"/>
<Button <Button
android:id="@+id/butSearchItunes" android:id="@+id/butSearchCombined"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" 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"/> android:text="@string/search_itunes_label"/>
<Button <Button