Added Deezer for loading Artist images

main
h4h13 2019-06-06 21:57:42 +05:30
parent e082da1dcc
commit b43f71cc32
6 changed files with 227 additions and 55 deletions

View File

@ -29,6 +29,7 @@ import code.name.monkey.retromusic.util.MusicUtil
import code.name.monkey.retromusic.util.NavigationUtil import code.name.monkey.retromusic.util.NavigationUtil
import java.util.* import java.util.*
class PlaylistAdapter(protected val activity: AppCompatActivity, dataSet: ArrayList<Playlist>, class PlaylistAdapter(protected val activity: AppCompatActivity, dataSet: ArrayList<Playlist>,
@param:LayoutRes protected var itemLayoutRes: Int, cabHolder: CabHolder?) : AbsMultiSelectAdapter<PlaylistAdapter.ViewHolder, Playlist>(activity, cabHolder, R.menu.menu_playlists_selection) { @param:LayoutRes protected var itemLayoutRes: Int, cabHolder: CabHolder?) : AbsMultiSelectAdapter<PlaylistAdapter.ViewHolder, Playlist>(activity, cabHolder, R.menu.menu_playlists_selection) {
var dataSet: ArrayList<Playlist> var dataSet: ArrayList<Playlist>
@ -60,6 +61,14 @@ class PlaylistAdapter(protected val activity: AppCompatActivity, dataSet: ArrayL
return ViewHolder(view) 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) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val playlist = dataSet[position] val playlist = dataSet[position]
@ -67,10 +76,10 @@ class PlaylistAdapter(protected val activity: AppCompatActivity, dataSet: ArrayL
holder.itemView.isActivated = isChecked(playlist) holder.itemView.isActivated = isChecked(playlist)
if (holder.title != null) { if (holder.title != null) {
holder.title!!.text = playlist.name holder.title!!.text = getPlaylistTitle(playlist)
} }
if (holder.text != null) { if (holder.text != null) {
holder.text!!.text = String.format(Locale.getDefault(), "%d Songs", songs!!.size) holder.text!!.text = getPlaylistText(playlist)
} }
if (holder.image != null) { if (holder.image != null) {
holder.image!!.setImageDrawable(getIconRes(playlist)) holder.image!!.setImageDrawable(getIconRes(playlist))

View File

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

View File

@ -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<Data>,
val next: String,
val total: Int
)

View File

@ -17,6 +17,8 @@ package code.name.monkey.retromusic.glide.artistimage
import android.content.Context import android.content.Context
import android.text.TextUtils import android.text.TextUtils
import code.name.monkey.retromusic.App 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.LastFMRestClient
import code.name.monkey.retromusic.rest.model.LastFmArtist import code.name.monkey.retromusic.rest.model.LastFmArtist
import code.name.monkey.retromusic.util.LastFMUtil 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 ArtistImage(val artistName: String, val skipOkHttpCache: Boolean)
class ArtistImageFetcher(private val context: Context, class ArtistImageFetcher(private val context: Context,
private val lastFMRestClient: LastFMRestClient, private val deezerApiService: DeezerApiService,
private val okHttp: OkHttpClient, private val okHttp: OkHttpClient,
private val model: ArtistImage) : DataFetcher<InputStream> { private val model: ArtistImage) : DataFetcher<InputStream> {
@Volatile @Volatile
private var isCancelled: Boolean = false private var isCancelled: Boolean = false
private var call: Call<LastFmArtist>? = null private var call: Call<DeezerResponse>? = null
private var streamFetcher: OkHttpStreamFetcher? = null private var streamFetcher: OkHttpStreamFetcher? = null
@ -63,36 +65,29 @@ class ArtistImageFetcher(private val context: Context,
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) { override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
try { try {
if (!MusicUtil.isArtistNameUnknown(model.artistName) && RetroUtil.isAllowedToDownloadMetadata(context)) { if (!MusicUtil.isArtistNameUnknown(model.artistName) && RetroUtil.isAllowedToDownloadMetadata(context)) {
call = lastFMRestClient.apiService.getArtistInfo(model.artistName, null, if (model.skipOkHttpCache) "no-cache" else null) call = deezerApiService.getArtistImage(model.artistName)
call!!.enqueue(object : Callback<LastFmArtist> { call?.enqueue(object : Callback<DeezerResponse> {
override fun onResponse(call: Call<LastFmArtist>, response: Response<LastFmArtist>) { override fun onFailure(call: Call<DeezerResponse>, t: Throwable) {
callback.onLoadFailed(Exception(t))
}
override fun onResponse(call: Call<DeezerResponse>, response: Response<DeezerResponse>) {
if (isCancelled) { if (isCancelled) {
callback.onDataReady(null) callback.onDataReady(null)
return return
} }
try {
val lastFmArtist = response.body() val deezerResponse: DeezerResponse? = response.body()
if (lastFmArtist == null || lastFmArtist.artist == null || lastFmArtist.artist.image == null) { println(deezerResponse)
callback.onLoadFailed(Exception("No artist image url found")) val url = deezerResponse?.data?.get(0)?.pictureXl
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 = OkHttpStreamFetcher(okHttp, GlideUrl(url))
streamFetcher!!.loadData(priority, callback) streamFetcher?.loadData(priority, callback)
} catch (e: Exception) {
callback.onLoadFailed(Exception("No artist image url found"))
}
} }
override fun onFailure(call: Call<LastFmArtist>, throwable: Throwable) {
callback.onLoadFailed(Exception(throwable))
}
}) })
} }
} catch (e: Exception) { } catch (e: Exception) {
callback.onLoadFailed(e) callback.onLoadFailed(e)
@ -108,9 +103,7 @@ class ArtistImageFetcher(private val context: Context,
override fun cancel() { override fun cancel() {
isCancelled = true isCancelled = true
if (call != null) { call?.cancel()
call!!.cancel()
}
if (streamFetcher != null) { if (streamFetcher != null) {
streamFetcher!!.cancel() 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<ArtistImage, InputStream> { class ArtistImageLoader(private val context: Context,
private val deezerApiService: DeezerApiService,
private val okhttp: OkHttpClient) : ModelLoader<ArtistImage, InputStream> {
override fun buildLoadData(model: ArtistImage, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream>? { override fun buildLoadData(model: ArtistImage, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream>? {
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 { 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<ArtistImage, InputStream> { class Factory(private val context: Context) : ModelLoaderFactory<ArtistImage, InputStream> {
private val lastFMClient: LastFMRestClient = LastFMRestClient(LastFMRestClient.createDefaultOkHttpClientBuilder(context) private val deezerApiService: DeezerApiService = DeezerApiService.invoke(DeezerApiService.createDefaultOkHttpClient(context)
.connectTimeout(TIMEOUT.toLong(), TimeUnit.MILLISECONDS) .connectTimeout(TIMEOUT.toLong(), TimeUnit.MILLISECONDS)
.readTimeout(TIMEOUT.toLong(), TimeUnit.MILLISECONDS) .readTimeout(TIMEOUT.toLong(), TimeUnit.MILLISECONDS)
.writeTimeout(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<ArtistImage, InputStream> { override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<ArtistImage, InputStream> {
return ArtistImageLoader(context, lastFMClient, okHttp) return ArtistImageLoader(context, deezerApiService, okHttp)
} }
override fun teardown() {} override fun teardown() {}

View File

@ -14,9 +14,17 @@
package code.name.monkey.retromusic.model; package code.name.monkey.retromusic.model;
import android.content.Context;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; 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) * @author Karim Abou Zeid (kabouzeid)
*/ */
@ -34,6 +42,19 @@ public class Playlist implements Parcelable {
this.name = ""; 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<Song> getSongs(Context context) {
// this default implementation covers static playlists
return PlaylistSongsLoader.INSTANCE.getPlaylistSongList(context, id).blockingFirst();
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View File

@ -29,6 +29,10 @@ import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.widget.Toast; 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.audio.AudioFileIO;
import org.jaudiotagger.tag.FieldKey; import org.jaudiotagger.tag.FieldKey;
@ -39,14 +43,12 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.regex.Pattern; 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.R;
import code.name.monkey.retromusic.helper.MusicPlayerRemote; import code.name.monkey.retromusic.helper.MusicPlayerRemote;
import code.name.monkey.retromusic.loaders.PlaylistLoader; import code.name.monkey.retromusic.loaders.PlaylistLoader;
import code.name.monkey.retromusic.loaders.SongLoader; import code.name.monkey.retromusic.loaders.SongLoader;
import code.name.monkey.retromusic.model.Artist; 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.Playlist;
import code.name.monkey.retromusic.model.Song; import code.name.monkey.retromusic.model.Song;
import code.name.monkey.retromusic.model.lyrics.AbsSynchronizedLyrics; 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 @NonNull
public static String getSongInfoString(@NonNull Song song) { public static String getSongInfoString(@NonNull Song song) {
return MusicUtil.buildInfoString( 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 @NonNull
public static String getArtistInfoString(@NonNull final Context context, public static String getArtistInfoString(@NonNull final Context context,
@NonNull final Artist artist) { @NonNull final Artist artist) {
@ -130,7 +120,7 @@ public class MusicUtil {
return songCount + " " + songString; return songCount + " " + songString;
} }
@NonNull /*@NonNull
public static String getPlaylistInfoString(@NonNull final Context context, public static String getPlaylistInfoString(@NonNull final Context context,
@NonNull List<Song> songs) { @NonNull List<Song> songs) {
final int songCount = songs.size(); final int songCount = songs.size();
@ -143,6 +133,69 @@ public class MusicUtil {
} }
return songCount + " " + songString + " • " + MusicUtil.getReadableDurationString(duration); 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<Song> 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) { public static String getReadableDurationString(long songDurationMillis) {