From b43f71cc32f88466ad0872fe1c645e41fcac493f Mon Sep 17 00:00:00 2001 From: h4h13 Date: Thu, 6 Jun 2019 21:57:42 +0530 Subject: [PATCH] Added Deezer for loading Artist images --- .../adapter/playlist/PlaylistAdapter.kt | 13 ++- .../retromusic/deezer/DeezerApiService.kt | 63 ++++++++++++ .../retromusic/deezer/DeezerResponse.kt | 31 ++++++ .../glide/artistimage/ArtistImageLoader.kt | 57 +++++------ .../monkey/retromusic/model/Playlist.java | 21 ++++ .../monkey/retromusic/util/MusicUtil.java | 97 ++++++++++++++----- 6 files changed, 227 insertions(+), 55 deletions(-) create mode 100644 app/src/main/java/code/name/monkey/retromusic/deezer/DeezerApiService.kt create mode 100644 app/src/main/java/code/name/monkey/retromusic/deezer/DeezerResponse.kt diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/playlist/PlaylistAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/playlist/PlaylistAdapter.kt index d2e1c873..100cd9ed 100755 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/playlist/PlaylistAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/playlist/PlaylistAdapter.kt @@ -29,6 +29,7 @@ import code.name.monkey.retromusic.util.MusicUtil import code.name.monkey.retromusic.util.NavigationUtil import java.util.* + class PlaylistAdapter(protected val activity: AppCompatActivity, dataSet: ArrayList, @param:LayoutRes protected var itemLayoutRes: Int, cabHolder: CabHolder?) : AbsMultiSelectAdapter(activity, cabHolder, R.menu.menu_playlists_selection) { var dataSet: ArrayList @@ -60,6 +61,14 @@ class PlaylistAdapter(protected val activity: AppCompatActivity, dataSet: ArrayL return ViewHolder(view) } + protected fun getPlaylistTitle(playlist: Playlist): String { + return playlist.name + } + + protected fun getPlaylistText(playlist: Playlist): String { + return playlist.getInfoString(activity) + } + override fun onBindViewHolder(holder: ViewHolder, position: Int) { val playlist = dataSet[position] @@ -67,10 +76,10 @@ class PlaylistAdapter(protected val activity: AppCompatActivity, dataSet: ArrayL holder.itemView.isActivated = isChecked(playlist) if (holder.title != null) { - holder.title!!.text = playlist.name + holder.title!!.text = getPlaylistTitle(playlist) } if (holder.text != null) { - holder.text!!.text = String.format(Locale.getDefault(), "%d Songs", songs!!.size) + holder.text!!.text = getPlaylistText(playlist) } if (holder.image != null) { holder.image!!.setImageDrawable(getIconRes(playlist)) diff --git a/app/src/main/java/code/name/monkey/retromusic/deezer/DeezerApiService.kt b/app/src/main/java/code/name/monkey/retromusic/deezer/DeezerApiService.kt new file mode 100644 index 00000000..16a64bae --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/deezer/DeezerApiService.kt @@ -0,0 +1,63 @@ +package code.name.monkey.retromusic.deezer + +import android.content.Context +import okhttp3.Cache +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import retrofit2.Call +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.create +import retrofit2.http.GET +import retrofit2.http.Query +import java.io.File +import java.util.* + +private const val BASE_QUERY_ARTIST = "search/artist" +private const val BASE_URL = "https://api.deezer.com/" + +interface DeezerApiService { + + @GET("$BASE_QUERY_ARTIST&limit=1") + fun getArtistImage( + @Query("q") artistName: String + ): Call + + companion object { + operator fun invoke(client: okhttp3.Call.Factory): DeezerApiService { + return Retrofit.Builder() + .baseUrl(BASE_URL) + .callFactory(client) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create() + } + + fun createDefaultOkHttpClient(context: Context): OkHttpClient.Builder = + OkHttpClient.Builder() + .cache(createDefaultCache(context)) + .addInterceptor(createCacheControlInterceptor()) + + private fun createDefaultCache(context: Context): Cache? { + val cacheDir = File(context.applicationContext.cacheDir.absolutePath, "/okhttp-deezer/") + if (cacheDir.mkdir() or cacheDir.isDirectory) { + return Cache(cacheDir, 1024 * 1024 * 10) + } + return null + } + + private fun createCacheControlInterceptor(): Interceptor { + return Interceptor { chain -> + val modifiedRequest = chain.request().newBuilder() + .addHeader("Cache-Control", + String.format( + Locale.getDefault(), + "max-age=%d, max-stale=%d", + 31536000, 31536000 + ) + ).build() + chain.proceed(modifiedRequest) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/deezer/DeezerResponse.kt b/app/src/main/java/code/name/monkey/retromusic/deezer/DeezerResponse.kt new file mode 100644 index 00000000..4a076112 --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/deezer/DeezerResponse.kt @@ -0,0 +1,31 @@ +package code.name.monkey.retromusic.deezer + +import com.google.gson.annotations.SerializedName + +data class Data( + val id: String, + val link: String, + val name: String, + @SerializedName("nb_album") + val nbAlbum: Int, + @SerializedName("nb_fan") + val nbFan: Int, + val picture: String, + @SerializedName("picture_big") + val pictureBig: String, + @SerializedName("picture_medium") + val pictureMedium: String, + @SerializedName("picture_small") + val pictureSmall: String, + @SerializedName("picture_xl") + val pictureXl: String, + val radio: Boolean, + val tracklist: String, + val type: String +) + +data class DeezerResponse( + val data: List, + val next: String, + val total: Int +) \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/glide/artistimage/ArtistImageLoader.kt b/app/src/main/java/code/name/monkey/retromusic/glide/artistimage/ArtistImageLoader.kt index a3675071..3518f130 100644 --- a/app/src/main/java/code/name/monkey/retromusic/glide/artistimage/ArtistImageLoader.kt +++ b/app/src/main/java/code/name/monkey/retromusic/glide/artistimage/ArtistImageLoader.kt @@ -17,6 +17,8 @@ package code.name.monkey.retromusic.glide.artistimage import android.content.Context import android.text.TextUtils import code.name.monkey.retromusic.App +import code.name.monkey.retromusic.deezer.DeezerApiService +import code.name.monkey.retromusic.deezer.DeezerResponse import code.name.monkey.retromusic.rest.LastFMRestClient import code.name.monkey.retromusic.rest.model.LastFmArtist import code.name.monkey.retromusic.util.LastFMUtil @@ -43,12 +45,12 @@ import java.util.concurrent.TimeUnit class ArtistImage(val artistName: String, val skipOkHttpCache: Boolean) class ArtistImageFetcher(private val context: Context, - private val lastFMRestClient: LastFMRestClient, + private val deezerApiService: DeezerApiService, private val okHttp: OkHttpClient, private val model: ArtistImage) : DataFetcher { @Volatile private var isCancelled: Boolean = false - private var call: Call? = null + private var call: Call? = null private var streamFetcher: OkHttpStreamFetcher? = null @@ -62,37 +64,30 @@ class ArtistImageFetcher(private val context: Context, override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { try { - if (!MusicUtil.isArtistNameUnknown(model.artistName) && RetroUtil.isAllowedToDownloadMetadata(context) ) { - call = lastFMRestClient.apiService.getArtistInfo(model.artistName, null, if (model.skipOkHttpCache) "no-cache" else null) - call!!.enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { + if (!MusicUtil.isArtistNameUnknown(model.artistName) && RetroUtil.isAllowedToDownloadMetadata(context)) { + call = deezerApiService.getArtistImage(model.artistName) + call?.enqueue(object : Callback { + override fun onFailure(call: Call, t: Throwable) { + callback.onLoadFailed(Exception(t)) + } + + override fun onResponse(call: Call, response: Response) { if (isCancelled) { callback.onDataReady(null) return } - - val lastFmArtist = response.body() - if (lastFmArtist == null || lastFmArtist.artist == null || lastFmArtist.artist.image == null) { + try { + val deezerResponse: DeezerResponse? = response.body() + println(deezerResponse) + val url = deezerResponse?.data?.get(0)?.pictureXl + streamFetcher = OkHttpStreamFetcher(okHttp, GlideUrl(url)) + streamFetcher?.loadData(priority, callback) + } catch (e: Exception) { callback.onLoadFailed(Exception("No artist image url found")) - return } - - val url = LastFMUtil.getLargestArtistImageUrl(lastFmArtist.artist.image) - if (TextUtils.isEmpty(url) || TextUtils.isEmpty(url.trim { it <= ' ' })) { - callback.onLoadFailed(Exception("No artist image url found")) - return - } - - streamFetcher = OkHttpStreamFetcher(okHttp, GlideUrl(url)) - streamFetcher!!.loadData(priority, callback) } - override fun onFailure(call: Call, throwable: Throwable) { - callback.onLoadFailed(Exception(throwable)) - } }) - - } } catch (e: Exception) { callback.onLoadFailed(e) @@ -108,9 +103,7 @@ class ArtistImageFetcher(private val context: Context, override fun cancel() { isCancelled = true - if (call != null) { - call!!.cancel() - } + call?.cancel() if (streamFetcher != null) { streamFetcher!!.cancel() } @@ -121,10 +114,12 @@ class ArtistImageFetcher(private val context: Context, } } -class ArtistImageLoader(private val context: Context, private val lastFMClient: LastFMRestClient, private val okhttp: OkHttpClient) : ModelLoader { +class ArtistImageLoader(private val context: Context, + private val deezerApiService: DeezerApiService, + private val okhttp: OkHttpClient) : ModelLoader { override fun buildLoadData(model: ArtistImage, width: Int, height: Int, options: Options): ModelLoader.LoadData? { - return ModelLoader.LoadData(ObjectKey(model.artistName), ArtistImageFetcher(context, lastFMClient, okhttp, model)) + return ModelLoader.LoadData(ObjectKey(model.artistName), ArtistImageFetcher(context, deezerApiService, okhttp, model)) } override fun handles(model: ArtistImage): Boolean { @@ -132,7 +127,7 @@ class ArtistImageLoader(private val context: Context, private val lastFMClient: } class Factory(private val context: Context) : ModelLoaderFactory { - private val lastFMClient: LastFMRestClient = LastFMRestClient(LastFMRestClient.createDefaultOkHttpClientBuilder(context) + private val deezerApiService: DeezerApiService = DeezerApiService.invoke(DeezerApiService.createDefaultOkHttpClient(context) .connectTimeout(TIMEOUT.toLong(), TimeUnit.MILLISECONDS) .readTimeout(TIMEOUT.toLong(), TimeUnit.MILLISECONDS) .writeTimeout(TIMEOUT.toLong(), TimeUnit.MILLISECONDS) @@ -145,7 +140,7 @@ class ArtistImageLoader(private val context: Context, private val lastFMClient: override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader { - return ArtistImageLoader(context, lastFMClient, okHttp) + return ArtistImageLoader(context, deezerApiService, okHttp) } override fun teardown() {} diff --git a/app/src/main/java/code/name/monkey/retromusic/model/Playlist.java b/app/src/main/java/code/name/monkey/retromusic/model/Playlist.java index 6d66cbdd..63fc3967 100644 --- a/app/src/main/java/code/name/monkey/retromusic/model/Playlist.java +++ b/app/src/main/java/code/name/monkey/retromusic/model/Playlist.java @@ -14,9 +14,17 @@ package code.name.monkey.retromusic.model; +import android.content.Context; import android.os.Parcel; import android.os.Parcelable; +import androidx.annotation.NonNull; + +import java.util.ArrayList; + +import code.name.monkey.retromusic.loaders.PlaylistSongsLoader; +import code.name.monkey.retromusic.util.MusicUtil; + /** * @author Karim Abou Zeid (kabouzeid) */ @@ -34,6 +42,19 @@ public class Playlist implements Parcelable { this.name = ""; } + @NonNull + public String getInfoString(@NonNull Context context) { + int songCount = getSongs(context).size(); + String songCountString = MusicUtil.getSongCountString(context, songCount); + return MusicUtil.buildInfoString(songCountString, ""); + } + + @NonNull + public ArrayList getSongs(Context context) { + // this default implementation covers static playlists + return PlaylistSongsLoader.INSTANCE.getPlaylistSongList(context, id).blockingFirst(); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/app/src/main/java/code/name/monkey/retromusic/util/MusicUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/MusicUtil.java index 1864b593..0dae40bb 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/MusicUtil.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/MusicUtil.java @@ -29,6 +29,10 @@ import android.text.TextUtils; import android.util.Log; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.FileProvider; + import org.jaudiotagger.audio.AudioFileIO; import org.jaudiotagger.tag.FieldKey; @@ -39,14 +43,12 @@ import java.util.List; import java.util.Locale; import java.util.regex.Pattern; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.FileProvider; import code.name.monkey.retromusic.R; import code.name.monkey.retromusic.helper.MusicPlayerRemote; import code.name.monkey.retromusic.loaders.PlaylistLoader; import code.name.monkey.retromusic.loaders.SongLoader; import code.name.monkey.retromusic.model.Artist; +import code.name.monkey.retromusic.model.Genre; import code.name.monkey.retromusic.model.Playlist; import code.name.monkey.retromusic.model.Song; import code.name.monkey.retromusic.model.lyrics.AbsSynchronizedLyrics; @@ -83,6 +85,12 @@ public class MusicUtil { } } + @NonNull + public static String getSongCountString(@NonNull final Context context, int songCount) { + final String songString = songCount == 1 ? context.getResources().getString(R.string.song) : context.getResources().getString(R.string.songs); + return songCount + " " + songString; + } + @NonNull public static String getSongInfoString(@NonNull Song song) { return MusicUtil.buildInfoString( @@ -91,24 +99,6 @@ public class MusicUtil { ); } - /** - * Build a concatenated string from the provided arguments - * The intended purpose is to show extra annotations - * to a music library item. - * Ex: for a given album --> buildInfoString(album.artist, album.songCount) - */ - public static String buildInfoString(@NonNull final String string1, @NonNull final String string2) { - if (TextUtils.isEmpty(string1)) { - //noinspection ConstantConditions - return TextUtils.isEmpty(string2) ? "" : string2; - } - if (TextUtils.isEmpty(string2)) { - //noinspection ConstantConditions - return TextUtils.isEmpty(string1) ? "" : string1; - } - return string1 + " • " + string2; - } - @NonNull public static String getArtistInfoString(@NonNull final Context context, @NonNull final Artist artist) { @@ -130,7 +120,7 @@ public class MusicUtil { return songCount + " " + songString; } - @NonNull + /*@NonNull public static String getPlaylistInfoString(@NonNull final Context context, @NonNull List songs) { final int songCount = songs.size(); @@ -143,6 +133,69 @@ public class MusicUtil { } return songCount + " " + songString + " • " + MusicUtil.getReadableDurationString(duration); + }*/ + + @NonNull + public static String getGenreInfoString(@NonNull final Context context, @NonNull final Genre genre) { + int songCount = genre.getSongCount(); + return MusicUtil.getSongCountString(context, songCount); + } + + @NonNull + public static String getPlaylistInfoString(@NonNull final Context context, @NonNull List songs) { + final long duration = getTotalDuration(context, songs); + + return MusicUtil.buildInfoString( + MusicUtil.getSongCountString(context, songs.size()), + MusicUtil.getReadableDurationString(duration) + ); + } + + + /** + * Build a concatenated string from the provided arguments + * The intended purpose is to show extra annotations + * to a music library item. + * Ex: for a given album --> buildInfoString(album.artist, album.songCount) + */ + @NonNull + public static String buildInfoString(@Nullable final String string1, @Nullable final String string2) { + // Skip empty strings + if (TextUtils.isEmpty(string1)) { + //noinspection ConstantConditions + return TextUtils.isEmpty(string2) ? "" : string2; + } + if (TextUtils.isEmpty(string2)) { + //noinspection ConstantConditions + return TextUtils.isEmpty(string1) ? "" : string1; + } + + return string1 + " • " + string2; + } + + /** + * Build a concatenated string from the provided arguments + * The intended purpose is to show extra annotations + * to a music library item. + * Ex: for a given album --> buildInfoString(album.artist, album.songCount) + */ + @NonNull + public static String buildInfoString(@Nullable final String string1, @Nullable final String string2, @NonNull final String string3) { + // Skip empty strings + if (TextUtils.isEmpty(string1)) { + //noinspection ConstantConditions + return TextUtils.isEmpty(string2) ? "" : string2; + } + if (TextUtils.isEmpty(string2)) { + //noinspection ConstantConditions + return TextUtils.isEmpty(string1) ? "" : string1; + } + if (TextUtils.isEmpty(string3)) { + //noinspection ConstantConditions + return TextUtils.isEmpty(string1) ? "" : string3; + } + + return string1 + " • " + string2 + " • " + string3; } public static String getReadableDurationString(long songDurationMillis) {