From 2d912929372eed3fa290e5e778028d9d3b046969 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Wed, 26 Jun 2019 08:27:19 +0200 Subject: [PATCH] Combined podcast search --- .../adapter/itunes/ItunesAdapter.java | 84 +----- .../discovery/FyydPodcastSearcher.java | 45 ++++ .../discovery/GpodnetPodcastSearcher.java | 47 ++++ .../discovery/ItunesPodcastSearcher.java | 81 ++++++ .../discovery/PodcastSearchResult.java | 77 ++++++ .../antennapod/discovery/PodcastSearcher.java | 9 + .../antennapod/fragment/AddFeedFragment.java | 3 + .../fragment/CombinedSearchFragment.java | 242 ++++++++++++++++++ .../fragment/FyydSearchFragment.java | 77 ++---- .../fragment/ItunesSearchFragment.java | 93 ++----- app/src/main/res/layout/addfeed.xml | 8 +- 11 files changed, 566 insertions(+), 200 deletions(-) create mode 100644 app/src/main/java/de/danoeh/antennapod/discovery/FyydPodcastSearcher.java create mode 100644 app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java create mode 100644 app/src/main/java/de/danoeh/antennapod/discovery/ItunesPodcastSearcher.java create mode 100644 app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearchResult.java create mode 100644 app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcher.java create mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/CombinedSearchFragment.java diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/itunes/ItunesAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/itunes/ItunesAdapter.java index 2cf17c85f..f5213e4ab 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/itunes/ItunesAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/itunes/ItunesAdapter.java @@ -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 { +public class ItunesAdapter extends ArrayAdapter { /** * Related Context */ @@ -33,7 +28,7 @@ public class ItunesAdapter extends ArrayAdapter { /** * List holding the podcasts found in the search */ - private final List data; + private final List data; /** * Constructor. @@ -41,7 +36,7 @@ public class ItunesAdapter extends ArrayAdapter { * @param context Related context * @param objects Search result */ - public ItunesAdapter(Context context, List objects) { + public ItunesAdapter(Context context, List objects) { super(context, 0, objects); this.data = objects; this.context = context; @@ -51,7 +46,7 @@ public class ItunesAdapter extends ArrayAdapter { @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 { 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 */ diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/FyydPodcastSearcher.java b/app/src/main/java/de/danoeh/antennapod/discovery/FyydPodcastSearcher.java new file mode 100644 index 000000000..af0597a07 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/discovery/FyydPodcastSearcher.java @@ -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> successHandler, Consumer errorHandler) { + return client.searchPodcasts(query, 10) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(result -> { + ArrayList results = processSearchResult(result); + successHandler.accept(results); + }, errorHandler); + } + + private ArrayList processSearchResult(FyydResponse response) { + ArrayList searchResults = new ArrayList<>(); + + if (!response.getData().isEmpty()) { + for (SearchHit searchHit : response.getData()) { + PodcastSearchResult podcast = PodcastSearchResult.fromFyyd(searchHit); + searchResults.add(podcast); + } + } + + return searchResults; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java b/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java new file mode 100644 index 000000000..530f234fd --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java @@ -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> successHandler, Consumer errorHandler) { + return Single.create((SingleOnSubscribe>) subscriber -> { + GpodnetService service = null; + try { + service = new GpodnetService(); + List gpodnetPodcasts = service.searchPodcasts(query, 0); + List 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); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/ItunesPodcastSearcher.java b/app/src/main/java/de/danoeh/antennapod/discovery/ItunesPodcastSearcher.java new file mode 100644 index 000000000..5d5837e18 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/discovery/ItunesPodcastSearcher.java @@ -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> successHandler, Consumer errorHandler) { + return Single.create((SingleOnSubscribe>) 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 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); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearchResult.java b/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearchResult.java new file mode 100644 index 000000000..39c5b8dee --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearchResult.java @@ -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()); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcher.java b/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcher.java new file mode 100644 index 000000000..2a4937e28 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcher.java @@ -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> successHandler, Consumer errorHandler); +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java index ee2373da8..ad8849a3e 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java @@ -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))); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CombinedSearchFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CombinedSearchFragment.java new file mode 100644 index 000000000..33f63fa96 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/CombinedSearchFragment.java @@ -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 searchResults = new ArrayList<>(); + private List 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 searchProviders = new ArrayList<>(); + searchProviders.add(new FyydPodcastSearcher(query)); + searchProviders.add(new ItunesPodcastSearcher(getContext(), query)); + searchProviders.add(new GpodnetPodcastSearcher(query)); + + List> 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>) subscriber -> { + latch.await(); + + HashMap resultRanking = new HashMap<>(); + HashMap urlToResult = new HashMap<>(); + for (List 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> sortedResults = new ArrayList<>(resultRanking.entrySet()); + Collections.sort(sortedResults, (o1, o2) -> Double.compare(o2.getValue(), o1.getValue())); + + List results = new ArrayList<>(); + for (Map.Entry 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 > Comparator> comparingByValue() { + return (Comparator> & Serializable) + (c1, c2) -> c1.getValue().compareTo(c2.getValue()); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FyydSearchFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FyydSearchFragment.java index dadc596e2..d51db8b07 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FyydSearchFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FyydSearchFragment.java @@ -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 searchResults; + private List 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); - } - } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItunesSearchFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItunesSearchFragment.java index a9f56d317..4f28b650e 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItunesSearchFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItunesSearchFragment.java @@ -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 searchResults; - private List topList; + private List searchResults; + private List 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 result) { + private void updateData(List 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>) emitter -> { + disposable = Single.create((SingleOnSubscribe>) 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 podcasts = parseFeed(feedString); + List podcasts = parseFeed(feedString); emitter.onSuccess(podcasts); } catch (IOException | JSONException e) { if (!disposable.isDisposed()) { @@ -288,15 +284,15 @@ public class ItunesSearchFragment extends Fragment { } } - private List parseFeed(String jsonString) throws JSONException { + private List parseFeed(String jsonString) throws JSONException { JSONObject result = new JSONObject(jsonString); JSONObject feed = result.getJSONObject("feed"); JSONArray entries = feed.getJSONArray("entry"); - List results = new ArrayList<>(); + List 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>) 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 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); + }); } } diff --git a/app/src/main/res/layout/addfeed.xml b/app/src/main/res/layout/addfeed.xml index 33951e060..92169e4a8 100644 --- a/app/src/main/res/layout/addfeed.xml +++ b/app/src/main/res/layout/addfeed.xml @@ -29,10 +29,16 @@ android:textSize="@dimen/text_size_medium"/>