mirror of
https://github.com/antebudimir/tempus.git
synced 2025-12-31 09:33:33 +00:00
feat: Enable downloading of song lyrics for offline viewing
This commit is contained in:
parent
8bb6c02e46
commit
c2b6d7eed5
12 changed files with 1652 additions and 67 deletions
1151
app/schemas/com.cappielloantonio.tempo.database.AppDatabase/12.json
Normal file
1151
app/schemas/com.cappielloantonio.tempo.database.AppDatabase/12.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -12,6 +12,7 @@ import com.cappielloantonio.tempo.database.converter.DateConverters;
|
|||
import com.cappielloantonio.tempo.database.dao.ChronologyDao;
|
||||
import com.cappielloantonio.tempo.database.dao.DownloadDao;
|
||||
import com.cappielloantonio.tempo.database.dao.FavoriteDao;
|
||||
import com.cappielloantonio.tempo.database.dao.LyricsDao;
|
||||
import com.cappielloantonio.tempo.database.dao.PlaylistDao;
|
||||
import com.cappielloantonio.tempo.database.dao.QueueDao;
|
||||
import com.cappielloantonio.tempo.database.dao.RecentSearchDao;
|
||||
|
|
@ -20,6 +21,7 @@ import com.cappielloantonio.tempo.database.dao.SessionMediaItemDao;
|
|||
import com.cappielloantonio.tempo.model.Chronology;
|
||||
import com.cappielloantonio.tempo.model.Download;
|
||||
import com.cappielloantonio.tempo.model.Favorite;
|
||||
import com.cappielloantonio.tempo.model.LyricsCache;
|
||||
import com.cappielloantonio.tempo.model.Queue;
|
||||
import com.cappielloantonio.tempo.model.RecentSearch;
|
||||
import com.cappielloantonio.tempo.model.Server;
|
||||
|
|
@ -28,9 +30,9 @@ import com.cappielloantonio.tempo.subsonic.models.Playlist;
|
|||
|
||||
@UnstableApi
|
||||
@Database(
|
||||
version = 11,
|
||||
entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class, SessionMediaItem.class, Playlist.class},
|
||||
autoMigrations = {@AutoMigration(from = 10, to = 11)}
|
||||
version = 12,
|
||||
entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class, SessionMediaItem.class, Playlist.class, LyricsCache.class},
|
||||
autoMigrations = {@AutoMigration(from = 10, to = 11), @AutoMigration(from = 11, to = 12)}
|
||||
)
|
||||
@TypeConverters({DateConverters.class})
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
|
|
@ -62,4 +64,6 @@ public abstract class AppDatabase extends RoomDatabase {
|
|||
public abstract SessionMediaItemDao sessionMediaItemDao();
|
||||
|
||||
public abstract PlaylistDao playlistDao();
|
||||
|
||||
public abstract LyricsDao lyricsDao();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
package com.cappielloantonio.tempo.database.dao;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Insert;
|
||||
import androidx.room.OnConflictStrategy;
|
||||
import androidx.room.Query;
|
||||
|
||||
import com.cappielloantonio.tempo.model.LyricsCache;
|
||||
|
||||
@Dao
|
||||
public interface LyricsDao {
|
||||
@Query("SELECT * FROM lyrics_cache WHERE song_id = :songId")
|
||||
LyricsCache getOne(String songId);
|
||||
|
||||
@Query("SELECT * FROM lyrics_cache WHERE song_id = :songId")
|
||||
LiveData<LyricsCache> observeOne(String songId);
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
void insert(LyricsCache lyricsCache);
|
||||
|
||||
@Query("DELETE FROM lyrics_cache WHERE song_id = :songId")
|
||||
void delete(String songId);
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.cappielloantonio.tempo.model
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlin.jvm.JvmOverloads
|
||||
|
||||
@Keep
|
||||
@Entity(tableName = "lyrics_cache")
|
||||
data class LyricsCache @JvmOverloads constructor(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "song_id")
|
||||
var songId: String,
|
||||
@ColumnInfo(name = "artist")
|
||||
var artist: String? = null,
|
||||
@ColumnInfo(name = "title")
|
||||
var title: String? = null,
|
||||
@ColumnInfo(name = "lyrics")
|
||||
var lyrics: String? = null,
|
||||
@ColumnInfo(name = "structured_lyrics")
|
||||
var structuredLyrics: String? = null,
|
||||
@ColumnInfo(name = "updated_at")
|
||||
var updatedAt: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package com.cappielloantonio.tempo.repository;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import com.cappielloantonio.tempo.database.AppDatabase;
|
||||
import com.cappielloantonio.tempo.database.dao.LyricsDao;
|
||||
import com.cappielloantonio.tempo.model.LyricsCache;
|
||||
|
||||
public class LyricsRepository {
|
||||
private final LyricsDao lyricsDao = AppDatabase.getInstance().lyricsDao();
|
||||
|
||||
public LyricsCache getLyrics(String songId) {
|
||||
GetLyricsThreadSafe getLyricsThreadSafe = new GetLyricsThreadSafe(lyricsDao, songId);
|
||||
Thread thread = new Thread(getLyricsThreadSafe);
|
||||
thread.start();
|
||||
|
||||
try {
|
||||
thread.join();
|
||||
return getLyricsThreadSafe.getLyrics();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public LiveData<LyricsCache> observeLyrics(String songId) {
|
||||
return lyricsDao.observeOne(songId);
|
||||
}
|
||||
|
||||
public void insert(LyricsCache lyricsCache) {
|
||||
InsertThreadSafe insert = new InsertThreadSafe(lyricsDao, lyricsCache);
|
||||
Thread thread = new Thread(insert);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
public void delete(String songId) {
|
||||
DeleteThreadSafe delete = new DeleteThreadSafe(lyricsDao, songId);
|
||||
Thread thread = new Thread(delete);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
private static class GetLyricsThreadSafe implements Runnable {
|
||||
private final LyricsDao lyricsDao;
|
||||
private final String songId;
|
||||
private LyricsCache lyricsCache;
|
||||
|
||||
public GetLyricsThreadSafe(LyricsDao lyricsDao, String songId) {
|
||||
this.lyricsDao = lyricsDao;
|
||||
this.songId = songId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
lyricsCache = lyricsDao.getOne(songId);
|
||||
}
|
||||
|
||||
public LyricsCache getLyrics() {
|
||||
return lyricsCache;
|
||||
}
|
||||
}
|
||||
|
||||
private static class InsertThreadSafe implements Runnable {
|
||||
private final LyricsDao lyricsDao;
|
||||
private final LyricsCache lyricsCache;
|
||||
|
||||
public InsertThreadSafe(LyricsDao lyricsDao, LyricsCache lyricsCache) {
|
||||
this.lyricsDao = lyricsDao;
|
||||
this.lyricsCache = lyricsCache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
lyricsDao.insert(lyricsCache);
|
||||
}
|
||||
}
|
||||
|
||||
private static class DeleteThreadSafe implements Runnable {
|
||||
private final LyricsDao lyricsDao;
|
||||
private final String songId;
|
||||
|
||||
public DeleteThreadSafe(LyricsDao lyricsDao, String songId) {
|
||||
this.lyricsDao = lyricsDao;
|
||||
this.songId = songId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
lyricsDao.delete(songId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,15 +4,16 @@ import android.annotation.SuppressLint;
|
|||
import android.content.ComponentName;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.text.Layout;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Layout;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
|
@ -29,10 +30,10 @@ import com.cappielloantonio.tempo.service.MediaService;
|
|||
import com.cappielloantonio.tempo.subsonic.models.Line;
|
||||
import com.cappielloantonio.tempo.subsonic.models.LyricsList;
|
||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.cappielloantonio.tempo.util.OpenSubsonicExtensionsUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
|
||||
import java.util.List;
|
||||
|
|
@ -48,6 +49,9 @@ public class PlayerLyricsFragment extends Fragment {
|
|||
private MediaBrowser mediaBrowser;
|
||||
private Handler syncLyricsHandler;
|
||||
private Runnable syncLyricsRunnable;
|
||||
private String currentLyrics;
|
||||
private LyricsList currentLyricsList;
|
||||
private String currentDescription;
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
|
|
@ -66,6 +70,7 @@ public class PlayerLyricsFragment extends Fragment {
|
|||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
initPanelContent();
|
||||
observeDownloadState();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -101,12 +106,26 @@ public class PlayerLyricsFragment extends Fragment {
|
|||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
bind = null;
|
||||
currentLyrics = null;
|
||||
currentLyricsList = null;
|
||||
currentDescription = null;
|
||||
}
|
||||
|
||||
private void initOverlay() {
|
||||
bind.syncLyricsTapButton.setOnClickListener(view -> {
|
||||
playerBottomSheetViewModel.changeSyncLyricsState();
|
||||
});
|
||||
|
||||
bind.downloadLyricsButton.setOnClickListener(view -> {
|
||||
boolean saved = playerBottomSheetViewModel.downloadCurrentLyrics();
|
||||
if (getContext() != null) {
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
saved ? R.string.player_lyrics_download_success : R.string.player_lyrics_download_failure,
|
||||
Toast.LENGTH_SHORT
|
||||
).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeBrowser() {
|
||||
|
|
@ -136,48 +155,89 @@ public class PlayerLyricsFragment extends Fragment {
|
|||
}
|
||||
|
||||
private void initPanelContent() {
|
||||
if (OpenSubsonicExtensionsUtil.isSongLyricsExtensionAvailable()) {
|
||||
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
||||
setPanelContent(null, lyricsList);
|
||||
});
|
||||
} else {
|
||||
playerBottomSheetViewModel.getLiveLyrics().observe(getViewLifecycleOwner(), lyrics -> {
|
||||
setPanelContent(lyrics, null);
|
||||
currentLyrics = lyrics;
|
||||
updatePanelContent();
|
||||
});
|
||||
|
||||
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
||||
currentLyricsList = lyricsList;
|
||||
updatePanelContent();
|
||||
});
|
||||
|
||||
playerBottomSheetViewModel.getLiveDescription().observe(getViewLifecycleOwner(), description -> {
|
||||
currentDescription = description;
|
||||
updatePanelContent();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void setPanelContent(String lyrics, LyricsList lyricsList) {
|
||||
playerBottomSheetViewModel.getLiveDescription().observe(getViewLifecycleOwner(), description -> {
|
||||
private void observeDownloadState() {
|
||||
playerBottomSheetViewModel.getLyricsCachedState().observe(getViewLifecycleOwner(), cached -> {
|
||||
if (bind != null) {
|
||||
MaterialButton downloadButton = (MaterialButton) bind.downloadLyricsButton;
|
||||
if (cached != null && cached) {
|
||||
downloadButton.setIconResource(R.drawable.ic_done);
|
||||
downloadButton.setContentDescription(getString(R.string.player_lyrics_downloaded_content_description));
|
||||
} else {
|
||||
downloadButton.setIconResource(R.drawable.ic_download);
|
||||
downloadButton.setContentDescription(getString(R.string.player_lyrics_download_content_description));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updatePanelContent() {
|
||||
if (bind == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, 0);
|
||||
|
||||
if (lyrics != null && !lyrics.trim().equals("")) {
|
||||
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(lyrics));
|
||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
||||
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
||||
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
||||
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
||||
} else if (lyricsList != null && lyricsList.getStructuredLyrics() != null) {
|
||||
setSyncLirics(lyricsList);
|
||||
if (hasStructuredLyrics(currentLyricsList)) {
|
||||
setSyncLirics(currentLyricsList);
|
||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
||||
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
||||
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
||||
bind.syncLyricsTapButton.setVisibility(View.VISIBLE);
|
||||
} else if (description != null && !description.trim().equals("")) {
|
||||
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(description));
|
||||
bind.downloadLyricsButton.setVisibility(View.VISIBLE);
|
||||
bind.downloadLyricsButton.setEnabled(true);
|
||||
} else if (hasText(currentLyrics)) {
|
||||
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(currentLyrics));
|
||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
||||
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
||||
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
||||
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
||||
bind.downloadLyricsButton.setVisibility(View.VISIBLE);
|
||||
bind.downloadLyricsButton.setEnabled(true);
|
||||
} else if (hasText(currentDescription)) {
|
||||
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(currentDescription));
|
||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
||||
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
||||
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
||||
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
||||
bind.downloadLyricsButton.setVisibility(View.GONE);
|
||||
bind.downloadLyricsButton.setEnabled(false);
|
||||
} else {
|
||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.GONE);
|
||||
bind.emptyDescriptionImageView.setVisibility(View.VISIBLE);
|
||||
bind.titleEmptyDescriptionLabel.setVisibility(View.VISIBLE);
|
||||
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
||||
bind.downloadLyricsButton.setVisibility(View.GONE);
|
||||
bind.downloadLyricsButton.setEnabled(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
private boolean hasText(String value) {
|
||||
return value != null && !value.trim().isEmpty();
|
||||
}
|
||||
|
||||
private boolean hasStructuredLyrics(LyricsList lyricsList) {
|
||||
return lyricsList != null
|
||||
&& lyricsList.getStructuredLyrics() != null
|
||||
&& !lyricsList.getStructuredLyrics().isEmpty()
|
||||
&& lyricsList.getStructuredLyrics().get(0) != null
|
||||
&& lyricsList.getStructuredLyrics().get(0).getLine() != null
|
||||
&& !lyricsList.getStructuredLyrics().get(0).getLine().isEmpty();
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
|
|
@ -198,9 +258,12 @@ public class PlayerLyricsFragment extends Fragment {
|
|||
|
||||
private void defineProgressHandler() {
|
||||
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
||||
if (lyricsList != null) {
|
||||
if (!hasStructuredLyrics(lyricsList)) {
|
||||
releaseHandler();
|
||||
return;
|
||||
}
|
||||
|
||||
if (lyricsList.getStructuredLyrics() != null && lyricsList.getStructuredLyrics().get(0) != null && !lyricsList.getStructuredLyrics().get(0).getSynced()) {
|
||||
if (!lyricsList.getStructuredLyrics().get(0).getSynced()) {
|
||||
releaseHandler();
|
||||
return;
|
||||
}
|
||||
|
|
@ -217,9 +280,6 @@ public class PlayerLyricsFragment extends Fragment {
|
|||
};
|
||||
|
||||
syncLyricsHandler.postDelayed(syncLyricsRunnable, 250);
|
||||
} else {
|
||||
releaseHandler();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -227,7 +287,7 @@ public class PlayerLyricsFragment extends Fragment {
|
|||
LyricsList lyricsList = playerBottomSheetViewModel.getLiveLyricsList().getValue();
|
||||
int timestamp = (int) (mediaBrowser.getCurrentPosition());
|
||||
|
||||
if (lyricsList != null && lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) {
|
||||
if (hasStructuredLyrics(lyricsList)) {
|
||||
StringBuilder lyricsBuilder = new StringBuilder();
|
||||
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();
|
||||
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||
actionChangeDownloadStorage();
|
||||
actionDeleteDownloadStorage();
|
||||
actionKeepScreenOn();
|
||||
actionAutoDownloadLyrics();
|
||||
|
||||
bindMediaService();
|
||||
actionAppEqualizer();
|
||||
|
|
@ -357,6 +358,21 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||
});
|
||||
}
|
||||
|
||||
private void actionAutoDownloadLyrics() {
|
||||
SwitchPreference preference = findPreference("auto_download_lyrics");
|
||||
if (preference == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
preference.setChecked(Preferences.isAutoDownloadLyricsEnabled());
|
||||
preference.setOnPreferenceChangeListener((pref, newValue) -> {
|
||||
if (newValue instanceof Boolean) {
|
||||
Preferences.setAutoDownloadLyricsEnabled((Boolean) newValue);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void getScanStatus() {
|
||||
settingViewModel.getScanStatus(new ScanCallback() {
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ object Preferences {
|
|||
private const val ROUNDED_CORNER_SIZE = "rounded_corner_size"
|
||||
private const val PODCAST_SECTION_VISIBILITY = "podcast_section_visibility"
|
||||
private const val RADIO_SECTION_VISIBILITY = "radio_section_visibility"
|
||||
private const val AUTO_DOWNLOAD_LYRICS = "auto_download_lyrics"
|
||||
private const val MUSIC_DIRECTORY_SECTION_VISIBILITY = "music_directory_section_visibility"
|
||||
private const val REPLAY_GAIN_MODE = "replay_gain_mode"
|
||||
private const val AUDIO_TRANSCODE_PRIORITY = "audio_transcode_priority"
|
||||
|
|
@ -163,6 +164,24 @@ object Preferences {
|
|||
App.getInstance().preferences.edit().putString(OPEN_SUBSONIC_EXTENSIONS, Gson().toJson(extension)).apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isAutoDownloadLyricsEnabled(): Boolean {
|
||||
val preferences = App.getInstance().preferences
|
||||
|
||||
if (preferences.contains(AUTO_DOWNLOAD_LYRICS)) {
|
||||
return preferences.getBoolean(AUTO_DOWNLOAD_LYRICS, false)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setAutoDownloadLyricsEnabled(isEnabled: Boolean) {
|
||||
App.getInstance().preferences.edit()
|
||||
.putBoolean(AUTO_DOWNLOAD_LYRICS, isEnabled)
|
||||
.apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getLocalAddress(): String? {
|
||||
return App.getInstance().preferences.getString(LOCAL_ADDRESS, null)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.viewmodel;
|
|||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.OptIn;
|
||||
|
|
@ -9,14 +10,17 @@ import androidx.lifecycle.AndroidViewModel;
|
|||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
|
||||
import com.cappielloantonio.tempo.interfaces.StarCallback;
|
||||
import com.cappielloantonio.tempo.model.Download;
|
||||
import com.cappielloantonio.tempo.model.LyricsCache;
|
||||
import com.cappielloantonio.tempo.model.Queue;
|
||||
import com.cappielloantonio.tempo.repository.AlbumRepository;
|
||||
import com.cappielloantonio.tempo.repository.ArtistRepository;
|
||||
import com.cappielloantonio.tempo.repository.FavoriteRepository;
|
||||
import com.cappielloantonio.tempo.repository.LyricsRepository;
|
||||
import com.cappielloantonio.tempo.repository.OpenRepository;
|
||||
import com.cappielloantonio.tempo.repository.QueueRepository;
|
||||
import com.cappielloantonio.tempo.repository.SongRepository;
|
||||
|
|
@ -31,6 +35,7 @@ import com.cappielloantonio.tempo.util.MappingUtil;
|
|||
import com.cappielloantonio.tempo.util.NetworkUtil;
|
||||
import com.cappielloantonio.tempo.util.OpenSubsonicExtensionsUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
|
|
@ -47,14 +52,20 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
|||
private final QueueRepository queueRepository;
|
||||
private final FavoriteRepository favoriteRepository;
|
||||
private final OpenRepository openRepository;
|
||||
private final LyricsRepository lyricsRepository;
|
||||
private final MutableLiveData<String> lyricsLiveData = new MutableLiveData<>(null);
|
||||
private final MutableLiveData<LyricsList> lyricsListLiveData = new MutableLiveData<>(null);
|
||||
private final MutableLiveData<Boolean> lyricsCachedLiveData = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<String> descriptionLiveData = new MutableLiveData<>(null);
|
||||
private final MutableLiveData<Child> liveMedia = new MutableLiveData<>(null);
|
||||
private final MutableLiveData<AlbumID3> liveAlbum = new MutableLiveData<>(null);
|
||||
private final MutableLiveData<ArtistID3> liveArtist = new MutableLiveData<>(null);
|
||||
private final MutableLiveData<List<Child>> instantMix = new MutableLiveData<>(null);
|
||||
private final Gson gson = new Gson();
|
||||
private boolean lyricsSyncState = true;
|
||||
private LiveData<LyricsCache> cachedLyricsSource;
|
||||
private String currentSongId;
|
||||
private final Observer<LyricsCache> cachedLyricsObserver = this::onCachedLyricsChanged;
|
||||
|
||||
|
||||
public PlayerBottomSheetViewModel(@NonNull Application application) {
|
||||
|
|
@ -66,6 +77,7 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
|||
queueRepository = new QueueRepository();
|
||||
favoriteRepository = new FavoriteRepository();
|
||||
openRepository = new OpenRepository();
|
||||
lyricsRepository = new LyricsRepository();
|
||||
}
|
||||
|
||||
public LiveData<List<Queue>> getQueueSong() {
|
||||
|
|
@ -139,12 +151,49 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
|||
}
|
||||
|
||||
public void refreshMediaInfo(LifecycleOwner owner, Child media) {
|
||||
if (OpenSubsonicExtensionsUtil.isSongLyricsExtensionAvailable()) {
|
||||
openRepository.getLyricsBySongId(media.getId()).observe(owner, lyricsListLiveData::postValue);
|
||||
lyricsLiveData.postValue(null);
|
||||
} else {
|
||||
songRepository.getSongLyrics(media).observe(owner, lyricsLiveData::postValue);
|
||||
lyricsListLiveData.postValue(null);
|
||||
lyricsCachedLiveData.postValue(false);
|
||||
|
||||
clearCachedLyricsObserver();
|
||||
|
||||
String songId = media != null ? media.getId() : currentSongId;
|
||||
|
||||
if (TextUtils.isEmpty(songId) || owner == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentSongId = songId;
|
||||
|
||||
observeCachedLyrics(owner, songId);
|
||||
|
||||
LyricsCache cachedLyrics = lyricsRepository.getLyrics(songId);
|
||||
if (cachedLyrics != null) {
|
||||
onCachedLyricsChanged(cachedLyrics);
|
||||
}
|
||||
|
||||
if (NetworkUtil.isOffline() || media == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (OpenSubsonicExtensionsUtil.isSongLyricsExtensionAvailable()) {
|
||||
openRepository.getLyricsBySongId(media.getId()).observe(owner, lyricsList -> {
|
||||
lyricsListLiveData.postValue(lyricsList);
|
||||
lyricsLiveData.postValue(null);
|
||||
|
||||
if (shouldAutoDownloadLyrics() && hasStructuredLyrics(lyricsList)) {
|
||||
saveLyricsToCache(media, null, lyricsList);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
songRepository.getSongLyrics(media).observe(owner, lyrics -> {
|
||||
lyricsLiveData.postValue(lyrics);
|
||||
lyricsListLiveData.postValue(null);
|
||||
|
||||
if (shouldAutoDownloadLyrics() && !TextUtils.isEmpty(lyrics)) {
|
||||
saveLyricsToCache(media, lyrics, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -153,6 +202,17 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
|||
}
|
||||
|
||||
public void setLiveMedia(LifecycleOwner owner, String mediaType, String mediaId) {
|
||||
currentSongId = mediaId;
|
||||
|
||||
if (!TextUtils.isEmpty(mediaId)) {
|
||||
refreshMediaInfo(owner, null);
|
||||
} else {
|
||||
clearCachedLyricsObserver();
|
||||
lyricsLiveData.postValue(null);
|
||||
lyricsListLiveData.postValue(null);
|
||||
lyricsCachedLiveData.postValue(false);
|
||||
}
|
||||
|
||||
if (mediaType != null) {
|
||||
switch (mediaType) {
|
||||
case Constants.MEDIA_TYPE_MUSIC:
|
||||
|
|
@ -162,7 +222,12 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
|||
case Constants.MEDIA_TYPE_PODCAST:
|
||||
liveMedia.postValue(null);
|
||||
break;
|
||||
default:
|
||||
liveMedia.postValue(null);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
liveMedia.postValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -233,6 +298,105 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
|||
return false;
|
||||
}
|
||||
|
||||
private void observeCachedLyrics(LifecycleOwner owner, String songId) {
|
||||
if (TextUtils.isEmpty(songId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
cachedLyricsSource = lyricsRepository.observeLyrics(songId);
|
||||
cachedLyricsSource.observe(owner, cachedLyricsObserver);
|
||||
}
|
||||
|
||||
private void clearCachedLyricsObserver() {
|
||||
if (cachedLyricsSource != null) {
|
||||
cachedLyricsSource.removeObserver(cachedLyricsObserver);
|
||||
cachedLyricsSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void onCachedLyricsChanged(LyricsCache lyricsCache) {
|
||||
if (lyricsCache == null) {
|
||||
lyricsCachedLiveData.postValue(false);
|
||||
return;
|
||||
}
|
||||
|
||||
lyricsCachedLiveData.postValue(true);
|
||||
|
||||
if (!TextUtils.isEmpty(lyricsCache.getStructuredLyrics())) {
|
||||
try {
|
||||
LyricsList cachedList = gson.fromJson(lyricsCache.getStructuredLyrics(), LyricsList.class);
|
||||
lyricsListLiveData.postValue(cachedList);
|
||||
lyricsLiveData.postValue(null);
|
||||
} catch (Exception exception) {
|
||||
lyricsListLiveData.postValue(null);
|
||||
lyricsLiveData.postValue(lyricsCache.getLyrics());
|
||||
}
|
||||
} else {
|
||||
lyricsListLiveData.postValue(null);
|
||||
lyricsLiveData.postValue(lyricsCache.getLyrics());
|
||||
}
|
||||
}
|
||||
|
||||
private void saveLyricsToCache(Child media, String lyrics, LyricsList lyricsList) {
|
||||
if (media == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((lyricsList == null || !hasStructuredLyrics(lyricsList)) && TextUtils.isEmpty(lyrics)) {
|
||||
return;
|
||||
}
|
||||
|
||||
LyricsCache lyricsCache = new LyricsCache(media.getId());
|
||||
lyricsCache.setArtist(media.getArtist());
|
||||
lyricsCache.setTitle(media.getTitle());
|
||||
lyricsCache.setUpdatedAt(System.currentTimeMillis());
|
||||
|
||||
if (lyricsList != null && hasStructuredLyrics(lyricsList)) {
|
||||
lyricsCache.setStructuredLyrics(gson.toJson(lyricsList));
|
||||
lyricsCache.setLyrics(null);
|
||||
} else {
|
||||
lyricsCache.setLyrics(lyrics);
|
||||
lyricsCache.setStructuredLyrics(null);
|
||||
}
|
||||
|
||||
lyricsRepository.insert(lyricsCache);
|
||||
lyricsCachedLiveData.postValue(true);
|
||||
}
|
||||
|
||||
private boolean hasStructuredLyrics(LyricsList lyricsList) {
|
||||
return lyricsList != null
|
||||
&& lyricsList.getStructuredLyrics() != null
|
||||
&& !lyricsList.getStructuredLyrics().isEmpty()
|
||||
&& lyricsList.getStructuredLyrics().get(0) != null
|
||||
&& lyricsList.getStructuredLyrics().get(0).getLine() != null
|
||||
&& !lyricsList.getStructuredLyrics().get(0).getLine().isEmpty();
|
||||
}
|
||||
|
||||
private boolean shouldAutoDownloadLyrics() {
|
||||
return Preferences.isAutoDownloadLyricsEnabled();
|
||||
}
|
||||
|
||||
public boolean downloadCurrentLyrics() {
|
||||
Child media = getLiveMedia().getValue();
|
||||
if (media == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LyricsList lyricsList = lyricsListLiveData.getValue();
|
||||
String lyrics = lyricsLiveData.getValue();
|
||||
|
||||
if ((lyricsList == null || !hasStructuredLyrics(lyricsList)) && TextUtils.isEmpty(lyrics)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
saveLyricsToCache(media, lyrics, lyricsList);
|
||||
return true;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getLyricsCachedState() {
|
||||
return lyricsCachedLiveData;
|
||||
}
|
||||
|
||||
public void changeSyncLyricsState() {
|
||||
lyricsSyncState = !lyricsSyncState;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,25 @@
|
|||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<Button
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/download_lyrics_button"
|
||||
style="@style/Widget.Material3.Button.TonalButton.Icon"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_margin="16dp"
|
||||
android:alpha="0.7"
|
||||
android:contentDescription="@string/player_lyrics_download_content_description"
|
||||
android:insetLeft="0dp"
|
||||
android:insetTop="0dp"
|
||||
android:insetRight="0dp"
|
||||
android:insetBottom="0dp"
|
||||
android:visibility="gone"
|
||||
app:cornerRadius="64dp"
|
||||
app:icon="@drawable/ic_download"
|
||||
app:layout_constraintBottom_toTopOf="@+id/sync_lyrics_tap_button"
|
||||
app:layout_constraintEnd_toEndOf="@+id/now_playing_song_lyrics_sroll_view" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/sync_lyrics_tap_button"
|
||||
style="@style/Widget.Material3.Button.TonalButton.Icon"
|
||||
android:layout_width="48dp"
|
||||
|
|
|
|||
|
|
@ -199,6 +199,10 @@
|
|||
<string name="player_playback_speed">%1$.2fx</string>
|
||||
<string name="player_queue_clean_all_button">Clean play queue</string>
|
||||
<string name="player_queue_save_queue_success">Saved play queue</string>
|
||||
<string name="player_lyrics_download_content_description">Download lyrics for offline playback</string>
|
||||
<string name="player_lyrics_downloaded_content_description">Lyrics downloaded for offline playback</string>
|
||||
<string name="player_lyrics_download_success">Lyrics saved for offline playback.</string>
|
||||
<string name="player_lyrics_download_failure">Lyrics are not available to download.</string>
|
||||
<string name="player_server_priority">Server Priority</string>
|
||||
<string name="player_unknown_format">Unknown format</string>
|
||||
<string name="player_transcoding">Transcoding</string>
|
||||
|
|
@ -327,6 +331,8 @@
|
|||
<string name="settings_queue_syncing_title">Sync play queue for this user [Not Fully Baked]</string>
|
||||
<string name="settings_radio">Show radio</string>
|
||||
<string name="settings_radio_summary">If enabled, show the radio section. Restart the app for it to take full effect.</string>
|
||||
<string name="settings_auto_download_lyrics">Auto download lyrics</string>
|
||||
<string name="settings_auto_download_lyrics_summary">Automatically save lyrics when they are available so they can be shown while offline.</string>
|
||||
<string name="settings_replay_gain">Set replay gain mode</string>
|
||||
<string name="settings_rounded_corner">Rounded corners</string>
|
||||
<string name="settings_rounded_corner_size">Corners size</string>
|
||||
|
|
|
|||
|
|
@ -86,6 +86,12 @@
|
|||
android:summary="@string/settings_radio_summary"
|
||||
android:key="radio_section_visibility" />
|
||||
|
||||
<SwitchPreference
|
||||
android:title="@string/settings_auto_download_lyrics"
|
||||
android:defaultValue="false"
|
||||
android:summary="@string/settings_auto_download_lyrics_summary"
|
||||
android:key="auto_download_lyrics" />
|
||||
|
||||
<SwitchPreference
|
||||
android:title="@string/settings_music_directory"
|
||||
android:defaultValue="true"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue