mirror of
https://github.com/antebudimir/tempus.git
synced 2026-01-01 09:53:33 +00:00
feat: Load media downloaded as file for offline use
This commit is contained in:
parent
cce6456951
commit
3ba19be4d9
7 changed files with 170 additions and 15 deletions
|
|
@ -24,6 +24,7 @@ import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.DiscTitle;
|
import com.cappielloantonio.tempo.subsonic.models.DiscTitle;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
@ -135,10 +136,18 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
||||||
|
|
||||||
holder.item.trackNumberTextView.setText(MusicUtil.getReadableTrackNumber(holder.itemView.getContext(), song.getTrack()));
|
holder.item.trackNumberTextView.setText(MusicUtil.getReadableTrackNumber(holder.itemView.getContext(), song.getTrack()));
|
||||||
|
|
||||||
if (DownloadUtil.getDownloadTracker(holder.itemView.getContext()).isDownloaded(song.getId())) {
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.VISIBLE);
|
if (DownloadUtil.getDownloadTracker(holder.itemView.getContext()).isDownloaded(song.getId())) {
|
||||||
|
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.GONE);
|
if (ExternalAudioReader.getUri(song) != null) {
|
||||||
|
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showCoverArt) CustomGlideRequest.Builder
|
if (showCoverArt) CustomGlideRequest.Builder
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ package com.cappielloantonio.tempo.ui.dialog;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.OptIn;
|
import androidx.annotation.OptIn;
|
||||||
|
|
@ -12,6 +15,8 @@ import androidx.media3.common.util.UnstableApi;
|
||||||
import com.cappielloantonio.tempo.R;
|
import com.cappielloantonio.tempo.R;
|
||||||
import com.cappielloantonio.tempo.databinding.DialogDeleteDownloadStorageBinding;
|
import com.cappielloantonio.tempo.databinding.DialogDeleteDownloadStorageBinding;
|
||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||||
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
@OptIn(markerClass = UnstableApi.class)
|
@OptIn(markerClass = UnstableApi.class)
|
||||||
|
|
@ -43,6 +48,16 @@ public class DeleteDownloadStorageDialog extends DialogFragment {
|
||||||
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
|
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
|
||||||
positiveButton.setOnClickListener(v -> {
|
positiveButton.setOnClickListener(v -> {
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).removeAll();
|
DownloadUtil.getDownloadTracker(requireContext()).removeAll();
|
||||||
|
String uriString = Preferences.getDownloadDirectoryUri();
|
||||||
|
if (uriString != null) {
|
||||||
|
DocumentFile directory = DocumentFile.fromTreeUri(requireContext(), Uri.parse(uriString));
|
||||||
|
if (directory != null && directory.canWrite()) {
|
||||||
|
for (DocumentFile file : directory.listFiles()) {
|
||||||
|
file.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ExternalAudioReader.refreshCache();
|
||||||
|
}
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ import com.cappielloantonio.tempo.ui.dialog.StreamingCacheStorageDialog;
|
||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.cappielloantonio.tempo.util.UIUtil;
|
import com.cappielloantonio.tempo.util.UIUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||||
import com.cappielloantonio.tempo.viewmodel.SettingViewModel;
|
import com.cappielloantonio.tempo.viewmodel.SettingViewModel;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
@ -90,6 +91,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
);
|
);
|
||||||
|
|
||||||
Preferences.setDownloadDirectoryUri(uri.toString());
|
Preferences.setDownloadDirectoryUri(uri.toString());
|
||||||
|
ExternalAudioReader.refreshCache();
|
||||||
Toast.makeText(requireContext(), "Download folder set.", Toast.LENGTH_SHORT).show();
|
Toast.makeText(requireContext(), "Download folder set.", Toast.LENGTH_SHORT).show();
|
||||||
checkDownloadDirectory();
|
checkDownloadDirectory();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||||
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
|
|
@ -117,10 +119,13 @@ public class DownloadedBottomSheetDialog extends BottomSheetDialogFragment imple
|
||||||
|
|
||||||
TextView removeAll = view.findViewById(R.id.remove_all_text_view);
|
TextView removeAll = view.findViewById(R.id.remove_all_text_view);
|
||||||
removeAll.setOnClickListener(v -> {
|
removeAll.setOnClickListener(v -> {
|
||||||
List<MediaItem> mediaItems = MappingUtil.mapDownloads(songs);
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
|
List<MediaItem> mediaItems = MappingUtil.mapDownloads(songs);
|
||||||
|
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).remove(mediaItems, downloads);
|
DownloadUtil.getDownloadTracker(requireContext()).remove(mediaItems, downloads);
|
||||||
|
} else {
|
||||||
|
songs.forEach(ExternalAudioReader::delete);
|
||||||
|
}
|
||||||
|
|
||||||
dismissBottomSheet();
|
dismissBottomSheet();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
|
import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
|
@ -178,10 +179,14 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||||
|
|
||||||
TextView remove = view.findViewById(R.id.remove_text_view);
|
TextView remove = view.findViewById(R.id.remove_text_view);
|
||||||
remove.setOnClickListener(v -> {
|
remove.setOnClickListener(v -> {
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).remove(
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
MappingUtil.mapDownload(song),
|
DownloadUtil.getDownloadTracker(requireContext()).remove(
|
||||||
new Download(song)
|
MappingUtil.mapDownload(song),
|
||||||
);
|
new Download(song)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ExternalAudioReader.delete(song);
|
||||||
|
}
|
||||||
dismissBottomSheet();
|
dismissBottomSheet();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -254,11 +259,20 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initDownloadUI(TextView download, TextView remove) {
|
private void initDownloadUI(TextView download, TextView remove) {
|
||||||
if (DownloadUtil.getDownloadTracker(requireContext()).isDownloaded(song.getId())) {
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
remove.setVisibility(View.VISIBLE);
|
if (DownloadUtil.getDownloadTracker(requireContext()).isDownloaded(song.getId())) {
|
||||||
|
remove.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
download.setVisibility(View.VISIBLE);
|
||||||
|
remove.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
download.setVisibility(View.VISIBLE);
|
if (ExternalAudioReader.getUri(song) != null) {
|
||||||
remove.setVisibility(View.GONE);
|
remove.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
download.setVisibility(View.VISIBLE);
|
||||||
|
remove.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
package com.cappielloantonio.tempo.util;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.App;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
|
||||||
|
|
||||||
|
import java.text.Normalizer;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class ExternalAudioReader {
|
||||||
|
|
||||||
|
private static final Map<String, DocumentFile> cache = new HashMap<>();
|
||||||
|
private static String cachedDirUri;
|
||||||
|
|
||||||
|
private static String sanitizeFileName(String name) {
|
||||||
|
String sanitized = name.replaceAll("[\\/:*?\\\"<>|]", "_");
|
||||||
|
sanitized = sanitized.replaceAll("\\s+", " ").trim();
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeForComparison(String name) {
|
||||||
|
String s = sanitizeFileName(name);
|
||||||
|
s = Normalizer.normalize(s, Normalizer.Form.NFKD);
|
||||||
|
s = s.replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
|
||||||
|
return s.toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static synchronized void ensureCache() {
|
||||||
|
String uriString = Preferences.getDownloadDirectoryUri();
|
||||||
|
if (uriString == null) {
|
||||||
|
cache.clear();
|
||||||
|
cachedDirUri = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uriString.equals(cachedDirUri)) return;
|
||||||
|
|
||||||
|
cache.clear();
|
||||||
|
DocumentFile directory = DocumentFile.fromTreeUri(App.getContext(), Uri.parse(uriString));
|
||||||
|
if (directory != null && directory.canRead()) {
|
||||||
|
for (DocumentFile file : directory.listFiles()) {
|
||||||
|
String existing = file.getName();
|
||||||
|
if (existing != null) {
|
||||||
|
String base = existing.replaceFirst("\\.[^\\.]+$", "");
|
||||||
|
cache.put(normalizeForComparison(base), file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedDirUri = uriString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Rebuilds the cache on next access. */
|
||||||
|
public static synchronized void refreshCache() {
|
||||||
|
cachedDirUri = null;
|
||||||
|
cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildKey(String artist, String title, String album) {
|
||||||
|
String name = artist != null && !artist.isEmpty() ? artist + " - " + title : title;
|
||||||
|
if (album != null && !album.isEmpty()) name += " (" + album + ")";
|
||||||
|
return normalizeForComparison(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Uri findUri(String artist, String title, String album) {
|
||||||
|
ensureCache();
|
||||||
|
if (cachedDirUri == null) return null;
|
||||||
|
|
||||||
|
DocumentFile file = cache.get(buildKey(artist, title, album));
|
||||||
|
return file != null && file.exists() ? file.getUri() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Uri getUri(Child media) {
|
||||||
|
return findUri(media.getArtist(), media.getTitle(), media.getAlbum());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Uri getUri(PodcastEpisode episode) {
|
||||||
|
return findUri(episode.getArtist(), episode.getTitle(), episode.getAlbum());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean delete(Child media) {
|
||||||
|
ensureCache();
|
||||||
|
if (cachedDirUri == null) return false;
|
||||||
|
|
||||||
|
String key = buildKey(media.getArtist(), media.getTitle(), media.getAlbum());
|
||||||
|
DocumentFile file = cache.get(key);
|
||||||
|
boolean deleted = false;
|
||||||
|
if (file != null && file.exists()) {
|
||||||
|
deleted = file.delete();
|
||||||
|
}
|
||||||
|
if (deleted) {
|
||||||
|
cache.remove(key);
|
||||||
|
}
|
||||||
|
return deleted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -217,12 +217,20 @@ public class MappingUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Uri getUri(Child media) {
|
private static Uri getUri(Child media) {
|
||||||
|
if (Preferences.getDownloadDirectoryUri() != null) {
|
||||||
|
Uri local = ExternalAudioReader.getUri(media);
|
||||||
|
return local != null ? local : MusicUtil.getStreamUri(media.getId());
|
||||||
|
}
|
||||||
return DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(media.getId())
|
return DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(media.getId())
|
||||||
? getDownloadUri(media.getId())
|
? getDownloadUri(media.getId())
|
||||||
: MusicUtil.getStreamUri(media.getId());
|
: MusicUtil.getStreamUri(media.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Uri getUri(PodcastEpisode podcastEpisode) {
|
private static Uri getUri(PodcastEpisode podcastEpisode) {
|
||||||
|
if (Preferences.getDownloadDirectoryUri() != null) {
|
||||||
|
Uri local = ExternalAudioReader.getUri(podcastEpisode);
|
||||||
|
return local != null ? local : MusicUtil.getStreamUri(podcastEpisode.getStreamId());
|
||||||
|
}
|
||||||
return DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(podcastEpisode.getStreamId())
|
return DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(podcastEpisode.getStreamId())
|
||||||
? getDownloadUri(podcastEpisode.getStreamId())
|
? getDownloadUri(podcastEpisode.getStreamId())
|
||||||
: MusicUtil.getStreamUri(podcastEpisode.getStreamId());
|
: MusicUtil.getStreamUri(podcastEpisode.getStreamId());
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue