diff --git a/app/build.gradle b/app/build.gradle index d8b89902..8b55c1c3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -39,6 +39,7 @@ dependencies { // Jellyfin implementation 'com.github.jellyfin.jellyfin-apiclient-java:android:0.7.7' + // implementation "org.jellyfin.sdk:jellyfin-platform-android:1.0.0-beta.3" // Kotlin implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.4.32' diff --git a/app/src/main/java/com/cappielloantonio/play/database/AppDatabase.java b/app/src/main/java/com/cappielloantonio/play/database/AppDatabase.java index 383c69fa..45b05ebc 100644 --- a/app/src/main/java/com/cappielloantonio/play/database/AppDatabase.java +++ b/app/src/main/java/com/cappielloantonio/play/database/AppDatabase.java @@ -11,6 +11,7 @@ import com.cappielloantonio.play.database.dao.AlbumDao; import com.cappielloantonio.play.database.dao.ArtistDao; import com.cappielloantonio.play.database.dao.GenreDao; import com.cappielloantonio.play.database.dao.PlaylistDao; +import com.cappielloantonio.play.database.dao.PlaylistSongCrossDao; import com.cappielloantonio.play.database.dao.QueueDao; import com.cappielloantonio.play.database.dao.RecentSearchDao; import com.cappielloantonio.play.database.dao.SongArtistCrossDao; @@ -21,13 +22,14 @@ import com.cappielloantonio.play.model.AlbumArtistCross; import com.cappielloantonio.play.model.Artist; import com.cappielloantonio.play.model.Genre; import com.cappielloantonio.play.model.Playlist; +import com.cappielloantonio.play.model.PlaylistSongCross; import com.cappielloantonio.play.model.Queue; import com.cappielloantonio.play.model.RecentSearch; import com.cappielloantonio.play.model.Song; import com.cappielloantonio.play.model.SongArtistCross; import com.cappielloantonio.play.model.SongGenreCross; -@Database(entities = {Album.class, Artist.class, Genre.class, Playlist.class, Song.class, RecentSearch.class, SongGenreCross.class, Queue.class, AlbumArtistCross.class, SongArtistCross.class}, version = 10, exportSchema = false) +@Database(entities = {Album.class, Artist.class, Genre.class, Playlist.class, Song.class, RecentSearch.class, SongGenreCross.class, Queue.class, AlbumArtistCross.class, SongArtistCross.class, PlaylistSongCross.class}, version = 10, exportSchema = false) public abstract class AppDatabase extends RoomDatabase { private static final String TAG = "AppDatabase"; @@ -62,5 +64,7 @@ public abstract class AppDatabase extends RoomDatabase { public abstract SongArtistCrossDao songArtistCrossDao(); + public abstract PlaylistSongCrossDao playlistSongCrossDao(); + public abstract QueueDao queueDao(); } diff --git a/app/src/main/java/com/cappielloantonio/play/database/dao/PlaylistSongCrossDao.java b/app/src/main/java/com/cappielloantonio/play/database/dao/PlaylistSongCrossDao.java new file mode 100644 index 00000000..248a79d3 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/database/dao/PlaylistSongCrossDao.java @@ -0,0 +1,38 @@ +package com.cappielloantonio.play.database.dao; + +import androidx.lifecycle.LiveData; +import androidx.room.Dao; +import androidx.room.Delete; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; +import androidx.room.Update; + +import com.cappielloantonio.play.model.PlaylistSongCross; +import com.cappielloantonio.play.model.SongArtistCross; + +import java.util.List; + +@Dao +public interface PlaylistSongCrossDao { + @Query("SELECT * FROM playlist_song_cross") + LiveData> getAll(); + + @Query("SELECT EXISTS(SELECT * FROM playlist_song_cross WHERE id = :id)") + boolean exist(String id); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insert(PlaylistSongCross playlistSongCross); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insertAll(List playlistSongCrosses); + + @Delete + void delete(PlaylistSongCross playlistSongCross); + + @Update + void update(PlaylistSongCross playlistSongCross); + + @Query("DELETE FROM playlist_song_cross") + void deleteAll(); +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/play/database/dao/SongDao.java b/app/src/main/java/com/cappielloantonio/play/database/dao/SongDao.java index f5321e43..ab54b45f 100644 --- a/app/src/main/java/com/cappielloantonio/play/database/dao/SongDao.java +++ b/app/src/main/java/com/cappielloantonio/play/database/dao/SongDao.java @@ -51,6 +51,10 @@ public interface SongDao { @Query("SELECT * FROM song WHERE albumId = :albumID ORDER BY trackNumber ASC") LiveData> getLiveAlbumSong(String albumID); + // @Query("SELECT * FROM song WHERE albumId = :albumID ORDER BY trackNumber ASC") + @Query("SELECT song.* FROM song INNER JOIN playlist_song_cross ON song.id = playlist_song_cross.song_id AND playlist_song_cross.playlist_id = :playlistID") + LiveData> getLivePlaylistSong(String playlistID); + @Query("SELECT * FROM song WHERE albumId = :albumID ORDER BY trackNumber ASC") List getAlbumSong(String albumID); diff --git a/app/src/main/java/com/cappielloantonio/play/model/PlaylistSongCross.java b/app/src/main/java/com/cappielloantonio/play/model/PlaylistSongCross.java new file mode 100644 index 00000000..6eca3b19 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/model/PlaylistSongCross.java @@ -0,0 +1,49 @@ +package com.cappielloantonio.play.model; + +import androidx.annotation.NonNull; +import androidx.room.ColumnInfo; +import androidx.room.Entity; +import androidx.room.PrimaryKey; + +@Entity(tableName = "playlist_song_cross") +public class PlaylistSongCross { + @NonNull + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = "id") + private int id; + + @ColumnInfo(name = "playlist_id") + private String playlistId; + + @ColumnInfo(name = "song_id") + private String songId; + + public PlaylistSongCross(String playlistId, String songId) { + this.playlistId = playlistId; + this.songId = songId; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getPlaylistId() { + return playlistId; + } + + public void setPlaylistId(String playlistId) { + this.playlistId = playlistId; + } + + public String getSongId() { + return songId; + } + + public void setSongId(String songId) { + this.songId = songId; + } +} diff --git a/app/src/main/java/com/cappielloantonio/play/repository/PlaylistSongRepository.java b/app/src/main/java/com/cappielloantonio/play/repository/PlaylistSongRepository.java new file mode 100644 index 00000000..27fdf262 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/repository/PlaylistSongRepository.java @@ -0,0 +1,62 @@ +package com.cappielloantonio.play.repository; + +import android.app.Application; + +import com.cappielloantonio.play.database.AppDatabase; +import com.cappielloantonio.play.database.dao.PlaylistSongCrossDao; +import com.cappielloantonio.play.database.dao.SongArtistCrossDao; +import com.cappielloantonio.play.model.PlaylistSongCross; +import com.cappielloantonio.play.model.SongArtistCross; + +import java.util.List; + +public class PlaylistSongRepository { + private static final String TAG = "AlbumArtistRepository"; + + private PlaylistSongCrossDao playlistSongCrossDao; + + public PlaylistSongRepository(Application application) { + AppDatabase database = AppDatabase.getInstance(application); + playlistSongCrossDao = database.playlistSongCrossDao(); + } + + public void insertAll(List crosses) { + InsertAllThreadSafe insertAll = new InsertAllThreadSafe(playlistSongCrossDao, crosses); + Thread thread = new Thread(insertAll); + thread.start(); + } + + private static class InsertAllThreadSafe implements Runnable { + private PlaylistSongCrossDao playlistSongCrossDao; + private List crosses; + + public InsertAllThreadSafe(PlaylistSongCrossDao playlistSongCrossDao, List crosses) { + this.playlistSongCrossDao = playlistSongCrossDao; + this.crosses = crosses; + } + + @Override + public void run() { + playlistSongCrossDao.insertAll(crosses); + } + } + + public void deleteAll() { + DeleteAllPlaylistSongCrossThreadSafe delete = new DeleteAllPlaylistSongCrossThreadSafe(playlistSongCrossDao); + Thread thread = new Thread(delete); + thread.start(); + } + + private static class DeleteAllPlaylistSongCrossThreadSafe implements Runnable { + private PlaylistSongCrossDao playlistSongCrossDao; + + public DeleteAllPlaylistSongCrossThreadSafe(PlaylistSongCrossDao playlistSongCrossDao) { + this.playlistSongCrossDao = playlistSongCrossDao; + } + + @Override + public void run() { + playlistSongCrossDao.deleteAll(); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/play/repository/SongRepository.java b/app/src/main/java/com/cappielloantonio/play/repository/SongRepository.java index 30774506..c3c49d2c 100644 --- a/app/src/main/java/com/cappielloantonio/play/repository/SongRepository.java +++ b/app/src/main/java/com/cappielloantonio/play/repository/SongRepository.java @@ -27,6 +27,7 @@ public class SongRepository { private LiveData> listLiveSampleMostPlayedSongs; private LiveData> listLiveSampleArtistTopSongs; private LiveData> listLiveAlbumSongs; + private LiveData> listLivePlaylistSongs; private LiveData> listLiveSongByGenre; private LiveData> listLiveFilteredSongs; private LiveData> listLiveSongByYear; @@ -102,6 +103,11 @@ public class SongRepository { return listLiveAlbumSongs; } + public LiveData> getPlaylistLiveSong(String playlistID) { + listLivePlaylistSongs = songDao.getLivePlaylistSong(playlistID); + return listLivePlaylistSongs; + } + public List getAlbumListSong(String albumID, boolean randomOrder) { List songs = new ArrayList<>(); diff --git a/app/src/main/java/com/cappielloantonio/play/ui/fragment/PlaylistPageFragment.java b/app/src/main/java/com/cappielloantonio/play/ui/fragment/PlaylistPageFragment.java index 77dc7623..d8dad9f0 100644 --- a/app/src/main/java/com/cappielloantonio/play/ui/fragment/PlaylistPageFragment.java +++ b/app/src/main/java/com/cappielloantonio/play/ui/fragment/PlaylistPageFragment.java @@ -45,6 +45,7 @@ public class PlaylistPageFragment extends Fragment { playlistPageViewModel = new ViewModelProvider(requireActivity()).get(PlaylistPageViewModel.class); init(); + initBackCover(); initSongsView(); return view; @@ -82,7 +83,13 @@ public class PlaylistPageFragment extends Fragment { bind.animToolbar.getNavigationIcon().setColorFilter(getResources().getColor(R.color.white, null), PorterDuff.Mode.SRC_ATOP); } }); + } + private void initBackCover() { + CustomGlideRequest.Builder + .from(requireContext(), playlistPageViewModel.getPlaylist().getPrimary(), playlistPageViewModel.getPlaylist().getBlurHash(), CustomGlideRequest.PRIMARY, CustomGlideRequest.TOP_QUALITY, CustomGlideRequest.ALBUM_PIC) + .build() + .into(bind.albumBackCoverImageView); } private void initSongsView() { @@ -91,6 +98,8 @@ public class PlaylistPageFragment extends Fragment { songResultSearchAdapter = new SongResultSearchAdapter(activity, requireContext(), getChildFragmentManager()); bind.songRecyclerView.setAdapter(songResultSearchAdapter); - playlistPageViewModel.getPlaylistSongList().observe(requireActivity(), songs -> songResultSearchAdapter.setItems(songs)); + playlistPageViewModel.getPlaylistSongList().observe(requireActivity(), songs -> { + songResultSearchAdapter.setItems(songs); + }); } } \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/play/ui/fragment/SettingsFragment.java b/app/src/main/java/com/cappielloantonio/play/ui/fragment/SettingsFragment.java index 1bbfd8f9..02d53bb4 100644 --- a/app/src/main/java/com/cappielloantonio/play/ui/fragment/SettingsFragment.java +++ b/app/src/main/java/com/cappielloantonio/play/ui/fragment/SettingsFragment.java @@ -74,8 +74,8 @@ public class SettingsFragment extends PreferenceFragmentCompat { Preference cross_sync_button = findPreference(getString(R.string.genres_music_cross_sync)); cross_sync_button.setOnPreferenceClickListener(preference -> { AlertDialog.Builder builder = new AlertDialog.Builder(requireContext()); - builder.setMessage("Sync song's genres otherwise nothing will be shown in each genre category") - .setTitle("Song's genres not synchronized") + builder.setMessage("Force sync song's genres to display updated and correct songs in each genre category") + .setTitle("Force sync song's genres") .setNegativeButton(R.string.ignore, null) .setPositiveButton("Sync", (dialog, id) -> { Bundle bundle = SyncUtil.getSyncBundle(false, false, true, false, false, true); @@ -84,6 +84,20 @@ public class SettingsFragment extends PreferenceFragmentCompat { .show(); return true; }); + + Preference playlist_sync_button = findPreference(getString(R.string.playlist_song_cross_sync)); + playlist_sync_button.setOnPreferenceClickListener(preference -> { + AlertDialog.Builder builder = new AlertDialog.Builder(requireContext()); + builder.setMessage("Sync playlists' songs") + .setTitle("Force sync playlist") + .setNegativeButton(R.string.ignore, null) + .setPositiveButton("Sync", (dialog, id) -> { + Bundle bundle = SyncUtil.getSyncBundle(false, false, false, true, false, false); + activity.goFromSettingsToSync(bundle); + }) + .show(); + return true; + }); } } diff --git a/app/src/main/java/com/cappielloantonio/play/ui/fragment/SyncFragment.java b/app/src/main/java/com/cappielloantonio/play/ui/fragment/SyncFragment.java index e7ca2aff..572fc11e 100644 --- a/app/src/main/java/com/cappielloantonio/play/ui/fragment/SyncFragment.java +++ b/app/src/main/java/com/cappielloantonio/play/ui/fragment/SyncFragment.java @@ -20,6 +20,7 @@ import com.cappielloantonio.play.model.AlbumArtistCross; import com.cappielloantonio.play.model.Artist; import com.cappielloantonio.play.model.Genre; import com.cappielloantonio.play.model.Playlist; +import com.cappielloantonio.play.model.PlaylistSongCross; import com.cappielloantonio.play.model.Song; import com.cappielloantonio.play.model.SongArtistCross; import com.cappielloantonio.play.model.SongGenreCross; @@ -28,6 +29,7 @@ import com.cappielloantonio.play.repository.AlbumRepository; import com.cappielloantonio.play.repository.ArtistRepository; import com.cappielloantonio.play.repository.GenreRepository; import com.cappielloantonio.play.repository.PlaylistRepository; +import com.cappielloantonio.play.repository.PlaylistSongRepository; import com.cappielloantonio.play.repository.SongArtistRepository; import com.cappielloantonio.play.repository.SongRepository; import com.cappielloantonio.play.ui.activities.MainActivity; @@ -54,6 +56,7 @@ public class SyncFragment extends Fragment { private GenreRepository genreRepository; private SongArtistRepository songArtistRepository; private AlbumArtistRepository albumArtistRepository; + private PlaylistSongRepository playlistSongRepository; @Nullable @Override @@ -71,6 +74,7 @@ public class SyncFragment extends Fragment { genreRepository = new GenreRepository(activity.getApplication()); songArtistRepository = new SongArtistRepository(activity.getApplication()); albumArtistRepository = new AlbumArtistRepository(activity.getApplication()); + playlistSongRepository = new PlaylistSongRepository(activity.getApplication()); init(); syncLibraries(); @@ -181,6 +185,7 @@ public class SyncFragment extends Fragment { @Override public void onLoadMedia(List media) { playlistRepository.insertAll((ArrayList) media); + syncSongsPerPlaylist((ArrayList) media); animateProgressBar(true); } }); @@ -223,6 +228,22 @@ public class SyncFragment extends Fragment { PreferenceUtil.getInstance(requireContext()).setSongGenreSync(true); } + private void syncSongsPerPlaylist(List playlists) { + for (Playlist playlist : playlists) { + SyncUtil.getSongsPerPlaylist(requireContext(), new MediaCallback() { + @Override + public void onError(Exception exception) { + Log.e(TAG, "onError: " + exception.getMessage()); + } + + @Override + public void onLoadMedia(List media) { + playlistSongRepository.insertAll((ArrayList) media); + } + }, playlist.getId()); + } + } + private void animateProgressBar(boolean step) { syncViewModel.setProgress(step); diff --git a/app/src/main/java/com/cappielloantonio/play/util/SyncUtil.java b/app/src/main/java/com/cappielloantonio/play/util/SyncUtil.java index 6922020f..df1194bf 100644 --- a/app/src/main/java/com/cappielloantonio/play/util/SyncUtil.java +++ b/app/src/main/java/com/cappielloantonio/play/util/SyncUtil.java @@ -2,6 +2,7 @@ package com.cappielloantonio.play.util; import android.content.Context; import android.os.Bundle; +import android.util.Log; import com.cappielloantonio.play.App; import com.cappielloantonio.play.interfaces.MediaCallback; @@ -9,6 +10,7 @@ import com.cappielloantonio.play.model.Album; import com.cappielloantonio.play.model.Artist; import com.cappielloantonio.play.model.Genre; import com.cappielloantonio.play.model.Playlist; +import com.cappielloantonio.play.model.PlaylistSongCross; import com.cappielloantonio.play.model.Song; import com.cappielloantonio.play.model.SongGenreCross; @@ -19,6 +21,7 @@ import org.jellyfin.apiclient.model.querying.ItemFields; import org.jellyfin.apiclient.model.querying.ItemQuery; import org.jellyfin.apiclient.model.querying.ItemsByNameQuery; import org.jellyfin.apiclient.model.querying.ItemsResult; +import org.jellyfin.apiclient.model.playlists.PlaylistItemQuery; import java.util.ArrayList; import java.util.Arrays; @@ -207,6 +210,34 @@ public class SyncUtil { }); } + public static void getSongsPerPlaylist(Context context, MediaCallback callback, String playlistId) { + PlaylistItemQuery query = new PlaylistItemQuery(); + + query.setId(playlistId); + query.setUserId(App.getApiClientInstance(context).getCurrentUserId()); + query.setFields(new ItemFields[]{ItemFields.MediaSources}); + query.setUserId(App.getApiClientInstance(context).getCurrentUserId()); + + + App.getApiClientInstance(context).GetPlaylistItems(query, new Response() { + @Override + public void onResponse(ItemsResult result) { + ArrayList crosses = new ArrayList<>(); + + for (BaseItemDto itemDto : result.getItems()) { + crosses.add(new PlaylistSongCross(playlistId, itemDto.getId())); + } + + callback.onLoadMedia(crosses); + } + + @Override + public void onError(Exception exception) { + callback.onError(exception); + } + }); + } + public static Bundle getSyncBundle(Boolean syncAlbum, Boolean syncArtist, Boolean syncGenres, Boolean syncPlaylist, Boolean syncSong, Boolean crossSyncSongGenre) { Bundle bundle = new Bundle(); bundle.putBoolean("sync_album", syncAlbum); diff --git a/app/src/main/java/com/cappielloantonio/play/viewmodel/PlaylistPageViewModel.java b/app/src/main/java/com/cappielloantonio/play/viewmodel/PlaylistPageViewModel.java index 84a3da5e..23c5116e 100644 --- a/app/src/main/java/com/cappielloantonio/play/viewmodel/PlaylistPageViewModel.java +++ b/app/src/main/java/com/cappielloantonio/play/viewmodel/PlaylistPageViewModel.java @@ -1,6 +1,7 @@ package com.cappielloantonio.play.viewmodel; import android.app.Application; +import android.util.Log; import androidx.annotation.NonNull; import androidx.lifecycle.AndroidViewModel; @@ -14,6 +15,8 @@ import com.cappielloantonio.play.repository.SongRepository; import java.util.List; public class PlaylistPageViewModel extends AndroidViewModel { + private static final String TAG = "PlaylistPageViewModel"; + private SongRepository songRepository; private LiveData> songList; @@ -28,7 +31,8 @@ public class PlaylistPageViewModel extends AndroidViewModel { public LiveData> getPlaylistSongList() { // Prendere le canzoni di ciascuna playlist - songList = songRepository.getAlbumListLiveSong(playlist.getId()); + Log.i(TAG, "getPlaylistSongList: " + playlist.getId()); + songList = songRepository.getPlaylistLiveSong(playlist.getId()); return songList; } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b5c386a1..1397b5ba 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,6 +19,8 @@ Sync tracks, albums, artists, genres and playlists Genres-music cross sync Cross sync genres for each track + Playlist sync + Refresh playlists\' track list About Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 1.1 diff --git a/app/src/main/res/xml/global_preferences.xml b/app/src/main/res/xml/global_preferences.xml index 109adebe..7ed25a7f 100644 --- a/app/src/main/res/xml/global_preferences.xml +++ b/app/src/main/res/xml/global_preferences.xml @@ -40,6 +40,11 @@ android:key="@string/genres_music_cross_sync" android:summary="@string/genres_music_cross_sync_caption" /> + +