diff options
author | ByteHamster <info@bytehamster.com> | 2022-04-25 23:10:18 +0200 |
---|---|---|
committer | ByteHamster <info@bytehamster.com> | 2022-04-26 18:09:25 +0200 |
commit | 30be4628ae1cd07fe9d9ed584eb865f874869085 (patch) | |
tree | c09f4cf0187dfc82cda5f284eaea1ca9df51d6cd /net/discovery/src | |
parent | 20363ee41c814b14b16999505fa850a0943346dd (diff) | |
download | AntennaPod-30be4628ae1cd07fe9d9ed584eb865f874869085.zip |
Move feed discovery backends to their own module
Diffstat (limited to 'net/discovery/src')
10 files changed, 747 insertions, 0 deletions
diff --git a/net/discovery/src/main/AndroidManifest.xml b/net/discovery/src/main/AndroidManifest.xml new file mode 100644 index 000000000..3e42a802a --- /dev/null +++ b/net/discovery/src/main/AndroidManifest.xml @@ -0,0 +1 @@ +<manifest package="de.danoeh.antennapod.net.discovery" /> diff --git a/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/CombinedSearcher.java b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/CombinedSearcher.java new file mode 100644 index 000000000..6cbf8eb2e --- /dev/null +++ b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/CombinedSearcher.java @@ -0,0 +1,118 @@ +package de.danoeh.antennapod.net.discovery; + +import android.text.TextUtils; +import android.util.Log; +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"; + + public CombinedSearcher() { + } + + public Single<List<PodcastSearchResult>> search(String query) { + ArrayList<Disposable> disposables = new ArrayList<>(); + List<List<PodcastSearchResult>> singleResults = new ArrayList<>( + Collections.nCopies(PodcastSearcherRegistry.getSearchProviders().size(), null)); + CountDownLatch latch = new CountDownLatch(PodcastSearcherRegistry.getSearchProviders().size()); + for (int i = 0; i < PodcastSearcherRegistry.getSearchProviders().size(); i++) { + PodcastSearcherRegistry.SearcherInfo searchProviderInfo + = PodcastSearcherRegistry.getSearchProviders().get(i); + PodcastSearcher searcher = searchProviderInfo.searcher; + if (searchProviderInfo.weight <= 0.00001f || searcher.getClass() == CombinedSearcher.class) { + latch.countDown(); + continue; + } + 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) { + if (disposable != null) { + 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 = PodcastSearcherRegistry.getSearchProviders().get(i).weight; + 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; + } + + @Override + public Single<String> lookupUrl(String url) { + return PodcastSearcherRegistry.lookupUrl(url); + } + + @Override + public boolean urlNeedsLookup(String url) { + return PodcastSearcherRegistry.urlNeedsLookup(url); + } + + @Override + public String getName() { + ArrayList<String> names = new ArrayList<>(); + for (int i = 0; i < PodcastSearcherRegistry.getSearchProviders().size(); i++) { + PodcastSearcherRegistry.SearcherInfo searchProviderInfo + = PodcastSearcherRegistry.getSearchProviders().get(i); + PodcastSearcher searcher = searchProviderInfo.searcher; + if (searchProviderInfo.weight > 0.00001f && searcher.getClass() != CombinedSearcher.class) { + names.add(searcher.getName()); + } + } + return TextUtils.join(", ", names); + } +} diff --git a/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/FyydPodcastSearcher.java b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/FyydPodcastSearcher.java new file mode 100644 index 000000000..d4674c79d --- /dev/null +++ b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/FyydPodcastSearcher.java @@ -0,0 +1,53 @@ +package de.danoeh.antennapod.net.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()); + } + + @Override + public Single<String> lookupUrl(String url) { + return Single.just(url); + } + + @Override + public boolean urlNeedsLookup(String url) { + return false; + } + + @Override + public String getName() { + return "Fyyd"; + } +} diff --git a/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/GpodnetPodcastSearcher.java b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/GpodnetPodcastSearcher.java new file mode 100644 index 000000000..222c415ab --- /dev/null +++ b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/GpodnetPodcastSearcher.java @@ -0,0 +1,52 @@ +package de.danoeh.antennapod.net.discovery; + +import de.danoeh.antennapod.core.sync.SynchronizationCredentials; +import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; +import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetService; +import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.net.sync.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 -> { + try { + GpodnetService service = new GpodnetService(AntennapodHttpClient.getHttpClient(), + SynchronizationCredentials.getHosturl(), SynchronizationCredentials.getDeviceID(), + SynchronizationCredentials.getUsername(), SynchronizationCredentials.getPassword()); + 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); + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + + @Override + public Single<String> lookupUrl(String url) { + return Single.just(url); + } + + @Override + public boolean urlNeedsLookup(String url) { + return false; + } + + @Override + public String getName() { + return "Gpodder.net"; + } +} diff --git a/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/ItunesPodcastSearcher.java b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/ItunesPodcastSearcher.java new file mode 100644 index 000000000..b2ac1766c --- /dev/null +++ b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/ItunesPodcastSearcher.java @@ -0,0 +1,116 @@ +package de.danoeh.antennapod.net.discovery; + +import de.danoeh.antennapod.core.feed.FeedUrlNotFoundException; +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; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ItunesPodcastSearcher implements PodcastSearcher { + private static final String ITUNES_API_URL = "https://itunes.apple.com/search?media=podcast&term=%s"; + private static final String PATTERN_BY_ID = ".*/podcasts\\.apple\\.com/.*/podcast/.*/id(\\d+).*"; + + public ItunesPodcastSearcher() { + } + + @Override + 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); + 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); + if (podcast.feedUrl != null) { + podcasts.add(podcast); + } + } + } else { + subscriber.onError(new IOException(response.toString())); + } + } catch (IOException | JSONException e) { + subscriber.onError(e); + } + subscriber.onSuccess(podcasts); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + + @Override + public Single<String> lookupUrl(String url) { + Pattern pattern = Pattern.compile(PATTERN_BY_ID); + Matcher matcher = pattern.matcher(url); + final String lookupUrl = matcher.find() ? ("https://itunes.apple.com/lookup?id=" + matcher.group(1)) : url; + return Single.create(emitter -> { + OkHttpClient client = AntennapodHttpClient.getHttpClient(); + Request.Builder httpReq = new Request.Builder().url(lookupUrl); + try { + Response response = client.newCall(httpReq.build()).execute(); + if (response.isSuccessful()) { + String resultString = response.body().string(); + JSONObject result = new JSONObject(resultString); + JSONObject results = result.getJSONArray("results").getJSONObject(0); + String feedUrlName = "feedUrl"; + if (!results.has(feedUrlName)) { + String artistName = results.getString("artistName"); + String trackName = results.getString("trackName"); + emitter.onError(new FeedUrlNotFoundException(artistName, trackName)); + return; + } + String feedUrl = results.getString(feedUrlName); + emitter.onSuccess(feedUrl); + } else { + emitter.onError(new IOException(response.toString())); + } + } catch (IOException | JSONException e) { + emitter.onError(e); + } + }); + } + + @Override + public boolean urlNeedsLookup(String url) { + return url.contains("itunes.apple.com") || url.matches(PATTERN_BY_ID); + } + + @Override + public String getName() { + return "iTunes"; + } +} diff --git a/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/ItunesTopListLoader.java b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/ItunesTopListLoader.java new file mode 100644 index 000000000..827a3202f --- /dev/null +++ b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/ItunesTopListLoader.java @@ -0,0 +1,102 @@ +package de.danoeh.antennapod.net.discovery; + +import android.content.Context; +import android.util.Log; + +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.CacheControl; +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.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +public class ItunesTopListLoader { + private static final String TAG = "ITunesTopListLoader"; + private final Context context; + public static final String PREF_KEY_COUNTRY_CODE = "country_code"; + public static final String PREFS = "CountryRegionPrefs"; + public static final String DISCOVER_HIDE_FAKE_COUNTRY_CODE = "00"; + public static final String COUNTRY_CODE_UNSET = "99"; + + public ItunesTopListLoader(Context context) { + this.context = context; + } + + public Single<List<PodcastSearchResult>> loadToplist(String country, int limit) { + return Single.create((SingleOnSubscribe<List<PodcastSearchResult>>) emitter -> { + OkHttpClient client = AntennapodHttpClient.getHttpClient(); + String feedString; + String loadCountry = country; + if (COUNTRY_CODE_UNSET.equals(country)) { + loadCountry = Locale.getDefault().getCountry(); + } + try { + feedString = getTopListFeed(client, loadCountry, limit); + } catch (IOException e) { + if (COUNTRY_CODE_UNSET.equals(country)) { + feedString = getTopListFeed(client, "US", limit); + } else { + emitter.onError(e); + return; + } + } + + List<PodcastSearchResult> podcasts = parseFeed(feedString); + emitter.onSuccess(podcasts); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + + private String getTopListFeed(OkHttpClient client, String country, int limit) throws IOException { + String url = "https://itunes.apple.com/%s/rss/toppodcasts/limit=" + limit + "/explicit=true/json"; + Log.d(TAG, "Feed URL " + String.format(url, country)); + Request.Builder httpReq = new Request.Builder() + .cacheControl(new CacheControl.Builder().maxStale(1, TimeUnit.DAYS).build()) + .url(String.format(url, country)); + + try (Response response = client.newCall(httpReq.build()).execute()) { + if (response.isSuccessful()) { + return response.body().string(); + } + if (response.code() == 400) { + throw new IOException("iTunes does not have data for the selected country."); + } + String prefix = context.getString(R.string.error_msg_prefix); + throw new IOException(prefix + response); + } + } + + private List<PodcastSearchResult> parseFeed(String jsonString) throws JSONException { + JSONObject result = new JSONObject(jsonString); + JSONObject feed; + JSONArray entries; + try { + feed = result.getJSONObject("feed"); + entries = feed.getJSONArray("entry"); + } catch (JSONException e) { + return new ArrayList<>(); + } + + List<PodcastSearchResult> results = new ArrayList<>(); + for (int i = 0; i < entries.length(); i++) { + JSONObject json = entries.getJSONObject(i); + results.add(PodcastSearchResult.fromItunesToplist(json)); + } + + return results; + } + +} diff --git a/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastIndexPodcastSearcher.java b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastIndexPodcastSearcher.java new file mode 100644 index 000000000..4645aaf62 --- /dev/null +++ b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastIndexPodcastSearcher.java @@ -0,0 +1,126 @@ +package de.danoeh.antennapod.net.discovery; + +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.security.MessageDigest; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; + +import de.danoeh.antennapod.net.discovery.BuildConfig; +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; + +public class PodcastIndexPodcastSearcher implements PodcastSearcher { + private static final String PODCASTINDEX_API_URL = "https://api.podcastindex.org/api/1.0/search/byterm?q=%s"; + + public PodcastIndexPodcastSearcher() { + } + + @Override + public Single<List<PodcastSearchResult>> search(String query) { + return Single.create((SingleOnSubscribe<List<PodcastSearchResult>>) subscriber -> { + + Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + calendar.clear(); + Date now = new Date(); + calendar.setTime(now); + long secondsSinceEpoch = calendar.getTimeInMillis() / 1000L; + String apiHeaderTime = String.valueOf(secondsSinceEpoch); + String data4Hash = BuildConfig.PODCASTINDEX_API_KEY + BuildConfig.PODCASTINDEX_API_SECRET + apiHeaderTime; + String hashString = sha1(data4Hash); + + String encodedQuery; + try { + encodedQuery = URLEncoder.encode(query, "UTF-8"); + } catch (UnsupportedEncodingException e) { + // this won't ever be thrown + encodedQuery = query; + } + + String formattedUrl = String.format(PODCASTINDEX_API_URL, encodedQuery); + + OkHttpClient client = AntennapodHttpClient.getHttpClient(); + Request.Builder httpReq = new Request.Builder() + .addHeader("X-Auth-Date", apiHeaderTime) + .addHeader("X-Auth-Key", BuildConfig.PODCASTINDEX_API_KEY) + .addHeader("Authorization", hashString) + .addHeader("User-Agent", ClientConfig.USER_AGENT) + .url(formattedUrl); + 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("feeds"); + + for (int i = 0; i < j.length(); i++) { + JSONObject podcastJson = j.getJSONObject(i); + PodcastSearchResult podcast = PodcastSearchResult.fromPodcastIndex(podcastJson); + if (podcast.feedUrl != null) { + podcasts.add(podcast); + } + } + } else { + subscriber.onError(new IOException(response.toString())); + } + } catch (IOException | JSONException e) { + subscriber.onError(e); + } + subscriber.onSuccess(podcasts); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + + @Override + public Single<String> lookupUrl(String url) { + return Single.just(url); + } + + @Override + public boolean urlNeedsLookup(String url) { + return false; + } + + @Override + public String getName() { + return "Podcastindex.org"; + } + + private static String sha1(String clearString) { + try { + MessageDigest messageDigest = MessageDigest.getInstance("SHA-1"); + messageDigest.update(clearString.getBytes("UTF-8")); + return toHex(messageDigest.digest()); + } catch (Exception ignored) { + ignored.printStackTrace(); + return null; + } + } + + private static String toHex(byte[] bytes) { + StringBuilder buffer = new StringBuilder(); + for (byte b : bytes) { + buffer.append(String.format(Locale.getDefault(), "%02x", b)); + } + return buffer.toString(); + } +} diff --git a/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastSearchResult.java b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastSearchResult.java new file mode 100644 index 000000000..b3f352334 --- /dev/null +++ b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastSearchResult.java @@ -0,0 +1,110 @@ +package de.danoeh.antennapod.net.discovery; + +import androidx.annotation.Nullable; +import de.danoeh.antennapod.net.sync.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; + + /** + * artistName of the podcast feed + */ + @Nullable + public final String author; + + + private PodcastSearchResult(String title, @Nullable String imageUrl, @Nullable String feedUrl, @Nullable String author) { + this.title = title; + this.imageUrl = imageUrl; + this.feedUrl = feedUrl; + this.author = author; + } + + public static PodcastSearchResult dummy() { + return new PodcastSearchResult("", "", "", ""); + } + + /** + * 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); + String author = json.optString("artistName", null); + return new PodcastSearchResult(title, imageUrl, feedUrl, author); + } + + /** + * 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"); + + String author = null; + try { + author = json.getJSONObject("im:artist").getString("label"); + } catch (Exception e) { + // Some feeds have empty artist + } + return new PodcastSearchResult(title, imageUrl, feedUrl, author); + } + + public static PodcastSearchResult fromFyyd(SearchHit searchHit) { + return new PodcastSearchResult(searchHit.getTitle(), + searchHit.getThumbImageURL(), + searchHit.getXmlUrl(), + searchHit.getAuthor()); + } + + public static PodcastSearchResult fromGpodder(GpodnetPodcast searchHit) { + return new PodcastSearchResult(searchHit.getTitle(), + searchHit.getLogoUrl(), + searchHit.getUrl(), + searchHit.getAuthor()); + } + + public static PodcastSearchResult fromPodcastIndex(JSONObject json) { + String title = json.optString("title", ""); + String imageUrl = json.optString("image", null); + String feedUrl = json.optString("url", null); + String author = json.optString("author", null); + return new PodcastSearchResult(title, imageUrl, feedUrl, author); + } +} diff --git a/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastSearcher.java b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastSearcher.java new file mode 100644 index 000000000..76edbf843 --- /dev/null +++ b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastSearcher.java @@ -0,0 +1,14 @@ +package de.danoeh.antennapod.net.discovery; + +import io.reactivex.Single; +import java.util.List; + +public interface PodcastSearcher { + Single<List<PodcastSearchResult>> search(String query); + + Single<String> lookupUrl(String resultUrl); + + boolean urlNeedsLookup(String resultUrl); + + String getName(); +} diff --git a/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastSearcherRegistry.java b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastSearcherRegistry.java new file mode 100644 index 000000000..c7892bd09 --- /dev/null +++ b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastSearcherRegistry.java @@ -0,0 +1,55 @@ +package de.danoeh.antennapod.net.discovery; + +import io.reactivex.Single; + +import java.util.ArrayList; +import java.util.List; + +public class PodcastSearcherRegistry { + private static List<SearcherInfo> searchProviders; + + private PodcastSearcherRegistry() { + } + + public static synchronized List<SearcherInfo> getSearchProviders() { + if (searchProviders == null) { + searchProviders = new ArrayList<>(); + searchProviders.add(new SearcherInfo(new CombinedSearcher(), 1.0f)); + searchProviders.add(new SearcherInfo(new GpodnetPodcastSearcher(), 0.0f)); + searchProviders.add(new SearcherInfo(new FyydPodcastSearcher(), 1.0f)); + searchProviders.add(new SearcherInfo(new ItunesPodcastSearcher(), 1.0f)); + searchProviders.add(new SearcherInfo(new PodcastIndexPodcastSearcher(), 1.0f)); + } + return searchProviders; + } + + public static Single<String> lookupUrl(String url) { + for (PodcastSearcherRegistry.SearcherInfo searchProviderInfo : getSearchProviders()) { + if (searchProviderInfo.searcher.getClass() != CombinedSearcher.class + && searchProviderInfo.searcher.urlNeedsLookup(url)) { + return searchProviderInfo.searcher.lookupUrl(url); + } + } + return Single.just(url); + } + + public static boolean urlNeedsLookup(String url) { + for (PodcastSearcherRegistry.SearcherInfo searchProviderInfo : getSearchProviders()) { + if (searchProviderInfo.searcher.getClass() != CombinedSearcher.class + && searchProviderInfo.searcher.urlNeedsLookup(url)) { + return true; + } + } + return false; + } + + public static class SearcherInfo { + public final PodcastSearcher searcher; + public final float weight; + + public SearcherInfo(PodcastSearcher searcher, float weight) { + this.searcher = searcher; + this.weight = weight; + } + } +} |