Added Deezer for loading Artist images
This commit is contained in:
parent
e082da1dcc
commit
b43f71cc32
6 changed files with 227 additions and 55 deletions
|
@ -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<Playlist>,
|
||||
@param:LayoutRes protected var itemLayoutRes: Int, cabHolder: CabHolder?) : AbsMultiSelectAdapter<PlaylistAdapter.ViewHolder, Playlist>(activity, cabHolder, R.menu.menu_playlists_selection) {
|
||||
var dataSet: ArrayList<Playlist>
|
||||
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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<InputStream> {
|
||||
@Volatile
|
||||
private var isCancelled: Boolean = false
|
||||
private var call: Call<LastFmArtist>? = null
|
||||
private var call: Call<DeezerResponse>? = null
|
||||
private var streamFetcher: OkHttpStreamFetcher? = null
|
||||
|
||||
|
||||
|
@ -62,37 +64,30 @@ class ArtistImageFetcher(private val context: Context,
|
|||
|
||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
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<LastFmArtist> {
|
||||
override fun onResponse(call: Call<LastFmArtist>, response: Response<LastFmArtist>) {
|
||||
if (!MusicUtil.isArtistNameUnknown(model.artistName) && RetroUtil.isAllowedToDownloadMetadata(context)) {
|
||||
call = deezerApiService.getArtistImage(model.artistName)
|
||||
call?.enqueue(object : Callback<DeezerResponse> {
|
||||
override fun onFailure(call: Call<DeezerResponse>, t: Throwable) {
|
||||
callback.onLoadFailed(Exception(t))
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call<DeezerResponse>, response: Response<DeezerResponse>) {
|
||||
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<LastFmArtist>, 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<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>? {
|
||||
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<ArtistImage, InputStream> {
|
||||
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<ArtistImage, InputStream> {
|
||||
return ArtistImageLoader(context, lastFMClient, okHttp)
|
||||
return ArtistImageLoader(context, deezerApiService, okHttp)
|
||||
}
|
||||
|
||||
override fun teardown() {}
|
||||
|
|
|
@ -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<Song> 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;
|
||||
|
|
|
@ -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<Song> 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<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) {
|
||||
|
|
Loading…
Reference in a new issue