diff --git a/src/main/java/io/sfrei/tracksearch/clients/MultiSearchClient.java b/src/main/java/io/sfrei/tracksearch/clients/MultiSearchClient.java index c431a94..7c3ed15 100644 --- a/src/main/java/io/sfrei/tracksearch/clients/MultiSearchClient.java +++ b/src/main/java/io/sfrei/tracksearch/clients/MultiSearchClient.java @@ -19,8 +19,6 @@ import io.sfrei.tracksearch.clients.interfaces.Provider; import io.sfrei.tracksearch.clients.setup.QueryType; import io.sfrei.tracksearch.clients.setup.TrackSource; -import io.sfrei.tracksearch.clients.soundcloud.SoundCloudClient; -import io.sfrei.tracksearch.clients.youtube.YouTubeClient; import io.sfrei.tracksearch.config.TrackSearchConfig; import io.sfrei.tracksearch.exceptions.TrackSearchException; import io.sfrei.tracksearch.tracks.*; @@ -41,23 +39,40 @@ public class MultiSearchClient implements MultiTrackSearchClient, Provider youTubeClient; - private final TrackSearchClient soundCloudClient; - - private final Map> clientsForSource = new HashMap<>(); + private final Map> clientsBySource; + private final Set validURLPrefixes; public MultiSearchClient() { - this.youTubeClient = new YouTubeClient(); - this.soundCloudClient = new SoundCloudClient(); + clientsBySource = Arrays.stream(TrackSource.values()) + .collect(Collectors.toMap(source -> source, TrackSource::createClient)); - clientsForSource.put(TrackSource.Youtube, youTubeClient); - clientsForSource.put(TrackSource.Soundcloud, soundCloudClient); + validURLPrefixes = clientsBySource.values() + .stream().map(TrackSearchClient::validURLPrefixes) + .flatMap(Set::stream) + .collect(Collectors.toSet()); log.info("TrackSearchClient created with {} clients", allClients().size()); } private List> allClients() { - return new ArrayList<>(clientsForSource.values()); + return new ArrayList<>(clientsBySource.values()); + } + + @Override + public Set validURLPrefixes() { + return validURLPrefixes; + } + + @Override + public Track getTrack(@NonNull String url) throws TrackSearchException { + final TrackSearchClient trackSearchClient = clientsBySource.values() + .stream() + .filter(client -> client.isApplicableForURL(url)) + .findFirst() + .orElseThrow(() -> new TrackSearchException(String.format("No client found to handle URL: %s", url))); + + log().debug("Using {} for URL: {}", trackSearchClient.getClass().getSimpleName(), url); + return trackSearchClient.getTrack(url); } @Override @@ -79,9 +94,9 @@ public TrackList getNext(@NonNull final TrackList trackL public String getStreamUrl(@NonNull final Track track) throws TrackSearchException { if (track instanceof YouTubeTrack) { - return youTubeClient.getStreamUrl((YouTubeTrack) track); + return clientsBySource.get(TrackSource.Youtube).getStreamUrl(track); } else if (track instanceof SoundCloudTrack) { - return soundCloudClient.getStreamUrl((SoundCloudTrack) track); + return clientsBySource.get(TrackSource.Soundcloud).getStreamUrl(track); } throw new TrackSearchException("Track type is unknown"); } @@ -89,9 +104,9 @@ public String getStreamUrl(@NonNull final Track track) throws TrackSearchExcepti @Override public String getStreamUrl(@NonNull Track track, int retries) throws TrackSearchException { if (track instanceof YouTubeTrack) { - return youTubeClient.getStreamUrl((YouTubeTrack) track, retries); + return clientsBySource.get(TrackSource.Youtube).getStreamUrl(track, retries); } else if (track instanceof SoundCloudTrack) { - return soundCloudClient.getStreamUrl((SoundCloudTrack) track, retries); + return clientsBySource.get(TrackSource.Soundcloud).getStreamUrl(track, retries); } throw new TrackSearchException("Track type is unknown"); } @@ -104,8 +119,8 @@ public TrackList getTracksForSearch(@NonNull final String search, @NonNul throw new TrackSearchException("Provide at least one source"); final List> callClients = sources.stream() - .filter(clientsForSource::containsKey) - .map(clientsForSource::get) + .filter(clientsBySource::containsKey) + .map(clientsBySource::get) .collect(Collectors.toList()); return getTracksForSearch(search, callClients); diff --git a/src/main/java/io/sfrei/tracksearch/clients/MultiTrackSearchClient.java b/src/main/java/io/sfrei/tracksearch/clients/MultiTrackSearchClient.java index a0cb3b6..0930b75 100644 --- a/src/main/java/io/sfrei/tracksearch/clients/MultiTrackSearchClient.java +++ b/src/main/java/io/sfrei/tracksearch/clients/MultiTrackSearchClient.java @@ -55,7 +55,7 @@ public interface MultiTrackSearchClient extends TrackSearchClient { String getStreamUrl(@NotNull Track track) throws TrackSearchException; /** - * Search for tracks using a string containing keywords on pre selected track sources. + * Search for tracks using a string containing keywords on pre-selected track sources. * @param search keywords to search for. * @param sources available to search on. * @return a tracklist containing all found tracks for selected clients. diff --git a/src/main/java/io/sfrei/tracksearch/clients/TrackSearchClient.java b/src/main/java/io/sfrei/tracksearch/clients/TrackSearchClient.java index 0eac4c1..8e219be 100644 --- a/src/main/java/io/sfrei/tracksearch/clients/TrackSearchClient.java +++ b/src/main/java/io/sfrei/tracksearch/clients/TrackSearchClient.java @@ -21,14 +21,45 @@ import io.sfrei.tracksearch.tracks.TrackList; import lombok.NonNull; +import java.util.Set; + /** * Main interface containing all functionality a client offers to the user. + * * @param the track type the client implementing this is used for. */ public interface TrackSearchClient { + /** + * Retrieve all valid URL prefixes used to check {@link #isApplicableForURL(String)}. + * + * @return the set of valid URL prefixes. + */ + Set validURLPrefixes(); + + /** + * Test if this client can handle the provided URL to make sure it can + * be used for {@link #getTrack(String)}. + * + * @param url the URL to check. + * @return true if this client is applicable and false if not. + */ + default boolean isApplicableForURL(@NonNull String url) { + return validURLPrefixes().stream().anyMatch(url::startsWith); + } + + /** + * Get a track for the given URL. + * + * @param url the URL to create track for. + * @return the track for the provided URL. + * @throws TrackSearchException when the track cannot ba created. + */ + T getTrack(@NonNull String url) throws TrackSearchException; + /** * Search for tracks using a string containing keywords. + * * @param search keywords to search for. * @return a track list containing all found tracks. * @throws TrackSearchException when the client encountered a problem on searching. @@ -37,6 +68,7 @@ public interface TrackSearchClient { /** * Search for the next tracks for last result. + * * @param trackList a previous search result for that client. * @return a track list containing the next tracks available. * @throws TrackSearchException when the client encounters a problem on getting the next tracks. @@ -45,6 +77,7 @@ public interface TrackSearchClient { /** * Get the audio stream URL in the highest possible audio resolution. + * * @param track from this client. * @return the audio stream URL. * @throws TrackSearchException when the URL could not be exposed. @@ -53,7 +86,8 @@ public interface TrackSearchClient { /** * Get the audio stream URL in the highest possible audio resolution and retry when there was a failure. - * @param track from this client. + * + * @param track from this client. * @param retries retry when stream URL resolving was not successful. This is determined with another request/s. * @return the audio stream URL. * @throws TrackSearchException when the URL could not be exposed. @@ -62,6 +96,7 @@ public interface TrackSearchClient { /** * Check the track list for this client if the paging values to get next are present. + * * @param trackList a previous search result for this client. * @return either the paging values are present or not. */ diff --git a/src/main/java/io/sfrei/tracksearch/clients/interfaces/ClientHelper.java b/src/main/java/io/sfrei/tracksearch/clients/interfaces/ClientHelper.java index a1624d1..0cf6ef1 100644 --- a/src/main/java/io/sfrei/tracksearch/clients/interfaces/ClientHelper.java +++ b/src/main/java/io/sfrei/tracksearch/clients/interfaces/ClientHelper.java @@ -25,7 +25,7 @@ import java.util.function.Function; -public interface ClientHelper extends ClassLogger { +public interface ClientHelper extends ClientLogger { int INITIAL_TRY = 1; diff --git a/src/main/java/io/sfrei/tracksearch/clients/interfaces/ClassLogger.java b/src/main/java/io/sfrei/tracksearch/clients/interfaces/ClientLogger.java similarity index 96% rename from src/main/java/io/sfrei/tracksearch/clients/interfaces/ClassLogger.java rename to src/main/java/io/sfrei/tracksearch/clients/interfaces/ClientLogger.java index b7a776c..e5aa42e 100644 --- a/src/main/java/io/sfrei/tracksearch/clients/interfaces/ClassLogger.java +++ b/src/main/java/io/sfrei/tracksearch/clients/interfaces/ClientLogger.java @@ -18,7 +18,7 @@ import org.slf4j.Logger; -public interface ClassLogger { +public interface ClientLogger { /** * Obtain the logger from the implementing class. diff --git a/src/main/java/io/sfrei/tracksearch/clients/interfaces/Provider.java b/src/main/java/io/sfrei/tracksearch/clients/interfaces/Provider.java index ff1bfef..296d97b 100644 --- a/src/main/java/io/sfrei/tracksearch/clients/interfaces/Provider.java +++ b/src/main/java/io/sfrei/tracksearch/clients/interfaces/Provider.java @@ -23,7 +23,7 @@ import io.sfrei.tracksearch.tracks.TrackList; import org.jetbrains.annotations.Nullable; -public interface Provider extends TrackSearchClient, ClassLogger { +public interface Provider extends TrackSearchClient, ClientLogger { @Nullable default TrackList provideNext(final TrackList trackList) { diff --git a/src/main/java/io/sfrei/tracksearch/clients/setup/TrackSource.java b/src/main/java/io/sfrei/tracksearch/clients/setup/TrackSource.java index 764565a..dee20d0 100644 --- a/src/main/java/io/sfrei/tracksearch/clients/setup/TrackSource.java +++ b/src/main/java/io/sfrei/tracksearch/clients/setup/TrackSource.java @@ -16,6 +16,11 @@ package io.sfrei.tracksearch.clients.setup; +import io.sfrei.tracksearch.clients.TrackSearchClient; +import io.sfrei.tracksearch.clients.soundcloud.SoundCloudClient; +import io.sfrei.tracksearch.clients.youtube.YouTubeClient; +import io.sfrei.tracksearch.tracks.Track; + import java.util.Arrays; import java.util.Set; import java.util.stream.Collectors; @@ -28,4 +33,11 @@ public static Set setOf(TrackSource... sources) { return Arrays.stream(sources).collect(Collectors.toSet()); } + public TrackSearchClient createClient() { + return (TrackSearchClient) switch (this) { + case Youtube -> new YouTubeClient(); + case Soundcloud -> new SoundCloudClient(); + }; + } + } diff --git a/src/main/java/io/sfrei/tracksearch/clients/soundcloud/SoundCloudClient.java b/src/main/java/io/sfrei/tracksearch/clients/soundcloud/SoundCloudClient.java index 506a24e..944bafa 100644 --- a/src/main/java/io/sfrei/tracksearch/clients/soundcloud/SoundCloudClient.java +++ b/src/main/java/io/sfrei/tracksearch/clients/soundcloud/SoundCloudClient.java @@ -36,22 +36,21 @@ import retrofit2.Retrofit; import java.net.CookiePolicy; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; @Slf4j public class SoundCloudClient extends SingleSearchClient implements ClientHelper, Provider, UniformClientException { - public static final String HOSTNAME = "https://soundcloud.com"; + public static final String URL = "https://soundcloud.com"; private static final String INFORMATION_PREFIX = "sc"; public static final String POSITION_KEY = INFORMATION_PREFIX + TrackSearchConfig.POSITION_KEY_SUFFIX; public static final String OFFSET_KEY = INFORMATION_PREFIX + TrackSearchConfig.OFFSET_KEY_SUFFIX; private static final String PAGING_OFFSET = "limit"; private static final String PAGING_POSITION = "position"; + private static final Set VALID_URL_PREFIXES = Set.of(URL); // TODO: Extend + private final SoundCloudAPI api; private final SoundCloudUtility soundCloudUtility; @@ -62,7 +61,7 @@ public SoundCloudClient() { super(CookiePolicy.ACCEPT_ALL, null); final Retrofit base = new Retrofit.Builder() - .baseUrl(HOSTNAME) + .baseUrl(URL) .client(okHttpClient) .addConverterFactory(ResponseProviderFactory.create()) .build(); @@ -76,7 +75,16 @@ public static Map makeQueryInformation(final String query) { return new HashMap<>(Map.of(TrackList.QUERY_KEY, query)); } + @Override + public Set validURLPrefixes() { + return VALID_URL_PREFIXES; + } + + @Override public SoundCloudTrack getTrack(@NonNull final String url) throws TrackSearchException { + if (!isApplicableForURL(url)) + throw new SoundCloudException(String.format("%s not applicable for URL: %s", this.getClass().getSimpleName(), url)); + final String trackHTML = requestRefreshingClientId(api.getForUrlWithClientID(url, clientID)).getContentOrThrow(); final String trackURL = soundCloudUtility.extractTrackURL(trackHTML); final String trackJSON = requestRefreshingClientId(api.getForUrlWithClientID(trackURL, clientID)).getContentOrThrow(); diff --git a/src/main/java/io/sfrei/tracksearch/clients/youtube/YouTubeClient.java b/src/main/java/io/sfrei/tracksearch/clients/youtube/YouTubeClient.java index 16bff59..6f87703 100644 --- a/src/main/java/io/sfrei/tracksearch/clients/youtube/YouTubeClient.java +++ b/src/main/java/io/sfrei/tracksearch/clients/youtube/YouTubeClient.java @@ -41,12 +41,15 @@ import java.util.HashMap; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; @Slf4j public class YouTubeClient extends SingleSearchClient implements ClientHelper, Provider, UniformClientException { - public static final String HOSTNAME = "https://www.youtube.com"; + public static final String URL = "https://www.youtube.com"; public static final String PAGING_KEY = "ctoken"; private static final String INFORMATION_PREFIX = "yt"; public static final String POSITION_KEY = INFORMATION_PREFIX + TrackSearchConfig.POSITION_KEY_SUFFIX; @@ -57,13 +60,11 @@ public class YouTubeClient extends SingleSearchClient private static final Map VIDEO_SEARCH_PARAMS = Map.of("sp", "EgIQAQ%3D%3D"); public static final Map TRACK_PARAMS = Map.of("pbj", "1", "hl", "en", "alt", "json"); - private static final Map DEFAULT_SEARCH_PARAMS; + private static final Map DEFAULT_SEARCH_PARAMS = Stream.of(VIDEO_SEARCH_PARAMS.entrySet(), TRACK_PARAMS.entrySet()) + .flatMap(Set::stream) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - static { - DEFAULT_SEARCH_PARAMS = new HashMap<>(); - DEFAULT_SEARCH_PARAMS.putAll(VIDEO_SEARCH_PARAMS); - DEFAULT_SEARCH_PARAMS.putAll(TRACK_PARAMS); - } + private static final Set VALID_URL_PREFIXES = Set.of(URL); // TODO: Extend private final YouTubeAPI api; private final YouTubeUtility youTubeUtility; @@ -78,7 +79,7 @@ public YouTubeClient() { ); final Retrofit base = new Retrofit.Builder() - .baseUrl(HOSTNAME) + .baseUrl(URL) .client(okHttpClient) .addConverterFactory(ResponseProviderFactory.create()) .build(); @@ -92,10 +93,19 @@ public static Map makeQueryInformation(final String query, final return new HashMap<>(Map.of(TrackList.QUERY_KEY, query, PAGING_INFORMATION, pagingToken)); } - public YouTubeTrack getTrack(@NonNull final String trackUrl) throws TrackSearchException { - final String trackJSON = requestTrackJSON(api.getForUrlWithParams(trackUrl, TRACK_PARAMS)); + @Override + public Set validURLPrefixes() { + return VALID_URL_PREFIXES; + } + + @Override + public YouTubeTrack getTrack(@NonNull final String url) throws TrackSearchException { + if (!isApplicableForURL(url)) + throw new YouTubeException(String.format("%s not applicable for URL: %s", this.getClass().getSimpleName(), url)); + + final String trackJSON = requestTrackJSON(api.getForUrlWithParams(url, TRACK_PARAMS)); final YouTubeTrack youTubeTrack = youTubeUtility.extractYouTubeTrack(trackJSON, this::streamURLProvider); - final YouTubeTrackInfo trackInfo = youTubeUtility.extractTrackInfo(trackJSON, trackUrl, this::requestURL); + final YouTubeTrackInfo trackInfo = youTubeUtility.extractTrackInfo(trackJSON, url, this::requestURL); youTubeTrack.setTrackInfo(trackInfo); return youTubeTrack; } @@ -158,7 +168,7 @@ public String getStreamUrl(@NonNull final YouTubeTrack youtubeTrack) throws Trac log.trace("Use cached script for: {}", scriptUrl); scriptContent = scriptCache.get(scriptUrl); } else { - scriptContent = requestURL(HOSTNAME + scriptUrl).getContentOrThrow(); + scriptContent = requestURL(URL + scriptUrl).getContentOrThrow(); scriptCache.put(scriptUrl, scriptContent); } diff --git a/src/main/java/io/sfrei/tracksearch/tracks/deserializer/youtube/YouTubeListTrackDeserializer.java b/src/main/java/io/sfrei/tracksearch/tracks/deserializer/youtube/YouTubeListTrackDeserializer.java index ab4b1c0..8821073 100644 --- a/src/main/java/io/sfrei/tracksearch/tracks/deserializer/youtube/YouTubeListTrackDeserializer.java +++ b/src/main/java/io/sfrei/tracksearch/tracks/deserializer/youtube/YouTubeListTrackDeserializer.java @@ -50,7 +50,7 @@ public YouTubeTrack.ListYouTubeTrackBuilder deserialize(final JsonParser p, fina if (title == null || duration == null || ref == null) return null; - final String url = YouTubeClient.HOSTNAME.concat("/watch?v=").concat(ref); + final String url = YouTubeClient.URL.concat("/watch?v=").concat(ref); final YouTubeTrack.ListYouTubeTrackBuilder listYouTubeTrackBuilder = new YouTubeTrack.ListYouTubeTrackBuilder(); final YouTubeTrackBuilder youTubeTrackBuilder = listYouTubeTrackBuilder.getBuilder() @@ -66,7 +66,7 @@ public YouTubeTrack.ListYouTubeTrackBuilder deserialize(final JsonParser p, fina final String channelUrlSuffix = owner.path("navigationEndpoint", "commandMetadata", "webCommandMetadata") .asString("url"); - final String channelUrl = YouTubeClient.HOSTNAME.concat(channelUrlSuffix); + final String channelUrl = YouTubeClient.URL.concat(channelUrlSuffix); final String streamAmountText = rootElement.path("viewCountText").asString("simpleText"); final String streamAmountDigits = streamAmountText == null || streamAmountText.isEmpty() ? diff --git a/src/main/java/io/sfrei/tracksearch/tracks/deserializer/youtube/YouTubeURLTrackDeserializer.java b/src/main/java/io/sfrei/tracksearch/tracks/deserializer/youtube/YouTubeURLTrackDeserializer.java index b7310b4..7f00d9c 100644 --- a/src/main/java/io/sfrei/tracksearch/tracks/deserializer/youtube/YouTubeURLTrackDeserializer.java +++ b/src/main/java/io/sfrei/tracksearch/tracks/deserializer/youtube/YouTubeURLTrackDeserializer.java @@ -51,7 +51,7 @@ public YouTubeTrack.URLYouTubeTrackBuilder deserialize(final JsonParser p, final if (title == null || duration == null || ref == null) return null; - final String url = YouTubeClient.HOSTNAME.concat("/watch?v=").concat(ref); + final String url = YouTubeClient.URL.concat("/watch?v=").concat(ref); final YouTubeTrack.URLYouTubeTrackBuilder listYouTubeTrackBuilder = new YouTubeTrack.URLYouTubeTrackBuilder(); final YouTubeTrackBuilder youTubeTrackBuilder = listYouTubeTrackBuilder.getBuilder()