mirror of
https://github.com/antebudimir/tempus.git
synced 2026-04-15 16:27:26 +00:00
Merge branch 'eddyizm:development' into development
This commit is contained in:
commit
f01ca9fed0
21 changed files with 952 additions and 390 deletions
18
USAGE.md
18
USAGE.md
|
|
@ -160,7 +160,23 @@ If your server supports it - add a internet radio station feed
|
||||||
## Android Auto
|
## Android Auto
|
||||||
|
|
||||||
### Enabling on your head unit
|
### Enabling on your head unit
|
||||||
- You have to enable Android Auto developer options, which are different from actual Android dev options. Then you have to enable "Unknown sources" in Android Auto, otherwise the app won't appear as it isn't downloaded from Play Store. (screenshots needed)
|
To allow the Tempus app on your car's head unit, "Unknown sources" needs to be enabled in the Android Auto "Developer settings". This is because Tempus isn't installed through Play Store. Note that the Android Auto developer settings are different from the global Android "Developer options".
|
||||||
|
1. Switch to developer mode in the Android Auto settings by tapping ten times on the "Version" item at the bottom, followed by giving your permission.
|
||||||
|
<p align="left">
|
||||||
|
<img width="270" height="600" alt="1a" src="https://github.com/user-attachments/assets/f09f6999-9761-4b05-8ec7-bf221a15dda3" />
|
||||||
|
<img width="270" height="600" alt="1b" src="https://github.com/user-attachments/assets/0795e508-ba01-41c5-96a7-7c03b0156591" />
|
||||||
|
<img width="270" height="600" alt="1c" src="https://github.com/user-attachments/assets/51c15f67-fddb-452e-b5d3-5092edeab390" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
2. Go to the "Developer settings" by the menu at the top right.
|
||||||
|
<p align="left">
|
||||||
|
<img width="270" height="600" alt="2" src="https://github.com/user-attachments/assets/1ecd1f3e-026d-4d25-87f2-be7f12efbac6" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
3. Scroll down to the bottom and check "Unknown sources".
|
||||||
|
<p align="left">
|
||||||
|
<img width="270" height="600" alt="3" src="https://github.com/user-attachments/assets/37db88e9-1b76-417f-9c47-da9f3a750fff" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
### Server Settings
|
### Server Settings
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ android {
|
||||||
targetSdk 35
|
targetSdk 35
|
||||||
|
|
||||||
versionCode 11
|
versionCode 11
|
||||||
versionName '4.6.0'
|
versionName '4.6.2'
|
||||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||||
|
|
||||||
javaCompileOptions {
|
javaCompileOptions {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.repository;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.App;
|
import com.cappielloantonio.tempo.App;
|
||||||
|
|
@ -11,6 +12,7 @@ import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.AlbumInfo;
|
import com.cappielloantonio.tempo.subsonic.models.AlbumInfo;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
|
import com.cappielloantonio.tempo.util.Constants.SeedType;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
|
|
@ -204,38 +206,15 @@ public class AlbumRepository {
|
||||||
return albumInfo;
|
return albumInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void getInstantMix(AlbumID3 album, int count, MediaCallback callback) {
|
public void getInstantMix(AlbumID3 album, int count, final MediaCallback callback) {
|
||||||
Log.d("AlbumRepository", "Attempting getInstantMix for AlbumID: " + album.getId());
|
Log.d("AlbumRepository", "Starting Instant Mix for album: " + album.getName());
|
||||||
|
|
||||||
App.getSubsonicClientInstance(false)
|
new SongRepository().getInstantMix(album.getId(), SeedType.ALBUM, count, songs -> {
|
||||||
.getBrowsingClient()
|
|
||||||
.getSimilarSongs2(album.getId(), count)
|
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
|
||||||
@Override
|
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
|
||||||
List<Child> songs = new ArrayList<>();
|
|
||||||
|
|
||||||
if (response.isSuccessful()
|
if (songs != null && !songs.isEmpty()) {
|
||||||
&& response.body() != null
|
callback.onLoadMedia(songs);
|
||||||
&& response.body().getSubsonicResponse().getSimilarSongs2() != null) {
|
}
|
||||||
|
});
|
||||||
List<Child> similarSongs = response.body().getSubsonicResponse().getSimilarSongs2().getSongs();
|
|
||||||
|
|
||||||
if (similarSongs == null) {
|
|
||||||
Log.w("AlbumRepository", "API successful but 'songs' list was NULL for AlbumID: " + album.getId());
|
|
||||||
} else {
|
|
||||||
songs.addAll(similarSongs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
callback.onLoadMedia(songs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
|
||||||
callback.onLoadMedia(new ArrayList<>());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public MutableLiveData<List<Integer>> getDecades() {
|
public MutableLiveData<List<Integer>> getDecades() {
|
||||||
|
|
@ -248,7 +227,7 @@ public class AlbumRepository {
|
||||||
@Override
|
@Override
|
||||||
public void onLoadYear(int last) {
|
public void onLoadYear(int last) {
|
||||||
if (first != -1 && last != -1) {
|
if (first != -1 && last != -1) {
|
||||||
List<Integer> decadeList = new ArrayList();
|
List<Integer> decadeList = new ArrayList<>();
|
||||||
|
|
||||||
int startDecade = first - (first % 10);
|
int startDecade = first - (first % 10);
|
||||||
int lastDecade = last - (last % 10);
|
int lastDecade = last - (last % 10);
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,14 @@ import androidx.lifecycle.MutableLiveData;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.App;
|
import com.cappielloantonio.tempo.App;
|
||||||
|
import com.cappielloantonio.tempo.interfaces.MediaCallback;
|
||||||
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.ArtistInfo2;
|
import com.cappielloantonio.tempo.subsonic.models.ArtistInfo2;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.IndexID3;
|
import com.cappielloantonio.tempo.subsonic.models.IndexID3;
|
||||||
|
import com.cappielloantonio.tempo.util.Constants.SeedType;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
@ -149,7 +151,7 @@ public class ArtistRepository {
|
||||||
|
|
||||||
if(response.body().getSubsonicResponse().getArtists() != null && response.body().getSubsonicResponse().getArtists().getIndices() != null) {
|
if(response.body().getSubsonicResponse().getArtists() != null && response.body().getSubsonicResponse().getArtists().getIndices() != null) {
|
||||||
for (IndexID3 index : response.body().getSubsonicResponse().getArtists().getIndices()) {
|
for (IndexID3 index : response.body().getSubsonicResponse().getArtists().getIndices()) {
|
||||||
if(index != null && index.getArtists() != null) {
|
if(index.getArtists() != null) {
|
||||||
artists.addAll(index.getArtists());
|
artists.addAll(index.getArtists());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -287,28 +289,22 @@ public class ArtistRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
public MutableLiveData<List<Child>> getInstantMix(ArtistID3 artist, int count) {
|
public MutableLiveData<List<Child>> getInstantMix(ArtistID3 artist, int count) {
|
||||||
MutableLiveData<List<Child>> instantMix = new MutableLiveData<>();
|
// Delegate to the centralized SongRepository
|
||||||
|
return new SongRepository().getInstantMix(artist.getId(), SeedType.ARTIST, count);
|
||||||
App.getSubsonicClientInstance(false)
|
|
||||||
.getBrowsingClient()
|
|
||||||
.getSimilarSongs2(artist.getId(), count)
|
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
|
||||||
@Override
|
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
|
||||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSimilarSongs2() != null) {
|
|
||||||
instantMix.setValue(response.body().getSubsonicResponse().getSimilarSongs2().getSongs());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return instantMix;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void getInstantMix(ArtistID3 artist, int count, final MediaCallback callback) {
|
||||||
|
// Delegate to the centralized SongRepository
|
||||||
|
new SongRepository().getInstantMix(artist.getId(), SeedType.ARTIST, count, songs -> {
|
||||||
|
if (songs != null && !songs.isEmpty()) {
|
||||||
|
callback.onLoadMedia(songs);
|
||||||
|
} else {
|
||||||
|
callback.onLoadMedia(Collections.emptyList());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public MutableLiveData<List<Child>> getRandomSong(ArtistID3 artist, int count) {
|
public MutableLiveData<List<Child>> getRandomSong(ArtistID3 artist, int count) {
|
||||||
MutableLiveData<List<Child>> randomSongs = new MutableLiveData<>();
|
MutableLiveData<List<Child>> randomSongs = new MutableLiveData<>();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,33 @@
|
||||||
package com.cappielloantonio.tempo.repository;
|
package com.cappielloantonio.tempo.repository;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.App;
|
import com.cappielloantonio.tempo.App;
|
||||||
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.SubsonicResponse;
|
||||||
|
import com.cappielloantonio.tempo.util.Constants.SeedType;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
import retrofit2.Response;
|
import retrofit2.Response;
|
||||||
|
|
||||||
public class SongRepository {
|
public class SongRepository {
|
||||||
|
|
||||||
private static final String TAG = "SongRepository";
|
private static final String TAG = "SongRepository";
|
||||||
|
public interface MediaCallbackInternal {
|
||||||
|
void onSongsAvailable(List<Child> songs);
|
||||||
|
}
|
||||||
|
|
||||||
public MutableLiveData<List<Child>> getStarredSongs(boolean random, int size) {
|
public MutableLiveData<List<Child>> getStarredSongs(boolean random, int size) {
|
||||||
MutableLiveData<List<Child>> starredSongs = new MutableLiveData<>(Collections.emptyList());
|
MutableLiveData<List<Child>> starredSongs = new MutableLiveData<>(Collections.emptyList());
|
||||||
|
|
@ -42,219 +53,332 @@ public class SongRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
|
||||||
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return starredSongs;
|
return starredSongs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MutableLiveData<List<Child>> getInstantMix(String id, int count) {
|
/**
|
||||||
MutableLiveData<List<Child>> instantMix = new MutableLiveData<>();
|
* Used by ViewModels. Updates the LiveData list incrementally as songs are found.
|
||||||
|
*/
|
||||||
|
public MutableLiveData<List<Child>> getInstantMix(String id, SeedType type, int count) {
|
||||||
|
MutableLiveData<List<Child>> instantMix = new MutableLiveData<>(new ArrayList<>());
|
||||||
|
|
||||||
App.getSubsonicClientInstance(false)
|
performSmartMix(id, type, count, songs -> {
|
||||||
.getBrowsingClient()
|
List<Child> current = instantMix.getValue();
|
||||||
.getSimilarSongs2(id, count)
|
if (current != null) {
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
for (Child s : songs) {
|
||||||
@Override
|
if (!current.contains(s)) current.add(s);
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
}
|
||||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSimilarSongs2() != null) {
|
|
||||||
instantMix.setValue(response.body().getSubsonicResponse().getSimilarSongs2().getSongs());
|
if (current.size() < count / 2) {
|
||||||
|
fillWithRandom(count - current.size(), remainder -> {
|
||||||
|
for (Child r : remainder) {
|
||||||
|
if (!current.contains(r)) current.add(r);
|
||||||
}
|
}
|
||||||
}
|
instantMix.postValue(current);
|
||||||
|
});
|
||||||
@Override
|
} else {
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
instantMix.postValue(current);
|
||||||
instantMix.setValue(null);
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return instantMix;
|
return instantMix;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MutableLiveData<List<Child>> getRandomSample(int number, Integer fromYear, Integer toYear) {
|
/**
|
||||||
MutableLiveData<List<Child>> randomSongsSample = new MutableLiveData<>();
|
* Overloaded method used by other Repositories
|
||||||
|
*/
|
||||||
|
public void getInstantMix(String id, SeedType type, int count, MediaCallbackInternal callback) {
|
||||||
|
new MediaCallbackAccumulator(callback, count).start(id, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MediaCallbackAccumulator {
|
||||||
|
private final MediaCallbackInternal originalCallback;
|
||||||
|
private final int targetCount;
|
||||||
|
private final List<Child> accumulatedSongs = new ArrayList<>();
|
||||||
|
private final Set<String> trackIds = new HashSet<>();
|
||||||
|
private boolean isComplete = false;
|
||||||
|
|
||||||
|
MediaCallbackAccumulator(MediaCallbackInternal callback, int count) {
|
||||||
|
this.originalCallback = callback;
|
||||||
|
this.targetCount = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
void start(String id, SeedType type) {
|
||||||
|
performSmartMix(id, type, targetCount, this::onBatchReceived);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onBatchReceived(List<Child> batch) {
|
||||||
|
if (isComplete || batch == null || batch.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int added = 0;
|
||||||
|
for (Child song : batch) {
|
||||||
|
if (!trackIds.contains(song.getId()) && accumulatedSongs.size() < targetCount) {
|
||||||
|
trackIds.add(song.getId());
|
||||||
|
accumulatedSongs.add(song);
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accumulatedSongs.size() >= targetCount) {
|
||||||
|
originalCallback.onSongsAvailable(new ArrayList<>(accumulatedSongs));
|
||||||
|
isComplete = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void performSmartMix(final String id, final SeedType type, final int count, final MediaCallbackInternal callback) {
|
||||||
|
switch (type) {
|
||||||
|
case ARTIST:
|
||||||
|
fetchSimilarByArtist(id, count, callback);
|
||||||
|
break;
|
||||||
|
case ALBUM:
|
||||||
|
fetchAlbumSongsThenSimilar(id, count, callback);
|
||||||
|
break;
|
||||||
|
case TRACK:
|
||||||
|
fetchSingleTrackThenSimilar(id, count, callback);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fetchAlbumSongsThenSimilar(String albumId, int count, MediaCallbackInternal callback) {
|
||||||
|
App.getSubsonicClientInstance(false).getBrowsingClient().getAlbum(albumId).enqueue(new Callback<ApiResponse>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
|
if (response.isSuccessful() && response.body() != null &&
|
||||||
|
response.body().getSubsonicResponse().getAlbum() != null) {
|
||||||
|
List<Child> albumSongs = response.body().getSubsonicResponse().getAlbum().getSongs();
|
||||||
|
if (albumSongs != null && !albumSongs.isEmpty()) {
|
||||||
|
int fromAlbum = Math.min(count, albumSongs.size());
|
||||||
|
List<Child> limitedAlbumSongs = albumSongs.subList(0, fromAlbum);
|
||||||
|
callback.onSongsAvailable(new ArrayList<>(limitedAlbumSongs));
|
||||||
|
|
||||||
|
int remaining = count - fromAlbum;
|
||||||
|
if (remaining > 0 && albumSongs.get(0).getArtistId() != null) {
|
||||||
|
fetchSimilarByArtist(albumSongs.get(0).getArtistId(), remaining, callback);
|
||||||
|
} else if (remaining > 0) {
|
||||||
|
Log.d(TAG, "No artistId available, skipping similar artist fetch");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Album fetch failed or empty, calling fillWithRandom");
|
||||||
|
fillWithRandom(count, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
|
Log.d(TAG, "Album fetch failed: " + t.getMessage());
|
||||||
|
fillWithRandom(count, callback);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fetchSimilarByArtist(String artistId, final int count, final MediaCallbackInternal callback) {
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getAlbumSongListClient()
|
.getBrowsingClient()
|
||||||
.getRandomSongs(number, fromYear, toYear)
|
.getSimilarSongs2(artistId, count)
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
List<Child> songs = new ArrayList<>();
|
List<Child> similar = extractSongs(response, "similarSongs2");
|
||||||
|
if (!similar.isEmpty()) {
|
||||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null && response.body().getSubsonicResponse().getRandomSongs().getSongs() != null) {
|
List<Child> limitedSimilar = similar.subList(0, Math.min(count, similar.size()));
|
||||||
songs.addAll(response.body().getSubsonicResponse().getRandomSongs().getSongs());
|
callback.onSongsAvailable(limitedSimilar);
|
||||||
|
} else {
|
||||||
|
fillWithRandom(count, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
randomSongsSample.setValue(songs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
|
fillWithRandom(count, callback);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fetchSingleTrackThenSimilar(String trackId, int count, MediaCallbackInternal callback) {
|
||||||
|
App.getSubsonicClientInstance(false).getBrowsingClient().getSong(trackId).enqueue(new Callback<ApiResponse>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
|
Child song = response.body().getSubsonicResponse().getSong();
|
||||||
|
if (song != null) {
|
||||||
|
callback.onSongsAvailable(Collections.singletonList(song));
|
||||||
|
int remaining = count - 1;
|
||||||
|
if (remaining > 0) {
|
||||||
|
fetchSimilarOnly(trackId, remaining, callback);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fillWithRandom(count, callback);
|
||||||
|
}
|
||||||
|
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
|
fillWithRandom(count, callback);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fetchSimilarOnly(String id, int count, MediaCallbackInternal callback) {
|
||||||
|
App.getSubsonicClientInstance(false).getBrowsingClient().getSimilarSongs(id, count).enqueue(new Callback<ApiResponse>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
|
List<Child> songs = extractSongs(response, "similarSongs");
|
||||||
|
if (!songs.isEmpty()) {
|
||||||
|
List<Child> limitedSongs = songs.subList(0, Math.min(count, songs.size()));
|
||||||
|
callback.onSongsAvailable(limitedSongs);
|
||||||
|
} else {
|
||||||
|
fillWithRandom(count, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
|
fillWithRandom(count, callback);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void fillWithRandom(int target, final MediaCallbackInternal callback) {
|
||||||
|
App.getSubsonicClientInstance(false)
|
||||||
|
.getAlbumSongListClient()
|
||||||
|
.getRandomSongs(target, null, null)
|
||||||
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
|
List<Child> random = extractSongs(response, "randomSongs");
|
||||||
|
if (!random.isEmpty()) {
|
||||||
|
List<Child> limitedRandom = random.subList(0, Math.min(target, random.size()));
|
||||||
|
callback.onSongsAvailable(limitedRandom);
|
||||||
|
} else {
|
||||||
|
callback.onSongsAvailable(new ArrayList<>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
|
callback.onSongsAvailable(new ArrayList<>());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Child> extractSongs(Response<ApiResponse> response, String type) {
|
||||||
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
|
SubsonicResponse res = response.body().getSubsonicResponse();
|
||||||
|
List<Child> list = null;
|
||||||
|
if (type.equals("similarSongs") && res.getSimilarSongs() != null) {
|
||||||
|
list = res.getSimilarSongs().getSongs();
|
||||||
|
} else if (type.equals("similarSongs2") && res.getSimilarSongs2() != null) {
|
||||||
|
list = res.getSimilarSongs2().getSongs();
|
||||||
|
} else if (type.equals("randomSongs") && res.getRandomSongs() != null) {
|
||||||
|
list = res.getRandomSongs().getSongs();
|
||||||
|
}
|
||||||
|
return (list != null) ? list : new ArrayList<>();
|
||||||
|
}
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public MutableLiveData<List<Child>> getRandomSample(int number, Integer fromYear, Integer toYear) {
|
||||||
|
MutableLiveData<List<Child>> randomSongsSample = new MutableLiveData<>();
|
||||||
|
App.getSubsonicClientInstance(false).getAlbumSongListClient().getRandomSongs(number, fromYear, toYear).enqueue(new Callback<ApiResponse>() {
|
||||||
|
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
|
List<Child> songs = new ArrayList<>();
|
||||||
|
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null) {
|
||||||
|
songs.addAll(Objects.requireNonNull(response.body().getSubsonicResponse().getRandomSongs().getSongs()));
|
||||||
|
}
|
||||||
|
randomSongsSample.setValue(songs);
|
||||||
|
}
|
||||||
|
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
|
||||||
|
});
|
||||||
return randomSongsSample;
|
return randomSongsSample;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MutableLiveData<List<Child>> getRandomSampleWithGenre(int number, Integer fromYear, Integer toYear, String genre) {
|
public MutableLiveData<List<Child>> getRandomSampleWithGenre(int number, Integer fromYear, Integer toYear, String genre) {
|
||||||
MutableLiveData<List<Child>> randomSongsSample = new MutableLiveData<>();
|
MutableLiveData<List<Child>> randomSongsSample = new MutableLiveData<>();
|
||||||
|
App.getSubsonicClientInstance(false).getAlbumSongListClient().getRandomSongs(number, fromYear, toYear, genre).enqueue(new Callback<ApiResponse>() {
|
||||||
App.getSubsonicClientInstance(false)
|
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
.getAlbumSongListClient()
|
List<Child> songs = new ArrayList<>();
|
||||||
.getRandomSongs(number, fromYear, toYear, genre)
|
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null) {
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
songs.addAll(Objects.requireNonNull(response.body().getSubsonicResponse().getRandomSongs().getSongs()));
|
||||||
@Override
|
}
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
randomSongsSample.setValue(songs);
|
||||||
List<Child> songs = new ArrayList<>();
|
}
|
||||||
|
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
|
||||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null && response.body().getSubsonicResponse().getRandomSongs().getSongs() != null) {
|
});
|
||||||
songs.addAll(response.body().getSubsonicResponse().getRandomSongs().getSongs());
|
|
||||||
}
|
|
||||||
|
|
||||||
randomSongsSample.setValue(songs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return randomSongsSample;
|
return randomSongsSample;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void scrobble(String id, boolean submission) {
|
public void scrobble(String id, boolean submission) {
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false).getMediaAnnotationClient().scrobble(id, submission).enqueue(new Callback<ApiResponse>() {
|
||||||
.getMediaAnnotationClient()
|
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {}
|
||||||
.scrobble(id, submission)
|
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
});
|
||||||
@Override
|
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRating(String id, int rating) {
|
public void setRating(String id, int rating) {
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false).getMediaAnnotationClient().setRating(id, rating).enqueue(new Callback<ApiResponse>() {
|
||||||
.getMediaAnnotationClient()
|
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {}
|
||||||
.setRating(id, rating)
|
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
});
|
||||||
@Override
|
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public MutableLiveData<List<Child>> getSongsByGenre(String id, int page) {
|
public MutableLiveData<List<Child>> getSongsByGenre(String id, int page) {
|
||||||
MutableLiveData<List<Child>> songsByGenre = new MutableLiveData<>();
|
MutableLiveData<List<Child>> songsByGenre = new MutableLiveData<>();
|
||||||
|
App.getSubsonicClientInstance(false).getAlbumSongListClient().getSongsByGenre(id, 100, 100 * page).enqueue(new Callback<ApiResponse>() {
|
||||||
App.getSubsonicClientInstance(false)
|
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
.getAlbumSongListClient()
|
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) {
|
||||||
.getSongsByGenre(id, 100, 100 * page)
|
songsByGenre.setValue(response.body().getSubsonicResponse().getSongsByGenre().getSongs());
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
}
|
||||||
@Override
|
}
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
|
||||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) {
|
});
|
||||||
songsByGenre.setValue(response.body().getSubsonicResponse().getSongsByGenre().getSongs());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return songsByGenre;
|
return songsByGenre;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MutableLiveData<List<Child>> getSongsByGenres(ArrayList<String> genresId) {
|
public MutableLiveData<List<Child>> getSongsByGenres(ArrayList<String> genresId) {
|
||||||
MutableLiveData<List<Child>> songsByGenre = new MutableLiveData<>();
|
MutableLiveData<List<Child>> songsByGenre = new MutableLiveData<>();
|
||||||
|
for (String id : genresId) {
|
||||||
for (String id : genresId)
|
App.getSubsonicClientInstance(false).getAlbumSongListClient().getSongsByGenre(id, 500, 0).enqueue(new Callback<ApiResponse>() {
|
||||||
App.getSubsonicClientInstance(false)
|
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
.getAlbumSongListClient()
|
List<Child> songs = new ArrayList<>();
|
||||||
.getSongsByGenre(id, 500, 0)
|
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) {
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
songs.addAll(Objects.requireNonNull(response.body().getSubsonicResponse().getSongsByGenre().getSongs()));
|
||||||
@Override
|
}
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
songsByGenre.setValue(songs);
|
||||||
List<Child> songs = new ArrayList<>();
|
}
|
||||||
|
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
|
||||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) {
|
});
|
||||||
songs.addAll(response.body().getSubsonicResponse().getSongsByGenre().getSongs());
|
}
|
||||||
}
|
|
||||||
|
|
||||||
songsByGenre.setValue(songs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return songsByGenre;
|
return songsByGenre;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MutableLiveData<Child> getSong(String id) {
|
public MutableLiveData<Child> getSong(String id) {
|
||||||
MutableLiveData<Child> song = new MutableLiveData<>();
|
MutableLiveData<Child> song = new MutableLiveData<>();
|
||||||
|
App.getSubsonicClientInstance(false).getBrowsingClient().getSong(id).enqueue(new Callback<ApiResponse>() {
|
||||||
App.getSubsonicClientInstance(false)
|
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
.getBrowsingClient()
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
.getSong(id)
|
song.setValue(response.body().getSubsonicResponse().getSong());
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
}
|
||||||
@Override
|
}
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
|
||||||
if (response.isSuccessful() && response.body() != null) {
|
});
|
||||||
song.setValue(response.body().getSubsonicResponse().getSong());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return song;
|
return song;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MutableLiveData<String> getSongLyrics(Child song) {
|
public MutableLiveData<String> getSongLyrics(Child song) {
|
||||||
MutableLiveData<String> lyrics = new MutableLiveData<>(null);
|
MutableLiveData<String> lyrics = new MutableLiveData<>(null);
|
||||||
|
App.getSubsonicClientInstance(false).getMediaRetrievalClient().getLyrics(song.getArtist(), song.getTitle()).enqueue(new Callback<ApiResponse>() {
|
||||||
App.getSubsonicClientInstance(false)
|
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
.getMediaRetrievalClient()
|
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getLyrics() != null) {
|
||||||
.getLyrics(song.getArtist(), song.getTitle())
|
lyrics.setValue(response.body().getSubsonicResponse().getLyrics().getValue());
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
}
|
||||||
@Override
|
}
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
|
||||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getLyrics() != null) {
|
});
|
||||||
lyrics.setValue(response.body().getSubsonicResponse().getLyrics().getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return lyrics;
|
return lyrics;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -26,6 +26,7 @@ import com.cappielloantonio.tempo.repository.SongRepository;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
|
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
|
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
|
||||||
|
import com.cappielloantonio.tempo.util.Constants.SeedType;
|
||||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||||
|
|
@ -183,11 +184,13 @@ public class MediaManager {
|
||||||
@OptIn(markerClass = UnstableApi.class)
|
@OptIn(markerClass = UnstableApi.class)
|
||||||
public static void startQueue(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, List<Child> media, int startIndex) {
|
public static void startQueue(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, List<Child> media, int startIndex) {
|
||||||
if (mediaBrowserListenableFuture != null) {
|
if (mediaBrowserListenableFuture != null) {
|
||||||
|
|
||||||
mediaBrowserListenableFuture.addListener(() -> {
|
mediaBrowserListenableFuture.addListener(() -> {
|
||||||
try {
|
try {
|
||||||
if (mediaBrowserListenableFuture.isDone()) {
|
if (mediaBrowserListenableFuture.isDone()) {
|
||||||
final MediaBrowser browser = mediaBrowserListenableFuture.get();
|
final MediaBrowser browser = mediaBrowserListenableFuture.get();
|
||||||
final List<MediaItem> items = MappingUtil.mapMediaItems(media);
|
final List<MediaItem> items = MappingUtil.mapMediaItems(media);
|
||||||
|
|
||||||
new Handler(Looper.getMainLooper()).post(() -> {
|
new Handler(Looper.getMainLooper()).post(() -> {
|
||||||
justStarted.set(true);
|
justStarted.set(true);
|
||||||
browser.setMediaItems(items, startIndex, 0);
|
browser.setMediaItems(items, startIndex, 0);
|
||||||
|
|
@ -196,28 +199,31 @@ public class MediaManager {
|
||||||
Player.Listener timelineListener = new Player.Listener() {
|
Player.Listener timelineListener = new Player.Listener() {
|
||||||
@Override
|
@Override
|
||||||
public void onTimelineChanged(Timeline timeline, int reason) {
|
public void onTimelineChanged(Timeline timeline, int reason) {
|
||||||
|
|
||||||
int itemCount = browser.getMediaItemCount();
|
int itemCount = browser.getMediaItemCount();
|
||||||
if (itemCount > 0 && startIndex >= 0 && startIndex < itemCount) {
|
if (itemCount > 0 && startIndex >= 0 && startIndex < itemCount) {
|
||||||
browser.seekTo(startIndex, 0);
|
browser.seekTo(startIndex, 0);
|
||||||
browser.play();
|
browser.play();
|
||||||
browser.removeListener(this);
|
browser.removeListener(this);
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Cannot start playback: itemCount=" + itemCount + ", startIndex=" + startIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
browser.addListener(timelineListener);
|
browser.addListener(timelineListener);
|
||||||
});
|
});
|
||||||
|
|
||||||
backgroundExecutor.execute(() -> {
|
backgroundExecutor.execute(() -> {
|
||||||
|
Log.d(TAG, "Background: enqueuing to database");
|
||||||
enqueueDatabase(media, true, 0);
|
enqueueDatabase(media, true, 0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (ExecutionException | InterruptedException e) {
|
} catch (ExecutionException | InterruptedException e) {
|
||||||
Log.e(TAG, "Error executing startQueue logic: " + e.getMessage(), e);
|
Log.e(TAG, "Error in startQueue: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}, MoreExecutors.directExecutor());
|
}, MoreExecutors.directExecutor());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void startQueue(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, Child media) {
|
public static void startQueue(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, Child media) {
|
||||||
|
|
@ -442,7 +448,7 @@ public class MediaManager {
|
||||||
if (mediaItem != null && Preferences.isContinuousPlayEnabled() && Preferences.isInstantMixUsable()) {
|
if (mediaItem != null && Preferences.isContinuousPlayEnabled() && Preferences.isInstantMixUsable()) {
|
||||||
Preferences.setLastInstantMix();
|
Preferences.setLastInstantMix();
|
||||||
|
|
||||||
LiveData<List<Child>> instantMix = getSongRepository().getInstantMix(mediaItem.mediaId, 10);
|
LiveData<List<Child>> instantMix = getSongRepository().getInstantMix(mediaItem.mediaId, SeedType.TRACK, 10);
|
||||||
instantMix.observeForever(new Observer<List<Child>>() {
|
instantMix.observeForever(new Observer<List<Child>>() {
|
||||||
@Override
|
@Override
|
||||||
public void onChanged(List<Child> media) {
|
public void onChanged(List<Child> media) {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
package com.cappielloantonio.tempo.subsonic.models
|
package com.cappielloantonio.tempo.subsonic.models
|
||||||
|
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
class SimilarSongs {
|
class SimilarSongs {
|
||||||
|
@SerializedName("song")
|
||||||
var songs: List<Child>? = null
|
var songs: List<Child>? = null
|
||||||
}
|
}
|
||||||
|
|
@ -9,12 +9,12 @@ import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.Toast;
|
|
||||||
import android.widget.ToggleButton;
|
import android.widget.ToggleButton;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.lifecycle.Observer;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.media3.session.MediaBrowser;
|
import androidx.media3.session.MediaBrowser;
|
||||||
|
|
@ -32,6 +32,7 @@ import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
||||||
import com.cappielloantonio.tempo.service.MediaManager;
|
import com.cappielloantonio.tempo.service.MediaManager;
|
||||||
import com.cappielloantonio.tempo.service.MediaService;
|
import com.cappielloantonio.tempo.service.MediaService;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.AlbumCatalogueAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.AlbumCatalogueAdapter;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter;
|
||||||
|
|
@ -203,22 +204,27 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initPlayButtons() {
|
private void initPlayButtons() {
|
||||||
bind.artistPageShuffleButton.setOnClickListener(v -> artistPageViewModel.getArtistShuffleList().observe(getViewLifecycleOwner(), songs -> {
|
bind.artistPageShuffleButton.setOnClickListener(v -> artistPageViewModel.getArtistShuffleList().observe(getViewLifecycleOwner(), new Observer<List<Child>>() {
|
||||||
if (!songs.isEmpty()) {
|
@Override
|
||||||
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
public void onChanged(List<Child> songs) {
|
||||||
activity.setBottomSheetInPeek(true);
|
if (songs != null && !songs.isEmpty()) {
|
||||||
} else {
|
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
||||||
Toast.makeText(requireContext(), getString(R.string.artist_error_retrieving_tracks), Toast.LENGTH_SHORT).show();
|
activity.setBottomSheetInPeek(true);
|
||||||
|
artistPageViewModel.getArtistShuffleList().removeObserver(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
bind.artistPageRadioButton.setOnClickListener(v -> artistPageViewModel.getArtistInstantMix().observe(getViewLifecycleOwner(), songs -> {
|
bind.artistPageRadioButton.setOnClickListener(v -> artistPageViewModel.getArtistInstantMix().observe(getViewLifecycleOwner(), new Observer<List<Child>>() {
|
||||||
if (songs != null && !songs.isEmpty()) {
|
@Override
|
||||||
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
public void onChanged(List<Child> songs) {
|
||||||
activity.setBottomSheetInPeek(true);
|
if (songs != null && !songs.isEmpty()) {
|
||||||
} else {
|
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
||||||
Toast.makeText(requireContext(), getString(R.string.artist_error_retrieving_radio), Toast.LENGTH_SHORT).show();
|
activity.setBottomSheetInPeek(true);
|
||||||
|
artistPageViewModel.getArtistInstantMix().removeObserver(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
@ -1253,20 +1255,25 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||||
MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
|
MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMediaClick(Bundle bundle) {
|
public void onMediaClick(Bundle bundle) {
|
||||||
if (bundle.containsKey(Constants.MEDIA_MIX)) {
|
if (bundle.containsKey(Constants.MEDIA_MIX)) {
|
||||||
MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelable(Constants.TRACK_OBJECT));
|
Child track = bundle.getParcelable(Constants.TRACK_OBJECT);
|
||||||
activity.setBottomSheetInPeek(true);
|
activity.setBottomSheetInPeek(true);
|
||||||
|
|
||||||
if (mediaBrowserListenableFuture != null) {
|
if (mediaBrowserListenableFuture != null) {
|
||||||
homeViewModel.getMediaInstantMix(getViewLifecycleOwner(), bundle.getParcelable(Constants.TRACK_OBJECT)).observe(getViewLifecycleOwner(), songs -> {
|
final boolean[] playbackStarted = {false};
|
||||||
MusicUtil.ratingFilter(songs);
|
|
||||||
|
|
||||||
if (songs != null && !songs.isEmpty()) {
|
homeViewModel.getMediaInstantMix(getViewLifecycleOwner(), track)
|
||||||
MediaManager.enqueue(mediaBrowserListenableFuture, songs, true);
|
.observe(getViewLifecycleOwner(), songs -> {
|
||||||
}
|
if (playbackStarted[0] || songs == null || songs.isEmpty()) return;
|
||||||
});
|
|
||||||
|
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||||
|
if (playbackStarted[0]) return;
|
||||||
|
|
||||||
|
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
||||||
|
playbackStarted[0] = true;
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else if (bundle.containsKey(Constants.MEDIA_CHRONOLOGY)) {
|
} else if (bundle.containsKey(Constants.MEDIA_CHRONOLOGY)) {
|
||||||
List<Child> media = bundle.getParcelableArrayList(Constants.TRACKS_OBJECT);
|
List<Child> media = bundle.getParcelableArrayList(Constants.TRACKS_OBJECT);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import android.content.ClipboardManager;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
@ -43,7 +44,6 @@ import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||||
import com.cappielloantonio.tempo.viewmodel.AlbumBottomSheetViewModel;
|
import com.cappielloantonio.tempo.viewmodel.AlbumBottomSheetViewModel;
|
||||||
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
@ -61,7 +61,11 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||||
private List<Child> currentAlbumTracks = Collections.emptyList();
|
private List<Child> currentAlbumTracks = Collections.emptyList();
|
||||||
private List<MediaItem> currentAlbumMediaItems = Collections.emptyList();
|
private List<MediaItem> currentAlbumMediaItems = Collections.emptyList();
|
||||||
|
|
||||||
|
private boolean playbackStarted = false;
|
||||||
|
private boolean dismissalScheduled = false;
|
||||||
|
|
||||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||||
|
private static final String TAG = "AlbumBottomSheetDialog";
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -114,33 +118,74 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||||
|
|
||||||
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
|
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
|
||||||
favoriteToggle.setChecked(albumBottomSheetViewModel.getAlbum().getStarred() != null);
|
favoriteToggle.setChecked(albumBottomSheetViewModel.getAlbum().getStarred() != null);
|
||||||
favoriteToggle.setOnClickListener(v -> {
|
favoriteToggle.setOnClickListener(v -> albumBottomSheetViewModel.setFavorite(requireContext()));
|
||||||
albumBottomSheetViewModel.setFavorite(requireContext());
|
|
||||||
});
|
|
||||||
|
|
||||||
TextView playRadio = view.findViewById(R.id.play_radio_text_view);
|
TextView playRadio = view.findViewById(R.id.play_radio_text_view);
|
||||||
playRadio.setOnClickListener(v -> {
|
playRadio.setOnClickListener(v -> {
|
||||||
AlbumRepository albumRepository = new AlbumRepository();
|
playbackStarted = false;
|
||||||
albumRepository.getInstantMix(album, 20, new MediaCallback() {
|
dismissalScheduled = false;
|
||||||
|
Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_SHORT).show();
|
||||||
|
final Runnable failsafeTimeout = () -> {
|
||||||
|
if (!playbackStarted && !dismissalScheduled) {
|
||||||
|
Log.w(TAG, "No response received within 3 seconds");
|
||||||
|
if (isAdded() && getActivity() != null) {
|
||||||
|
Toast.makeText(getContext(),
|
||||||
|
R.string.bottom_sheet_problem_generating_instant_mix,
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
dismissBottomSheet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
view.postDelayed(failsafeTimeout, 3000);
|
||||||
|
|
||||||
|
new AlbumRepository().getInstantMix(album, 20, new MediaCallback() {
|
||||||
@Override
|
@Override
|
||||||
public void onError(Exception exception) {
|
public void onError(Exception exception) {
|
||||||
exception.printStackTrace();
|
view.removeCallbacks(failsafeTimeout);
|
||||||
|
Log.e(TAG, "Error: " + exception.getMessage());
|
||||||
|
if (isAdded() && getActivity() != null) {
|
||||||
|
String message = isOffline(exception) ?
|
||||||
|
"You're offline" : "Network error";
|
||||||
|
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
if (!playbackStarted && !dismissalScheduled) {
|
||||||
|
scheduleDelayedDismissal(v);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadMedia(List<?> media) {
|
public void onLoadMedia(List<?> media) {
|
||||||
|
view.removeCallbacks(failsafeTimeout);
|
||||||
|
if (!isAdded() || getActivity() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
MusicUtil.ratingFilter((ArrayList<Child>) media);
|
MusicUtil.ratingFilter((ArrayList<Child>) media);
|
||||||
|
|
||||||
if (!media.isEmpty()) {
|
if (!media.isEmpty()) {
|
||||||
|
boolean isFirstBatch = !playbackStarted;
|
||||||
MediaManager.startQueue(mediaBrowserListenableFuture, (ArrayList<Child>) media, 0);
|
MediaManager.startQueue(mediaBrowserListenableFuture, (ArrayList<Child>) media, 0);
|
||||||
((MainActivity) requireActivity()).setBottomSheetInPeek(true);
|
playbackStarted = true;
|
||||||
}
|
|
||||||
|
|
||||||
dismissBottomSheet();
|
if (getActivity() instanceof MainActivity) {
|
||||||
|
((MainActivity) getActivity()).setBottomSheetInPeek(true);
|
||||||
|
}
|
||||||
|
if (isFirstBatch && !dismissalScheduled) {
|
||||||
|
scheduleDelayedDismissal(v);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(getContext(),
|
||||||
|
R.string.bottom_sheet_problem_generating_instant_mix,
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
if (!playbackStarted && !dismissalScheduled) {
|
||||||
|
scheduleDelayedDismissal(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
TextView playRandom = view.findViewById(R.id.play_random_text_view);
|
TextView playRandom = view.findViewById(R.id.play_random_text_view);
|
||||||
playRandom.setOnClickListener(v -> {
|
playRandom.setOnClickListener(v -> {
|
||||||
AlbumRepository albumRepository = new AlbumRepository();
|
AlbumRepository albumRepository = new AlbumRepository();
|
||||||
|
|
@ -186,18 +231,16 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||||
});
|
});
|
||||||
|
|
||||||
TextView addToPlaylist = view.findViewById(R.id.add_to_playlist_text_view);
|
TextView addToPlaylist = view.findViewById(R.id.add_to_playlist_text_view);
|
||||||
addToPlaylist.setOnClickListener(v -> {
|
addToPlaylist.setOnClickListener(v -> albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> {
|
||||||
albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> {
|
Bundle bundle = new Bundle();
|
||||||
Bundle bundle = new Bundle();
|
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(songs));
|
||||||
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(songs));
|
|
||||||
|
|
||||||
PlaylistChooserDialog dialog = new PlaylistChooserDialog();
|
PlaylistChooserDialog dialog = new PlaylistChooserDialog();
|
||||||
dialog.setArguments(bundle);
|
dialog.setArguments(bundle);
|
||||||
dialog.show(requireActivity().getSupportFragmentManager(), null);
|
dialog.show(requireActivity().getSupportFragmentManager(), null);
|
||||||
|
|
||||||
dismissBottomSheet();
|
dismissBottomSheet();
|
||||||
});
|
}));
|
||||||
});
|
|
||||||
|
|
||||||
removeAllTextView = view.findViewById(R.id.remove_all_text_view);
|
removeAllTextView = view.findViewById(R.id.remove_all_text_view);
|
||||||
albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> {
|
albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> {
|
||||||
|
|
@ -291,4 +334,31 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||||
private void refreshShares() {
|
private void refreshShares() {
|
||||||
homeViewModel.refreshShares(requireActivity());
|
homeViewModel.refreshShares(requireActivity());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void scheduleDelayedDismissal(View view) {
|
||||||
|
if (dismissalScheduled) return;
|
||||||
|
dismissalScheduled = true;
|
||||||
|
|
||||||
|
view.postDelayed(() -> {
|
||||||
|
try {
|
||||||
|
if (mediaBrowserListenableFuture.isDone()) {
|
||||||
|
MediaBrowser browser = mediaBrowserListenableFuture.get();
|
||||||
|
if (browser != null && browser.isPlaying()) {
|
||||||
|
dismissBottomSheet();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error checking playback: " + e.getMessage());
|
||||||
|
}
|
||||||
|
view.postDelayed(() -> dismissBottomSheet(), 200);
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isOffline(Exception exception) {
|
||||||
|
return exception != null && exception.getMessage() != null &&
|
||||||
|
(exception.getMessage().contains("Network") ||
|
||||||
|
exception.getMessage().contains("timeout") ||
|
||||||
|
exception.getMessage().contains("offline"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.fragment.bottomsheetdialog;
|
||||||
|
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
@ -18,10 +19,12 @@ import androidx.media3.session.SessionToken;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.R;
|
import com.cappielloantonio.tempo.R;
|
||||||
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
||||||
|
import com.cappielloantonio.tempo.interfaces.MediaCallback;
|
||||||
import com.cappielloantonio.tempo.repository.ArtistRepository;
|
import com.cappielloantonio.tempo.repository.ArtistRepository;
|
||||||
import com.cappielloantonio.tempo.service.MediaManager;
|
import com.cappielloantonio.tempo.service.MediaManager;
|
||||||
import com.cappielloantonio.tempo.service.MediaService;
|
import com.cappielloantonio.tempo.service.MediaService;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
|
|
@ -29,6 +32,9 @@ import com.cappielloantonio.tempo.viewmodel.ArtistBottomSheetViewModel;
|
||||||
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;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener {
|
public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener {
|
||||||
private static final String TAG = "AlbumBottomSheetDialog";
|
private static final String TAG = "AlbumBottomSheetDialog";
|
||||||
|
|
@ -38,6 +44,9 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
|
||||||
|
|
||||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||||
|
|
||||||
|
private boolean playbackStarted = false;
|
||||||
|
private boolean dismissalScheduled = false;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
|
|
@ -86,20 +95,69 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
|
||||||
|
|
||||||
TextView playRadio = view.findViewById(R.id.play_radio_text_view);
|
TextView playRadio = view.findViewById(R.id.play_radio_text_view);
|
||||||
playRadio.setOnClickListener(v -> {
|
playRadio.setOnClickListener(v -> {
|
||||||
ArtistRepository artistRepository = new ArtistRepository();
|
Log.d(TAG, "Artist instant mix clicked");
|
||||||
|
Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_SHORT).show();
|
||||||
|
playbackStarted = false;
|
||||||
|
dismissalScheduled = false;
|
||||||
|
final Runnable failsafeTimeout = () -> {
|
||||||
|
if (!playbackStarted && !dismissalScheduled) {
|
||||||
|
Log.w(TAG, "No response received within 3 seconds");
|
||||||
|
if (isAdded() && getActivity() != null) {
|
||||||
|
Toast.makeText(getContext(),
|
||||||
|
R.string.bottom_sheet_problem_generating_instant_mix,
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
dismissBottomSheet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
view.postDelayed(failsafeTimeout, 3000);
|
||||||
|
|
||||||
artistRepository.getInstantMix(artist, 20).observe(getViewLifecycleOwner(), songs -> {
|
new ArtistRepository().getInstantMix(artist, 20, new MediaCallback() {
|
||||||
// navidrome may return null for this
|
@Override
|
||||||
if (songs == null)
|
public void onError(Exception exception) {
|
||||||
return;
|
view.removeCallbacks(failsafeTimeout);
|
||||||
MusicUtil.ratingFilter(songs);
|
Log.e(TAG, "Error: " + exception.getMessage());
|
||||||
|
if (isAdded() && getActivity() != null) {
|
||||||
if (!songs.isEmpty()) {
|
String message = isOffline(exception) ?
|
||||||
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
"You're offline" : "Network error";
|
||||||
((MainActivity) requireActivity()).setBottomSheetInPeek(true);
|
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
if (!playbackStarted && !dismissalScheduled) {
|
||||||
|
scheduleDelayedDismissal(v);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dismissBottomSheet();
|
@Override
|
||||||
|
public void onLoadMedia(List<?> media) {
|
||||||
|
view.removeCallbacks(failsafeTimeout);
|
||||||
|
if (!isAdded() || getActivity() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Received " + media.size() + " songs for artist");
|
||||||
|
|
||||||
|
MusicUtil.ratingFilter((ArrayList<Child>) media);
|
||||||
|
|
||||||
|
if (!media.isEmpty()) {
|
||||||
|
boolean isFirstBatch = !playbackStarted;
|
||||||
|
MediaManager.startQueue(mediaBrowserListenableFuture, (ArrayList<Child>) media, 0);
|
||||||
|
playbackStarted = true;
|
||||||
|
|
||||||
|
if (getActivity() instanceof MainActivity) {
|
||||||
|
((MainActivity) getActivity()).setBottomSheetInPeek(true);
|
||||||
|
}
|
||||||
|
if (isFirstBatch && !dismissalScheduled) {
|
||||||
|
scheduleDelayedDismissal(v);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(getContext(),
|
||||||
|
R.string.bottom_sheet_problem_generating_instant_mix,
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
if (!playbackStarted && !dismissalScheduled) {
|
||||||
|
scheduleDelayedDismissal(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -108,16 +166,10 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
|
||||||
ArtistRepository artistRepository = new ArtistRepository();
|
ArtistRepository artistRepository = new ArtistRepository();
|
||||||
artistRepository.getRandomSong(artist, 50).observe(getViewLifecycleOwner(), songs -> {
|
artistRepository.getRandomSong(artist, 50).observe(getViewLifecycleOwner(), songs -> {
|
||||||
MusicUtil.ratingFilter(songs);
|
MusicUtil.ratingFilter(songs);
|
||||||
|
|
||||||
if (!songs.isEmpty()) {
|
if (!songs.isEmpty()) {
|
||||||
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
||||||
((MainActivity) requireActivity()).setBottomSheetInPeek(true);
|
((MainActivity) requireActivity()).setBottomSheetInPeek(true);
|
||||||
|
|
||||||
dismissBottomSheet();
|
|
||||||
} else {
|
|
||||||
Toast.makeText(requireContext(), getString(R.string.artist_error_retrieving_tracks), Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dismissBottomSheet();
|
dismissBottomSheet();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -139,4 +191,31 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
|
||||||
private void releaseMediaBrowser() {
|
private void releaseMediaBrowser() {
|
||||||
MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
|
MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void scheduleDelayedDismissal(View view) {
|
||||||
|
if (dismissalScheduled) return;
|
||||||
|
dismissalScheduled = true;
|
||||||
|
|
||||||
|
view.postDelayed(() -> {
|
||||||
|
try {
|
||||||
|
if (mediaBrowserListenableFuture.isDone()) {
|
||||||
|
MediaBrowser browser = mediaBrowserListenableFuture.get();
|
||||||
|
if (browser != null && browser.isPlaying()) {
|
||||||
|
dismissBottomSheet();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error checking playback: " + e.getMessage());
|
||||||
|
}
|
||||||
|
view.postDelayed(() -> dismissBottomSheet(), 200);
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isOffline(Exception exception) {
|
||||||
|
return exception != null && exception.getMessage() != null &&
|
||||||
|
(exception.getMessage().contains("Network") ||
|
||||||
|
exception.getMessage().contains("timeout") ||
|
||||||
|
exception.getMessage().contains("offline"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5,6 +5,9 @@ import android.content.ClipboardManager;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
@ -23,6 +26,7 @@ import androidx.navigation.fragment.NavHostFragment;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.R;
|
import com.cappielloantonio.tempo.R;
|
||||||
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
||||||
|
import com.cappielloantonio.tempo.interfaces.MediaCallback;
|
||||||
import com.cappielloantonio.tempo.model.Download;
|
import com.cappielloantonio.tempo.model.Download;
|
||||||
import com.cappielloantonio.tempo.service.MediaManager;
|
import com.cappielloantonio.tempo.service.MediaManager;
|
||||||
import com.cappielloantonio.tempo.service.MediaService;
|
import com.cappielloantonio.tempo.service.MediaService;
|
||||||
|
|
@ -50,6 +54,7 @@ import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
public class SongBottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener {
|
public class SongBottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener {
|
||||||
|
|
@ -67,7 +72,11 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||||
private AssetLinkUtil.AssetLink currentAlbumLink;
|
private AssetLinkUtil.AssetLink currentAlbumLink;
|
||||||
private AssetLinkUtil.AssetLink currentArtistLink;
|
private AssetLinkUtil.AssetLink currentArtistLink;
|
||||||
|
|
||||||
|
private boolean playbackStarted = false;
|
||||||
|
private boolean dismissalScheduled = false;
|
||||||
|
|
||||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||||
|
private static final String TAG = "SongBottomSheetDialog";
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -143,22 +152,68 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||||
|
|
||||||
TextView playRadio = view.findViewById(R.id.play_radio_text_view);
|
TextView playRadio = view.findViewById(R.id.play_radio_text_view);
|
||||||
playRadio.setOnClickListener(v -> {
|
playRadio.setOnClickListener(v -> {
|
||||||
MediaManager.startQueue(mediaBrowserListenableFuture, song);
|
playbackStarted = false;
|
||||||
((MainActivity) requireActivity()).setBottomSheetInPeek(true);
|
dismissalScheduled = false;
|
||||||
|
Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_SHORT).show();
|
||||||
|
|
||||||
songBottomSheetViewModel.getInstantMix(getViewLifecycleOwner(), song).observe(getViewLifecycleOwner(), songs -> {
|
final Runnable failsafeTimeout = () -> {
|
||||||
MusicUtil.ratingFilter(songs);
|
if (!playbackStarted && !dismissalScheduled) {
|
||||||
|
Log.w(TAG, "No response received within 3 seconds");
|
||||||
if (songs == null) {
|
if (isAdded() && getActivity() != null) {
|
||||||
dismissBottomSheet();
|
Toast.makeText(getContext(),
|
||||||
return;
|
R.string.bottom_sheet_problem_generating_instant_mix,
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
dismissBottomSheet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
view.postDelayed(failsafeTimeout, 3000);
|
||||||
|
songBottomSheetViewModel.getInstantMix(song, 20, new MediaCallback() {
|
||||||
|
@Override
|
||||||
|
public void onError(Exception exception) {
|
||||||
|
view.removeCallbacks(failsafeTimeout);
|
||||||
|
Log.e(TAG, "Error: " + exception.getMessage());
|
||||||
|
if (isAdded() && getActivity() != null) {
|
||||||
|
String message = isOffline(exception) ?
|
||||||
|
"You're offline" : "Network error";
|
||||||
|
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
if (!playbackStarted && !dismissalScheduled) {
|
||||||
|
scheduleDelayedDismissal(v);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!songs.isEmpty()) {
|
@Override
|
||||||
MediaManager.enqueue(mediaBrowserListenableFuture, songs, true);
|
public void onLoadMedia(List<?> media) {
|
||||||
dismissBottomSheet();
|
view.removeCallbacks(failsafeTimeout);
|
||||||
|
if (!isAdded() || getActivity() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MusicUtil.ratingFilter((ArrayList<Child>) media);
|
||||||
|
|
||||||
|
if (!media.isEmpty()) {
|
||||||
|
boolean isFirstBatch = !playbackStarted;
|
||||||
|
MediaManager.startQueue(mediaBrowserListenableFuture, (ArrayList<Child>) media, 0);
|
||||||
|
playbackStarted = true;
|
||||||
|
|
||||||
|
if (getActivity() instanceof MainActivity) {
|
||||||
|
((MainActivity) getActivity()).setBottomSheetInPeek(true);
|
||||||
|
}
|
||||||
|
if (isFirstBatch && !dismissalScheduled) {
|
||||||
|
scheduleDelayedDismissal(v);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(getContext(),
|
||||||
|
R.string.bottom_sheet_problem_generating_instant_mix,
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
if (!playbackStarted && !dismissalScheduled) {
|
||||||
|
scheduleDelayedDismissal(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
TextView playNext = view.findViewById(R.id.play_next_text_view);
|
TextView playNext = view.findViewById(R.id.play_next_text_view);
|
||||||
|
|
@ -397,4 +452,31 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||||
private void refreshShares() {
|
private void refreshShares() {
|
||||||
homeViewModel.refreshShares(requireActivity());
|
homeViewModel.refreshShares(requireActivity());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void scheduleDelayedDismissal(View view) {
|
||||||
|
if (dismissalScheduled) return;
|
||||||
|
dismissalScheduled = true;
|
||||||
|
|
||||||
|
view.postDelayed(() -> {
|
||||||
|
try {
|
||||||
|
if (mediaBrowserListenableFuture.isDone()) {
|
||||||
|
MediaBrowser browser = mediaBrowserListenableFuture.get();
|
||||||
|
if (browser != null && browser.isPlaying()) {
|
||||||
|
dismissBottomSheet();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error checking playback: " + e.getMessage());
|
||||||
|
}
|
||||||
|
view.postDelayed(() -> dismissBottomSheet(), 200);
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isOffline(Exception exception) {
|
||||||
|
return exception != null && exception.getMessage() != null &&
|
||||||
|
(exception.getMessage().contains("Network") ||
|
||||||
|
exception.getMessage().contains("timeout") ||
|
||||||
|
exception.getMessage().contains("offline"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -133,4 +133,7 @@ object Constants {
|
||||||
const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF = "android.media3.session.demo.REPEAT_OFF"
|
const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF = "android.media3.session.demo.REPEAT_OFF"
|
||||||
const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE = "android.media3.session.demo.REPEAT_ONE"
|
const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE = "android.media3.session.demo.REPEAT_ONE"
|
||||||
const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL = "android.media3.session.demo.REPEAT_ALL"
|
const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL = "android.media3.session.demo.REPEAT_ALL"
|
||||||
|
enum class SeedType {
|
||||||
|
ARTIST, ALBUM, TRACK
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,10 +4,12 @@ import android.app.Application;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.OptIn;
|
||||||
import androidx.lifecycle.AndroidViewModel;
|
import androidx.lifecycle.AndroidViewModel;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
import androidx.lifecycle.Observer;
|
import androidx.lifecycle.Observer;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.model.Download;
|
import com.cappielloantonio.tempo.model.Download;
|
||||||
import com.cappielloantonio.tempo.interfaces.StarCallback;
|
import com.cappielloantonio.tempo.interfaces.StarCallback;
|
||||||
|
|
@ -33,7 +35,6 @@ public class AlbumBottomSheetViewModel extends AndroidViewModel {
|
||||||
private final ArtistRepository artistRepository;
|
private final ArtistRepository artistRepository;
|
||||||
private final FavoriteRepository favoriteRepository;
|
private final FavoriteRepository favoriteRepository;
|
||||||
private final SharingRepository sharingRepository;
|
private final SharingRepository sharingRepository;
|
||||||
|
|
||||||
private AlbumID3 album;
|
private AlbumID3 album;
|
||||||
|
|
||||||
public AlbumBottomSheetViewModel(@NonNull Application application) {
|
public AlbumBottomSheetViewModel(@NonNull Application application) {
|
||||||
|
|
@ -116,6 +117,7 @@ public class AlbumBottomSheetViewModel extends AndroidViewModel {
|
||||||
MutableLiveData<List<Child>> tracksLiveData = albumRepository.getAlbumTracks(album.getId());
|
MutableLiveData<List<Child>> tracksLiveData = albumRepository.getAlbumTracks(album.getId());
|
||||||
|
|
||||||
tracksLiveData.observeForever(new Observer<List<Child>>() {
|
tracksLiveData.observeForever(new Observer<List<Child>>() {
|
||||||
|
@OptIn(markerClass = UnstableApi.class)
|
||||||
@Override
|
@Override
|
||||||
public void onChanged(List<Child> songs) {
|
public void onChanged(List<Child> songs) {
|
||||||
if (songs != null && !songs.isEmpty()) {
|
if (songs != null && !songs.isEmpty()) {
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,6 @@ public class ArtistPageViewModel extends AndroidViewModel {
|
||||||
MappingUtil.mapDownloads(songs),
|
MappingUtil.mapDownloads(songs),
|
||||||
songs.stream().map(Download::new).collect(Collectors.toList())
|
songs.stream().map(Download::new).collect(Collectors.toList())
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Playlist;
|
import com.cappielloantonio.tempo.subsonic.models.Playlist;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Share;
|
import com.cappielloantonio.tempo.subsonic.models.Share;
|
||||||
|
import com.cappielloantonio.tempo.util.Constants.SeedType;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.google.common.reflect.TypeToken;
|
import com.google.common.reflect.TypeToken;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
|
|
@ -34,7 +35,6 @@ import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class HomeViewModel extends AndroidViewModel {
|
public class HomeViewModel extends AndroidViewModel {
|
||||||
private static final String TAG = "HomeViewModel";
|
private static final String TAG = "HomeViewModel";
|
||||||
|
|
@ -223,7 +223,7 @@ public class HomeViewModel extends AndroidViewModel {
|
||||||
public LiveData<List<Child>> getMediaInstantMix(LifecycleOwner owner, Child media) {
|
public LiveData<List<Child>> getMediaInstantMix(LifecycleOwner owner, Child media) {
|
||||||
mediaInstantMix.setValue(Collections.emptyList());
|
mediaInstantMix.setValue(Collections.emptyList());
|
||||||
|
|
||||||
songRepository.getInstantMix(media.getId(), 20).observe(owner, mediaInstantMix::postValue);
|
songRepository.getInstantMix(media.getId(), SeedType.TRACK, 20).observe(owner, mediaInstantMix::postValue);
|
||||||
|
|
||||||
return mediaInstantMix;
|
return mediaInstantMix;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
import androidx.lifecycle.Observer;
|
import androidx.lifecycle.Observer;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.media3.session.MediaBrowser;
|
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.interfaces.StarCallback;
|
import com.cappielloantonio.tempo.interfaces.StarCallback;
|
||||||
import com.cappielloantonio.tempo.model.Download;
|
import com.cappielloantonio.tempo.model.Download;
|
||||||
|
|
@ -278,7 +277,7 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
||||||
public LiveData<List<Child>> getMediaInstantMix(LifecycleOwner owner, Child media) {
|
public LiveData<List<Child>> getMediaInstantMix(LifecycleOwner owner, Child media) {
|
||||||
instantMix.setValue(Collections.emptyList());
|
instantMix.setValue(Collections.emptyList());
|
||||||
|
|
||||||
songRepository.getInstantMix(media.getId(), 20).observe(owner, instantMix::postValue);
|
songRepository.getInstantMix(media.getId(), Constants.SeedType.TRACK, 20).observe(owner, instantMix::postValue);
|
||||||
|
|
||||||
return instantMix;
|
return instantMix;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.interfaces.MediaCallback;
|
||||||
import com.cappielloantonio.tempo.interfaces.StarCallback;
|
import com.cappielloantonio.tempo.interfaces.StarCallback;
|
||||||
import com.cappielloantonio.tempo.model.Download;
|
import com.cappielloantonio.tempo.model.Download;
|
||||||
import com.cappielloantonio.tempo.repository.AlbumRepository;
|
import com.cappielloantonio.tempo.repository.AlbumRepository;
|
||||||
|
|
@ -21,6 +22,7 @@ import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Share;
|
import com.cappielloantonio.tempo.subsonic.models.Share;
|
||||||
|
import com.cappielloantonio.tempo.util.Constants.SeedType;
|
||||||
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.NetworkUtil;
|
import com.cappielloantonio.tempo.util.NetworkUtil;
|
||||||
|
|
@ -128,11 +130,22 @@ public class SongBottomSheetViewModel extends AndroidViewModel {
|
||||||
public LiveData<List<Child>> getInstantMix(LifecycleOwner owner, Child media) {
|
public LiveData<List<Child>> getInstantMix(LifecycleOwner owner, Child media) {
|
||||||
instantMix.setValue(Collections.emptyList());
|
instantMix.setValue(Collections.emptyList());
|
||||||
|
|
||||||
songRepository.getInstantMix(media.getId(), 20).observe(owner, instantMix::postValue);
|
songRepository.getInstantMix(media.getId(), SeedType.TRACK, 20).observe(owner, instantMix::postValue);
|
||||||
|
|
||||||
return instantMix;
|
return instantMix;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void getInstantMix(Child media, int count, MediaCallback callback) {
|
||||||
|
|
||||||
|
songRepository.getInstantMix(media.getId(), SeedType.TRACK, count, songs -> {
|
||||||
|
if (songs != null && !songs.isEmpty()) {
|
||||||
|
callback.onLoadMedia(songs);
|
||||||
|
} else {
|
||||||
|
callback.onLoadMedia(Collections.emptyList());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public MutableLiveData<Share> shareTrack() {
|
public MutableLiveData<Share> shareTrack() {
|
||||||
return sharingRepository.createShare(song.getId(), song.getTitle(), null);
|
return sharingRepository.createShare(song.getId(), song.getTitle(), null);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,21 @@
|
||||||
<item>300</item>
|
<item>300</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="streaming_cache_size_titles">
|
||||||
|
<item>禁用</item>
|
||||||
|
<item>128 MiB</item>
|
||||||
|
<item>256 MiB</item>
|
||||||
|
<item>512 MiB</item>
|
||||||
|
<item>1024 MiB</item>
|
||||||
|
</string-array>
|
||||||
|
<string-array name="streaming_cache_size_values">
|
||||||
|
<item>0</item>
|
||||||
|
<item>128</item>
|
||||||
|
<item>256</item>
|
||||||
|
<item>512</item>
|
||||||
|
<item>1024</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
<string-array name="max_bitrate_wifi_list_titles">
|
<string-array name="max_bitrate_wifi_list_titles">
|
||||||
<item>原始</item>
|
<item>原始</item>
|
||||||
<item>32 kbps</item>
|
<item>32 kbps</item>
|
||||||
|
|
@ -224,4 +239,19 @@
|
||||||
<item>4</item>
|
<item>4</item>
|
||||||
<item>8</item>
|
<item>8</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="skip_min_star_rating_titles">
|
||||||
|
<item>不筛选评分</item>
|
||||||
|
<item>1 星及以上</item>
|
||||||
|
<item>2 星及以上</item>
|
||||||
|
<item>3 星及以上</item>
|
||||||
|
<item>4 星及以上</item>
|
||||||
|
</string-array>
|
||||||
|
<string-array name="skip_min_star_rating_values">
|
||||||
|
<item>0</item>
|
||||||
|
<item>1</item>
|
||||||
|
<item>2</item>
|
||||||
|
<item>3</item>
|
||||||
|
<item>4</item>
|
||||||
|
</string-array>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="activity_battery_optimizations_conclusion">如果遇到问题,请访问 https://dontkillmyapp.com。 省电优化选项可能会影响应用的性能,网站上提供了如何禁用这些选项的详细说明。</string>
|
<string name="activity_battery_optimizations_conclusion">如果遇到问题,请访问 https://dontkillmyapp.com。 省电优化选项可能会影响应用的性能,网站上提供了如何禁用这些选项的详细说明。</string>
|
||||||
<string name="activity_battery_optimizations_summary">请禁用针对媒体锁屏播放的电池优化。</string>
|
<string name="activity_battery_optimizations_summary">请禁用针对锁屏播放的电池优化。</string>
|
||||||
<string name="activity_battery_optimizations_title">电池优化</string>
|
<string name="activity_battery_optimizations_title">电池优化</string>
|
||||||
<string name="activity_info_offline_mode">离线模式</string>
|
<string name="activity_info_offline_mode">离线模式</string>
|
||||||
<string name="album_bottom_sheet_add_to_playlist">添加到播放列表</string>
|
<string name="album_bottom_sheet_add_to_playlist">添加到播放列表</string>
|
||||||
<string name="album_bottom_sheet_add_to_queue">添加到队列</string>
|
<string name="album_bottom_sheet_add_to_queue">添加到队列</string>
|
||||||
<string name="album_bottom_sheet_download_all">全部下载</string>
|
<string name="album_bottom_sheet_download_all">全部下载</string>
|
||||||
<string name="album_bottom_sheet_go_to_artist">查看该艺术家</string>
|
<string name="album_bottom_sheet_go_to_artist">查看该艺术家</string>
|
||||||
<string name="album_bottom_sheet_instant_mix">即时混合</string>
|
<string name="album_bottom_sheet_instant_mix">即时混听</string>
|
||||||
<string name="album_bottom_sheet_play_next">下一首播放</string>
|
<string name="album_bottom_sheet_play_next">下一首播放</string>
|
||||||
<string name="album_bottom_sheet_remove_all">移除所有</string>
|
<string name="album_bottom_sheet_remove_all">移除所有</string>
|
||||||
<string name="album_bottom_sheet_share">分享</string>
|
<string name="album_bottom_sheet_share">分享</string>
|
||||||
|
|
@ -17,25 +17,27 @@
|
||||||
<string name="album_error_retrieving_artist">检索艺术家时出错</string>
|
<string name="album_error_retrieving_artist">检索艺术家时出错</string>
|
||||||
<string name="album_list_page_downloaded">已下载的专辑</string>
|
<string name="album_list_page_downloaded">已下载的专辑</string>
|
||||||
<string name="album_list_page_most_played">最常播放的专辑</string>
|
<string name="album_list_page_most_played">最常播放的专辑</string>
|
||||||
<string name="album_list_page_new_releases">新发行</string>
|
<string name="album_list_page_new_releases">新发行的专辑</string>
|
||||||
<string name="album_list_page_recently_added">最近添加的专辑</string>
|
<string name="album_list_page_recently_added">最近添加的专辑</string>
|
||||||
<string name="album_list_page_recently_played">最近播放的专辑</string>
|
<string name="album_list_page_recently_played">最近播放的专辑</string>
|
||||||
<string name="album_list_page_starred">收藏的专辑</string>
|
<string name="album_list_page_starred">收藏的专辑</string>
|
||||||
<string name="album_list_page_title">专辑</string>
|
<string name="album_list_page_title">专辑</string>
|
||||||
<string name="album_page_extra_info_button">更多相似</string>
|
<string name="album_page_extra_info_button">更多相似</string>
|
||||||
<string name="album_page_play_button">播放</string>
|
<string name="album_page_play_button">播放</string>
|
||||||
|
<string name="album_page_release_date_label">发行日期:%1$s</string>
|
||||||
|
<string name="album_page_release_dates_label">发行日期:%1$s(原版发行于 %2$s)</string>
|
||||||
<string name="album_page_shuffle_button">随机播放</string>
|
<string name="album_page_shuffle_button">随机播放</string>
|
||||||
<string name="album_page_tracks_count_and_duration">%1$d 首歌曲 • %2$d 分钟</string>
|
<string name="album_page_tracks_count_and_duration">%1$d 首歌曲 • %2$d 分钟</string>
|
||||||
<string name="app_name">Tempus</string>
|
<string name="app_name">Tempus</string>
|
||||||
<string name="artist_adapter_radio_station_starting">正在搜索...</string>
|
<string name="artist_adapter_radio_station_starting">正在搜索...</string>
|
||||||
<string name="artist_bottom_sheet_instant_mix">即时混合</string>
|
<string name="artist_bottom_sheet_instant_mix">即时混听</string>
|
||||||
<string name="artist_bottom_sheet_shuffle">随机播放</string>
|
<string name="artist_bottom_sheet_shuffle">随机播放</string>
|
||||||
<string name="artist_catalogue_title">艺术家</string>
|
<string name="artist_catalogue_title">艺术家</string>
|
||||||
<string name="artist_catalogue_title_expanded">浏览艺术家</string>
|
<string name="artist_catalogue_title_expanded">浏览艺术家</string>
|
||||||
<string name="artist_error_retrieving_radio">检索艺术家的电台时出错</string>
|
<string name="artist_error_retrieving_radio">检索艺术家的电台时出错</string>
|
||||||
<string name="artist_error_retrieving_tracks">检索艺术家曲目时出错</string>
|
<string name="artist_error_retrieving_tracks">检索艺术家歌曲时出错</string>
|
||||||
<string name="artist_list_page_downloaded">已下载的艺术家</string>
|
<string name="artist_list_page_downloaded">已下载的艺术家</string>
|
||||||
<string name="artist_list_page_starred">收藏的艺人</string>
|
<string name="artist_list_page_starred">收藏的艺术家</string>
|
||||||
<string name="artist_list_page_title">艺术家</string>
|
<string name="artist_list_page_title">艺术家</string>
|
||||||
<string name="artist_page_radio_button">电台</string>
|
<string name="artist_page_radio_button">电台</string>
|
||||||
<string name="artist_page_shuffle_button">随机播放</string>
|
<string name="artist_page_shuffle_button">随机播放</string>
|
||||||
|
|
@ -43,33 +45,63 @@
|
||||||
<string name="artist_page_title_album_more_like_this_button">更多相似</string>
|
<string name="artist_page_title_album_more_like_this_button">更多相似</string>
|
||||||
<string name="artist_page_title_album_section">专辑</string>
|
<string name="artist_page_title_album_section">专辑</string>
|
||||||
<string name="artist_page_title_biography_more_button">更多</string>
|
<string name="artist_page_title_biography_more_button">更多</string>
|
||||||
<string name="artist_page_title_biography_section">个人简介</string>
|
<string name="artist_page_title_biography_section">艺术家简介</string>
|
||||||
<string name="artist_page_title_most_streamed_song_section">最常播放的歌曲</string>
|
<string name="artist_page_title_most_streamed_song_section">最常播放的歌曲</string>
|
||||||
<string name="artist_page_title_most_streamed_song_see_all_button">查看全部</string>
|
<string name="artist_page_title_most_streamed_song_see_all_button">查看全部</string>
|
||||||
|
<string name="asset_link_chip_text">%1$s • %2$s</string>
|
||||||
|
<string name="asset_link_clipboard_label">Tempus 资源链接</string>
|
||||||
|
<string name="asset_link_copied_toast">已将 %1$s 复制到剪贴板</string>
|
||||||
|
<string name="asset_link_debug_toast">资源链接:%1$s</string>
|
||||||
|
<string name="asset_link_error_album">无法打开该专辑</string>
|
||||||
|
<string name="asset_link_error_artist">无法打开该艺术家页</string>
|
||||||
|
<string name="asset_link_error_playlist">无法打开该播放列表</string>
|
||||||
|
<string name="asset_link_error_song">无法打开该歌曲</string>
|
||||||
|
<string name="asset_link_error_unsupported">不支持的资源链接</string>
|
||||||
|
<string name="asset_link_label_album">专辑 UID</string>
|
||||||
|
<string name="asset_link_label_artist">艺术家 UID</string>
|
||||||
|
<string name="asset_link_label_genre">流派 UID</string>
|
||||||
|
<string name="asset_link_label_playlist">播放列表 UID</string>
|
||||||
|
<string name="asset_link_label_song">歌曲 UID</string>
|
||||||
|
<string name="asset_link_label_unknown">资源 UID</string>
|
||||||
|
<string name="asset_link_label_year">年份 UID</string>
|
||||||
<string name="battery_optimization_negative_button">忽略</string>
|
<string name="battery_optimization_negative_button">忽略</string>
|
||||||
<string name="battery_optimization_neutral_button">不要再问</string>
|
<string name="battery_optimization_neutral_button">不再询问</string>
|
||||||
<string name="battery_optimization_positive_button">禁用</string>
|
<string name="battery_optimization_positive_button">禁用</string>
|
||||||
|
<string name="cast_expanded_controller_loading">加载中...</string>
|
||||||
<string name="connection_alert_dialog_negative_button">取消</string>
|
<string name="connection_alert_dialog_negative_button">取消</string>
|
||||||
<string name="connection_alert_dialog_neutral_button">启用流量节省</string>
|
<string name="connection_alert_dialog_neutral_button">启用流量节省</string>
|
||||||
<string name="connection_alert_dialog_positive_button">确定</string>
|
<string name="connection_alert_dialog_positive_button">确定</string>
|
||||||
<string name="connection_alert_dialog_summary">已限制通过 Wi-Fi 以外的连接访问 Subsonic 服务器。 要阻止此警告对话框再次出现,请在应用程序设置中禁用连接检查。</string>
|
<string name="connection_alert_dialog_summary">已限制通过 Wi-Fi 以外的连接访问 Subsonic 服务器。 要阻止此警告对话框再次出现,请在应用程序设置中禁用连接检查。</string>
|
||||||
<string name="connection_alert_dialog_title">Wi-Fi网络未连接</string>
|
<string name="connection_alert_dialog_title">Wi-Fi 网络未连接</string>
|
||||||
|
<string name="content_description_shuffle_button">随机</string>
|
||||||
<string name="delete_download_storage_dialog_negative_button">取消</string>
|
<string name="delete_download_storage_dialog_negative_button">取消</string>
|
||||||
<string name="delete_download_storage_dialog_positive_button">继续</string>
|
<string name="delete_download_storage_dialog_positive_button">继续</string>
|
||||||
<string name="delete_download_storage_dialog_summary">请注意,继续执行此操作将永久删除从所有服务器下载的所有已保存的项目。</string>
|
<string name="delete_download_storage_dialog_summary">请注意,继续执行此操作将永久删除从所有服务器下载的所有已保存的项目。</string>
|
||||||
<string name="delete_download_storage_dialog_title">删除已保存的项目</string>
|
<string name="delete_download_storage_dialog_title">删除已保存的项目</string>
|
||||||
<string name="description_empty_title">没有可用的描述</string>
|
<string name="description_empty_title">没有可用的歌词</string>
|
||||||
|
<string name="disc_titlefull">第 %1$s 张光盘 - %2$s</string>
|
||||||
|
<string name="disc_titleless">第 %1$s 张光盘</string>
|
||||||
<string name="download_directory_dialog_negative_button">取消</string>
|
<string name="download_directory_dialog_negative_button">取消</string>
|
||||||
<string name="download_directory_dialog_positive_button">下载</string>
|
<string name="download_directory_dialog_positive_button">下载</string>
|
||||||
<string name="download_directory_dialog_summary">该文件夹中的所有曲目将被下载。 子文件夹中的曲目将不会被下载。</string>
|
<string name="download_directory_dialog_summary">该文件夹中的所有歌曲将被下载。子文件夹中的歌曲将不会被下载。</string>
|
||||||
<string name="download_directory_dialog_title">下载曲目</string>
|
<string name="download_directory_dialog_title">下载歌曲</string>
|
||||||
<string name="download_info_empty_subtitle">下载歌曲后,您可以在这里找到它。</string>
|
<string name="download_directory_set">设置歌曲下载位置</string>
|
||||||
|
<string name="download_info_empty_subtitle">下载歌曲后,您可以在这里找到它</string>
|
||||||
<string name="download_info_empty_title">还没有下载!</string>
|
<string name="download_info_empty_title">还没有下载!</string>
|
||||||
<string name="download_item_multiple_subtitle_formatter">%1$s • %2$s 个项目</string>
|
<string name="download_item_multiple_subtitle_formatter">%1$s • %2$s 个项目</string>
|
||||||
<string name="download_item_single_subtitle_formatter">%1$s 个项目</string>
|
<string name="download_item_single_subtitle_formatter">%1$s 个项目</string>
|
||||||
|
<string name="download_refresh_button_content_description">刷新下载项</string>
|
||||||
|
<string name="download_refresh_no_changes">没有遗漏的下载项。</string>
|
||||||
|
<string name="download_refresh_no_directory">设置下载目录以刷新下载内容。</string>
|
||||||
|
<plurals name="download_refresh_removed">
|
||||||
|
<item quantity="one">已移除 %d 个缺失的下载项。</item>
|
||||||
|
<item quantity="other">已移除 %d 个缺失的下载项。</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="download_shuffle_all_subtitle">随机播放全部</string>
|
||||||
<string name="download_storage_dialog_sub_summary">要使更改生效,请重新启动应用程序。</string>
|
<string name="download_storage_dialog_sub_summary">要使更改生效,请重新启动应用程序。</string>
|
||||||
<string name="download_storage_dialog_summary">更改已下载文件的目录将会立即删除以前已下载的所有文件。</string>
|
<string name="download_storage_dialog_summary">更改已下载文件的目录将会立即删除以前已下载的所有文件。</string>
|
||||||
<string name="download_storage_dialog_title">选择存储选项</string>
|
<string name="download_storage_dialog_title">选择存储选项</string>
|
||||||
|
<string name="download_storage_directory_dialog_neutral_button">目录</string>
|
||||||
<string name="download_storage_external_dialog_positive_button">外部</string>
|
<string name="download_storage_external_dialog_positive_button">外部</string>
|
||||||
<string name="download_storage_internal_dialog_negative_button">内部</string>
|
<string name="download_storage_internal_dialog_negative_button">内部</string>
|
||||||
<string name="download_title_section">下载</string>
|
<string name="download_title_section">下载</string>
|
||||||
|
|
@ -78,31 +110,75 @@
|
||||||
<string name="downloaded_bottom_sheet_remove">移除</string>
|
<string name="downloaded_bottom_sheet_remove">移除</string>
|
||||||
<string name="downloaded_bottom_sheet_remove_all">移除所有</string>
|
<string name="downloaded_bottom_sheet_remove_all">移除所有</string>
|
||||||
<string name="downloaded_bottom_sheet_shuffle">随机播放</string>
|
<string name="downloaded_bottom_sheet_shuffle">随机播放</string>
|
||||||
<string name="empty_string"></string>
|
<string name="empty_string" />
|
||||||
|
<string name="equalizer_enable">启用</string>
|
||||||
|
<string name="equalizer_fragment_title">均衡器</string>
|
||||||
|
<string name="equalizer_not_supported">此设备不支持</string>
|
||||||
|
<string name="equalizer_reset">重置</string>
|
||||||
<string name="error_required">必需</string>
|
<string name="error_required">必需</string>
|
||||||
<string name="error_server_prefix">必须是 http 或 https 前缀</string>
|
<string name="error_server_prefix">必须是 http 或 https 前缀</string>
|
||||||
|
<string name="exo_controls_heart_off_description">取消收藏</string>
|
||||||
|
<string name="exo_controls_heart_on_description">收藏</string>
|
||||||
<string name="exo_download_notification_channel_name">下载</string>
|
<string name="exo_download_notification_channel_name">下载</string>
|
||||||
|
<string name="filter_artist">筛选艺术家</string>
|
||||||
<string name="filter_info_selection">选择两个或多个过滤器</string>
|
<string name="filter_info_selection">选择两个或多个过滤器</string>
|
||||||
<string name="filter_title">筛选</string>
|
<string name="filter_title">筛选</string>
|
||||||
<string name="filter_title_expanded">筛选流派</string>
|
<string name="filter_title_expanded">筛选流派</string>
|
||||||
|
<string name="folder_play_collecting">正在读取文件夹中的歌曲...</string>
|
||||||
|
<string name="folder_play_no_songs">文件夹内未发现歌曲</string>
|
||||||
|
<string name="folder_play_playing">正在播放 %d 首歌曲</string>
|
||||||
|
<string name="generic_list_page_count">(%1$d)</string>
|
||||||
|
<string name="generic_list_page_count_unknown">(+%1$d)</string>
|
||||||
<string name="genre_catalogue_title">流派目录</string>
|
<string name="genre_catalogue_title">流派目录</string>
|
||||||
<string name="genre_catalogue_title_expanded">浏览流派</string>
|
<string name="genre_catalogue_title_expanded">浏览流派</string>
|
||||||
|
<string name="github_update_dialog_negative_button">稍后提醒</string>
|
||||||
|
<string name="github_update_dialog_neutral_button">支持项目</string>
|
||||||
|
<string name="github_update_dialog_positive_button">立即下载</string>
|
||||||
|
<string name="github_update_dialog_summary">GitHub 上发布了新版本。</string>
|
||||||
|
<string name="github_update_dialog_title">有可用更新</string>
|
||||||
|
<string name="home_option_reorganize">定制首页</string>
|
||||||
|
<string name="home_rearrangement_dialog_negative_button">取消</string>
|
||||||
|
<string name="home_rearrangement_dialog_neutral_button">重置</string>
|
||||||
|
<string name="home_rearrangement_dialog_positive_button">保存</string>
|
||||||
|
<string name="home_rearrangement_dialog_subtitle">请重启应用以应用更改。</string>
|
||||||
|
<string name="home_rearrangement_dialog_title">主页排序</string>
|
||||||
|
<string name="home_section_music">音乐</string>
|
||||||
|
<string name="home_section_podcast">播客</string>
|
||||||
|
<string name="home_section_radio">电台</string>
|
||||||
<string name="home_subtitle_best_of">您最喜欢的艺术家的热门歌曲</string>
|
<string name="home_subtitle_best_of">您最喜欢的艺术家的热门歌曲</string>
|
||||||
<string name="home_subtitle_made_for_you">从您喜欢的歌曲开始混音</string>
|
<string name="home_subtitle_made_for_you">从您喜欢的歌曲开始混听</string>
|
||||||
<string name="home_subtitle_new_internet_radio_station">添加新的电台</string>
|
<string name="home_subtitle_new_internet_radio_station">添加新的电台</string>
|
||||||
<string name="home_subtitle_new_podcast_channel">添加新的播客频道</string>
|
<string name="home_subtitle_new_podcast_channel">添加新的播客频道</string>
|
||||||
|
<plurals name="home_sync_starred_albums_count">
|
||||||
|
<item quantity="one">%d 个待同步专辑</item>
|
||||||
|
<item quantity="other">%d 个待同步专辑</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="home_sync_starred_albums_subtitle">标记为收藏的专辑可离线使用</string>
|
||||||
|
<string name="home_sync_starred_albums_title">同步收藏的专辑</string>
|
||||||
|
<string name="home_sync_starred_artists_subtitle">你收藏的艺术家有未下载的歌曲</string>
|
||||||
|
<string name="home_sync_starred_artists_title">同步收藏的艺术家</string>
|
||||||
|
<plurals name="home_sync_starred_artists_count">
|
||||||
|
<item quantity="one">%d 个待同步艺术家</item>
|
||||||
|
<item quantity="other">%d 个待同步艺术家</item>
|
||||||
|
</plurals>
|
||||||
<string name="home_sync_starred_cancel">取消</string>
|
<string name="home_sync_starred_cancel">取消</string>
|
||||||
<string name="home_sync_starred_download">下载</string>
|
<string name="home_sync_starred_download">下载</string>
|
||||||
<string name="home_sync_starred_subtitle">下载这些曲目可能需要大量移动数据</string>
|
<plurals name="home_sync_starred_songs_count">
|
||||||
<string name="home_sync_starred_title">似乎有一些已收藏的曲目需要同步</string>
|
<item quantity="one">%d 首待同步歌曲</item>
|
||||||
|
<item quantity="other">%d 首待同步歌曲</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="home_sync_starred_subtitle">下载这些歌曲可能需要大量移动数据流量</string>
|
||||||
|
<string name="home_sync_starred_title">似乎有一些收藏的歌曲需要同步</string>
|
||||||
<string name="home_title_best_of">最佳</string>
|
<string name="home_title_best_of">最佳</string>
|
||||||
<string name="home_title_discovery">发现</string>
|
<string name="home_title_discovery">发现</string>
|
||||||
<string name="home_title_discovery_shuffle_all_button">全部随机播放</string>
|
<string name="home_title_discovery_shuffle_all_button">随机播放全部</string>
|
||||||
<string name="home_title_flashback">闪回</string>
|
<string name="home_title_flashback">重温旧曲</string>
|
||||||
<string name="home_title_internet_radio_station">网络广播电台</string>
|
<string name="home_title_internet_radio_station">网络广播电台</string>
|
||||||
|
<string name="home_title_last_month">上月</string>
|
||||||
<string name="home_title_last_played">最近播放</string>
|
<string name="home_title_last_played">最近播放</string>
|
||||||
<string name="home_title_last_played_see_all_button">查看全部</string>
|
<string name="home_title_last_played_see_all_button">查看全部</string>
|
||||||
<string name="home_title_last_week">上周</string>
|
<string name="home_title_last_week">上周</string>
|
||||||
|
<string name="home_title_last_year">去年</string>
|
||||||
<string name="home_title_made_for_you">为您定制</string>
|
<string name="home_title_made_for_you">为您定制</string>
|
||||||
<string name="home_title_most_played">最常播放</string>
|
<string name="home_title_most_played">最常播放</string>
|
||||||
<string name="home_title_most_played_see_all_button">查看全部</string>
|
<string name="home_title_most_played_see_all_button">查看全部</string>
|
||||||
|
|
@ -119,7 +195,7 @@
|
||||||
<string name="home_title_starred_albums_see_all_button">查看全部</string>
|
<string name="home_title_starred_albums_see_all_button">查看全部</string>
|
||||||
<string name="home_title_starred_artists">★ 收藏的艺术家</string>
|
<string name="home_title_starred_artists">★ 收藏的艺术家</string>
|
||||||
<string name="home_title_starred_artists_see_all_button">查看全部</string>
|
<string name="home_title_starred_artists_see_all_button">查看全部</string>
|
||||||
<string name="home_title_starred_tracks">★ 收藏的曲目</string>
|
<string name="home_title_starred_tracks">★ 收藏的歌曲</string>
|
||||||
<string name="home_title_starred_tracks_see_all_button">查看全部</string>
|
<string name="home_title_starred_tracks_see_all_button">查看全部</string>
|
||||||
<string name="home_title_top_songs">你最喜欢的歌曲</string>
|
<string name="home_title_top_songs">你最喜欢的歌曲</string>
|
||||||
<string name="label_dot_separator" translatable="false">•</string>
|
<string name="label_dot_separator" translatable="false">•</string>
|
||||||
|
|
@ -146,31 +222,53 @@
|
||||||
<string name="menu_group_by_album">专辑</string>
|
<string name="menu_group_by_album">专辑</string>
|
||||||
<string name="menu_group_by_artist">艺术家</string>
|
<string name="menu_group_by_artist">艺术家</string>
|
||||||
<string name="menu_group_by_genre">流派</string>
|
<string name="menu_group_by_genre">流派</string>
|
||||||
<string name="menu_group_by_track">曲目</string>
|
<string name="menu_group_by_track">歌曲</string>
|
||||||
<string name="menu_group_by_year">年份</string>
|
<string name="menu_group_by_year">年份</string>
|
||||||
<string name="menu_home_label">首页</string>
|
<string name="menu_home_label">首页</string>
|
||||||
|
<string name="menu_last_month_name">上月</string>
|
||||||
|
<string name="menu_last_week_name">上周</string>
|
||||||
<string name="menu_last_year_name">去年</string>
|
<string name="menu_last_year_name">去年</string>
|
||||||
<string name="menu_library_label">曲库</string>
|
<string name="menu_library_label">曲库</string>
|
||||||
|
<string name="menu_pin_button">添加到主页</string>
|
||||||
|
<string name="menu_rate_album">专辑评分</string>
|
||||||
<string name="menu_search_button">搜索</string>
|
<string name="menu_search_button">搜索</string>
|
||||||
<string name="menu_settings_button">设置</string>
|
<string name="menu_settings_button">设置</string>
|
||||||
|
<string name="menu_sort_album_count">专辑数量</string>
|
||||||
<string name="menu_sort_artist">艺术家</string>
|
<string name="menu_sort_artist">艺术家</string>
|
||||||
<string name="menu_sort_name">姓名</string>
|
<string name="menu_sort_least_recently_starred">最早收藏</string>
|
||||||
|
<string name="menu_sort_most_played">最多播放</string>
|
||||||
|
<string name="menu_sort_most_recently_starred">最近收藏</string>
|
||||||
|
<string name="menu_sort_name">名称</string>
|
||||||
<string name="menu_sort_random">随机</string>
|
<string name="menu_sort_random">随机</string>
|
||||||
<string name="menu_sort_recently_added">最近添加</string>
|
<string name="menu_sort_recently_added">最近添加</string>
|
||||||
|
<string name="menu_sort_recently_played">最近播放</string>
|
||||||
<string name="menu_sort_year">年份</string>
|
<string name="menu_sort_year">年份</string>
|
||||||
|
<string name="menu_unpin_button">从主页移除</string>
|
||||||
|
<string name="player_lyrics_download_content_description">下载离线歌词</string>
|
||||||
|
<string name="player_lyrics_download_failure">暂无歌词可供下载。</string>
|
||||||
|
<string name="player_lyrics_download_success">离线歌词已保存。</string>
|
||||||
|
<string name="player_lyrics_downloaded_content_description">已下载的离线歌词</string>
|
||||||
<string name="player_playback_speed">%1$.2fx</string>
|
<string name="player_playback_speed">%1$.2fx</string>
|
||||||
<string name="player_queue_clean_all_button">清空队列</string>
|
<string name="player_queue_clean_all_button">清空队列</string>
|
||||||
|
<string name="player_queue_load_queue">加载队列</string>
|
||||||
|
<string name="player_queue_save_queue_success">保存队列</string>
|
||||||
|
<string name="player_queue_save_to_playlist">保存队列到播放列表</string>
|
||||||
<string name="player_server_priority">服务器优先级</string>
|
<string name="player_server_priority">服务器优先级</string>
|
||||||
|
<string name="player_transcoding">正在转码</string>
|
||||||
|
<string name="player_transcoding_requested">已请求转码</string>
|
||||||
|
<string name="player_unknown_format">未知格式</string>
|
||||||
<string name="playlist_catalogue_title">播放列表目录</string>
|
<string name="playlist_catalogue_title">播放列表目录</string>
|
||||||
<string name="playlist_catalogue_title_expanded">浏览播放列表</string>
|
<string name="playlist_catalogue_title_expanded">浏览播放列表</string>
|
||||||
<string name="playlist_chooser_dialog_empty">尚未创建播放列表</string>
|
<string name="playlist_chooser_dialog_empty">尚未创建播放列表</string>
|
||||||
<string name="playlist_chooser_dialog_negative_button">取消</string>
|
<string name="playlist_chooser_dialog_negative_button">取消</string>
|
||||||
<string name="playlist_chooser_dialog_neutral_button">新建</string>
|
<string name="playlist_chooser_dialog_neutral_button">新建</string>
|
||||||
<string name="playlist_chooser_dialog_title">添加到播放列表</string>
|
<string name="playlist_chooser_dialog_title">添加到播放列表</string>
|
||||||
<string name="playlist_chooser_dialog_toast_add_success">将歌曲添加到播放列表</string>
|
|
||||||
<string name="playlist_chooser_dialog_toast_add_failure">未能将歌曲添加到播放列表</string>
|
<string name="playlist_chooser_dialog_toast_add_failure">未能将歌曲添加到播放列表</string>
|
||||||
<string name="playlist_counted_tracks">%1$d 首曲目 • %2$s</string>
|
<string name="playlist_chooser_dialog_toast_add_success">将歌曲添加到播放列表</string>
|
||||||
<string name="playlist_duration">持续时间 • %1$s</string>
|
<string name="playlist_chooser_dialog_toast_all_skipped">所有歌曲已存在,无需重复添加</string>
|
||||||
|
<string name="playlist_counted_tracks">%1$d 首歌曲 • %2$s</string>
|
||||||
|
<string name="playlist_duration">时长 • %1$s</string>
|
||||||
|
<string name="playlist_editor_dialog_action_delete_toast">长按删除</string>
|
||||||
<string name="playlist_editor_dialog_hint_name">播放列表名称</string>
|
<string name="playlist_editor_dialog_hint_name">播放列表名称</string>
|
||||||
<string name="playlist_editor_dialog_negative_button">取消</string>
|
<string name="playlist_editor_dialog_negative_button">取消</string>
|
||||||
<string name="playlist_editor_dialog_neutral_button">删除</string>
|
<string name="playlist_editor_dialog_neutral_button">删除</string>
|
||||||
|
|
@ -189,6 +287,7 @@
|
||||||
<string name="podcast_channel_catalogue_title_expanded">浏览频道</string>
|
<string name="podcast_channel_catalogue_title_expanded">浏览频道</string>
|
||||||
<string name="podcast_channel_editor_dialog_hint_rss_url">RSS 网址</string>
|
<string name="podcast_channel_editor_dialog_hint_rss_url">RSS 网址</string>
|
||||||
<string name="podcast_channel_editor_dialog_title">播客频道</string>
|
<string name="podcast_channel_editor_dialog_title">播客频道</string>
|
||||||
|
<string name="podcast_channel_not_supported_snackbar">此服务器不支持播客。</string>
|
||||||
<string name="podcast_channel_page_title_description_section">描述</string>
|
<string name="podcast_channel_page_title_description_section">描述</string>
|
||||||
<string name="podcast_channel_page_title_episode_section">剧集</string>
|
<string name="podcast_channel_page_title_episode_section">剧集</string>
|
||||||
<string name="podcast_channel_page_title_no_episode_available">没有可用的剧集</string>
|
<string name="podcast_channel_page_title_no_episode_available">没有可用的剧集</string>
|
||||||
|
|
@ -197,6 +296,8 @@
|
||||||
<string name="podcast_info_empty_subtitle">添加频道后,您将在此处找到它</string>
|
<string name="podcast_info_empty_subtitle">添加频道后,您将在此处找到它</string>
|
||||||
<string name="podcast_info_empty_title">未找到播客!</string>
|
<string name="podcast_info_empty_title">未找到播客!</string>
|
||||||
<string name="podcast_release_date_duration_formatter">%1$s • %2$s</string>
|
<string name="podcast_release_date_duration_formatter">%1$s • %2$s</string>
|
||||||
|
<string name="radio_dialog_not_supported_snackbar">服务器不支持网络电台管理。</string>
|
||||||
|
<string name="radio_editor_dialog_added">已添加电台</string>
|
||||||
<string name="radio_editor_dialog_hint_homepage_url">电台主页 URL</string>
|
<string name="radio_editor_dialog_hint_homepage_url">电台主页 URL</string>
|
||||||
<string name="radio_editor_dialog_hint_name">电台名称</string>
|
<string name="radio_editor_dialog_hint_name">电台名称</string>
|
||||||
<string name="radio_editor_dialog_hint_stream_url">广播流 URL</string>
|
<string name="radio_editor_dialog_hint_stream_url">广播流 URL</string>
|
||||||
|
|
@ -204,6 +305,7 @@
|
||||||
<string name="radio_editor_dialog_neutral_button">删除</string>
|
<string name="radio_editor_dialog_neutral_button">删除</string>
|
||||||
<string name="radio_editor_dialog_positive_button">保存</string>
|
<string name="radio_editor_dialog_positive_button">保存</string>
|
||||||
<string name="radio_editor_dialog_title">网络广播电台</string>
|
<string name="radio_editor_dialog_title">网络广播电台</string>
|
||||||
|
<string name="radio_editor_dialog_updated">已更新电台</string>
|
||||||
<string name="radio_station_info_empty_button">单击以隐藏该部分\n重启应用后生效</string>
|
<string name="radio_station_info_empty_button">单击以隐藏该部分\n重启应用后生效</string>
|
||||||
<string name="radio_station_info_empty_subtitle">添加广播电台后,您可以在此处找到它</string>
|
<string name="radio_station_info_empty_subtitle">添加广播电台后,您可以在此处找到它</string>
|
||||||
<string name="radio_station_info_empty_title">没有找到电台!</string>
|
<string name="radio_station_info_empty_title">没有找到电台!</string>
|
||||||
|
|
@ -212,10 +314,14 @@
|
||||||
<string name="rating_dialog_title">评分</string>
|
<string name="rating_dialog_title">评分</string>
|
||||||
<string name="search_hint">搜索标题、艺术家或专辑</string>
|
<string name="search_hint">搜索标题、艺术家或专辑</string>
|
||||||
<string name="search_info_minimum_characters">输入至少三个字符</string>
|
<string name="search_info_minimum_characters">输入至少三个字符</string>
|
||||||
|
<string name="search_sort_summary">启用后将按时间排序搜索,关闭则按名称排序。</string>
|
||||||
|
<string name="search_sort_title">按时间排序最近搜索</string>
|
||||||
<string name="search_title_album">专辑</string>
|
<string name="search_title_album">专辑</string>
|
||||||
<string name="search_title_artist">艺术家</string>
|
<string name="search_title_artist">艺术家</string>
|
||||||
<string name="search_title_song">歌曲</string>
|
<string name="search_title_song">歌曲</string>
|
||||||
|
<string name="server_signup_dialog_action_delete_toast">长按删除</string>
|
||||||
<string name="server_signup_dialog_action_low_security">低安全性</string>
|
<string name="server_signup_dialog_action_low_security">低安全性</string>
|
||||||
|
<string name="server_signup_dialog_hint_local_address">本地 URL</string>
|
||||||
<string name="server_signup_dialog_hint_name">服务器名称</string>
|
<string name="server_signup_dialog_hint_name">服务器名称</string>
|
||||||
<string name="server_signup_dialog_hint_password">密码</string>
|
<string name="server_signup_dialog_hint_password">密码</string>
|
||||||
<string name="server_signup_dialog_hint_url">服务器地址</string>
|
<string name="server_signup_dialog_hint_url">服务器地址</string>
|
||||||
|
|
@ -231,84 +337,118 @@
|
||||||
<string name="server_unreachable_dialog_title">服务器无法访问</string>
|
<string name="server_unreachable_dialog_title">服务器无法访问</string>
|
||||||
<string name="settings_about_summary">Tempus 是 Subsonic 的开源轻量级音乐客户端,专为 Android 设计和构建。</string>
|
<string name="settings_about_summary">Tempus 是 Subsonic 的开源轻量级音乐客户端,专为 Android 设计和构建。</string>
|
||||||
<string name="settings_about_title">关于</string>
|
<string name="settings_about_title">关于</string>
|
||||||
|
<string name="settings_album_detail">显示专辑详情</string>
|
||||||
|
<string name="settings_album_detail_summary">启用后将在专辑页显示流派、歌曲数量等信息</string>
|
||||||
|
<string name="settings_allow_playlist_duplicates">允许添加重复歌曲到播放列表</string>
|
||||||
|
<string name="settings_allow_playlist_duplicates_summary">启用后则添加到播放列表时将不再检查重复内容。</string>
|
||||||
<string name="settings_always_on_display">保持屏幕常亮</string>
|
<string name="settings_always_on_display">保持屏幕常亮</string>
|
||||||
|
<string name="settings_app_equalizer">均衡器</string>
|
||||||
|
<string name="settings_app_equalizer_summary">打开内置均衡器</string>
|
||||||
|
<string name="settings_artist_sort_by_album_count">按专辑数量排序艺术家</string>
|
||||||
|
<string name="settings_artist_sort_by_album_count_summary">启用后按专辑数量排序;关闭则按名称排序。</string>
|
||||||
|
<string name="settings_audio_quality">显示音频质量</string>
|
||||||
|
<string name="settings_audio_quality_summary">显示歌曲的码率和音频格式。</string>
|
||||||
<string name="settings_audio_transcode_download_format">转码格式</string>
|
<string name="settings_audio_transcode_download_format">转码格式</string>
|
||||||
<string name="settings_audio_transcode_download_priority_summary">如果启用,Tempus 将不会强制使用下面的转码设置下载曲目。</string>
|
<string name="settings_audio_transcode_download_priority_summary">启用后 Tempus 将不会强制使用下面的转码设置下载歌曲。</string>
|
||||||
<string name="settings_audio_transcode_download_priority_title">优先考虑服务器上用于流式传输的设置</string>
|
<string name="settings_audio_transcode_download_priority_title">优先考虑服务器上用于流式传输的设置</string>
|
||||||
<string name="settings_audio_transcode_download_summary">如果启用,Tempus 将下载转码后的曲目。</string>
|
<string name="settings_audio_transcode_download_summary">启用后 Tempus 将下载转码后的歌曲。</string>
|
||||||
<string name="settings_audio_transcode_download_title">下载转码后的曲目</string>
|
<string name="settings_audio_transcode_download_title">下载转码后的歌曲</string>
|
||||||
<string name="settings_audio_transcode_estimate_content_length_summary">如果启用,将发送请求到服务器以查询曲目的估计持续时间。</string>
|
<string name="settings_audio_transcode_estimate_content_length_summary">启用后将发送请求到服务器以查询歌曲的估计持续时间。</string>
|
||||||
<string name="settings_audio_transcode_estimate_content_length_title">估计内容长度</string>
|
<string name="settings_audio_transcode_estimate_content_length_title">估计内容长度</string>
|
||||||
<string name="settings_audio_transcode_format_download">用于下载的转码格式</string>
|
<string name="settings_audio_transcode_format_download">用于下载的转码格式</string>
|
||||||
<string name="settings_audio_transcode_format_mobile">移动数据下的转码格式</string>
|
<string name="settings_audio_transcode_format_mobile">移动数据下的转码格式</string>
|
||||||
<string name="settings_audio_transcode_format_wifi">Wi-Fi 下的转码格式</string>
|
<string name="settings_audio_transcode_format_wifi">Wi-Fi 下的转码格式</string>
|
||||||
<string name="settings_audio_transcode_priority_summary">如果启用,Tempus 将不会强制使用下面的转码设置流式传输曲目。</string>
|
<string name="settings_audio_transcode_priority_summary">启用后 Tempus 将不会强制使用下面的转码设置流式传输歌曲。</string>
|
||||||
<string name="settings_audio_transcode_priority_title">优先考虑服务器转码设置</string>
|
<string name="settings_audio_transcode_priority_title">优先考虑服务器转码设置</string>
|
||||||
<string name="settings_audio_transcode_priority_toast">曲目转码设置优先级设置为服务器</string>
|
<string name="settings_audio_transcode_priority_toast">歌曲转码设置优先级设置为服务器</string>
|
||||||
|
<string name="settings_auto_download_lyrics">自动下载歌词</string>
|
||||||
|
<string name="settings_auto_download_lyrics_summary">自动保存可用歌词,以便离线时查看。</string>
|
||||||
<string name="settings_buffering_strategy">缓存策略</string>
|
<string name="settings_buffering_strategy">缓存策略</string>
|
||||||
<string name="settings_buffering_strategy_summary">为了使更改生效,您必须手动重新启动应用程序。</string>
|
<string name="settings_buffering_strategy_summary">为了使更改生效,您必须手动重新启动应用程序。</string>
|
||||||
<string name="settings_continuous_play_summary">允许在播放列表结束后,播放相似的曲目。</string>
|
<string name="settings_choose_download_folder">选择一个音乐下载目录</string>
|
||||||
|
<string name="settings_clear_download_folder">清空下载文件夹</string>
|
||||||
|
<string name="settings_continuous_play_summary">允许在播放列表结束后,播放相似的歌曲。</string>
|
||||||
<string name="settings_continuous_play_title">连续播放</string>
|
<string name="settings_continuous_play_title">连续播放</string>
|
||||||
<string name="settings_covers_cache">图片缓存大小</string>
|
<string name="settings_covers_cache">图片缓存大小</string>
|
||||||
<string name="settings_data_saving_mode_summary">为了减少数据消耗,请避免下载封面。</string>
|
<string name="settings_data_saving_mode_summary">为了减少数据消耗,请避免下载封面。</string>
|
||||||
<string name="settings_data_saving_mode_title">限制移动数据使用</string>
|
<string name="settings_data_saving_mode_title">限制移动数据使用</string>
|
||||||
<string name="settings_delete_download_storage_summary">继续当前操作将导致所有已保存的项目被永久删除。</string>
|
<string name="settings_delete_download_storage_summary">继续当前操作将导致所有已保存的项目被永久删除。</string>
|
||||||
<string name="settings_delete_download_storage_title">删除已保存的项目</string>
|
<string name="settings_delete_download_storage_title">删除已保存的项目</string>
|
||||||
|
<string name="settings_download_folder_cleared">下载文件夹已清除。</string>
|
||||||
|
<string name="settings_download_folder_set">已设置下载文件夹</string>
|
||||||
<string name="settings_download_storage_title">下载存储</string>
|
<string name="settings_download_storage_title">下载存储</string>
|
||||||
<string name="settings_system_equalizer_summary">调整音频设置</string>
|
|
||||||
<string name="settings_system_equalizer_title">系统均衡器</string>
|
|
||||||
<string name="settings_github_link">https://github.com/eddyizm/tempus</string>
|
<string name="settings_github_link">https://github.com/eddyizm/tempus</string>
|
||||||
<string name="settings_github_summary">关注开发进展</string>
|
<string name="settings_github_summary">关注开发进展</string>
|
||||||
<string name="settings_github_title">Github</string>
|
<string name="settings_github_title">Github</string>
|
||||||
|
<string name="settings_github_update">更新</string>
|
||||||
|
<string name="settings_github_update_summary">GitHub 版本默认会自动检查 APK 更新。您可以关闭此开关以禁用自动检查。</string>
|
||||||
|
<string name="settings_github_update_title">请访问 Github 以检查更新</string>
|
||||||
<string name="settings_image_size">设置图像分辨率</string>
|
<string name="settings_image_size">设置图像分辨率</string>
|
||||||
|
<string name="settings_item_rating">显示评分</string>
|
||||||
|
<string name="settings_item_rating_summary">启用后则显示项目的评分和收藏状态。</string>
|
||||||
<string name="settings_language">语言</string>
|
<string name="settings_language">语言</string>
|
||||||
<string name="settings_logout_title">注销登录</string>
|
<string name="settings_logout_title">注销登录</string>
|
||||||
<string name="settings_max_bitrate_download">用于下载的比特率</string>
|
<string name="settings_max_bitrate_download">用于下载的码率</string>
|
||||||
<string name="settings_max_bitrate_mobile">移动数据下的比特率</string>
|
<string name="settings_max_bitrate_mobile">移动数据下的码率</string>
|
||||||
<string name="settings_max_bitrate_wifi">Wi-Fi 下的比特率</string>
|
<string name="settings_max_bitrate_wifi">Wi-Fi 下的码率</string>
|
||||||
<string name="settings_media_cache">媒体文件缓存大小</string>
|
<string name="settings_media_cache">媒体文件缓存大小</string>
|
||||||
<string name="settings_music_directory">显示音乐目录</string>
|
<string name="settings_music_directory">显示音乐目录</string>
|
||||||
<string name="settings_music_directory_summary">如果启用,则显示音乐目录部分。 请注意,要使文件夹导航正常工作,服务器必须支持此功能。</string>
|
<string name="settings_music_directory_summary">启用后则显示音乐目录部分。 请注意,要使文件夹导航正常工作,服务器必须支持此功能。</string>
|
||||||
<string name="settings_podcast">显示播客</string>
|
<string name="settings_podcast">显示播客</string>
|
||||||
<string name="settings_podcast_summary">如果启用,则显示播客部分。</string>
|
<string name="settings_podcast_summary">启用后则显示播客部分。</string>
|
||||||
<string name="settings_audio_quality">显示音频质量</string>
|
|
||||||
<string name="settings_audio_quality_summary">显示曲目的比特率和音频格式。</string>
|
|
||||||
<string name="settings_item_rating">显示评分</string>
|
|
||||||
<string name="settings_item_rating_summary">如果启用,则显示项目的评分和收藏状态。</string>
|
|
||||||
<string name="settings_queue_syncing_countdown">同步定时器</string>
|
<string name="settings_queue_syncing_countdown">同步定时器</string>
|
||||||
<string name="settings_queue_syncing_summary">如果启用,将允许当前用户保存其播放队列,并能够在打开应用程序时加载保存状态。</string>
|
<string name="settings_queue_syncing_summary">启用后将允许当前用户保存其播放队列,并能够在打开应用程序时加载保存状态。</string>
|
||||||
<string name="settings_queue_syncing_title">同步当前用户的播放队列</string>
|
<string name="settings_queue_syncing_title">同步当前用户的播放队列</string>
|
||||||
<string name="settings_radio">显示广播</string>
|
<string name="settings_radio">显示电台</string>
|
||||||
<string name="settings_radio_summary">如果启用,则显示电台部分。</string>
|
<string name="settings_radio_summary">启用后,则显示电台部分。</string>
|
||||||
<string name="settings_replay_gain">设置播放增益模式</string>
|
<string name="settings_replay_gain">设置播放增益模式</string>
|
||||||
<string name="settings_rounded_corner">圆角</string>
|
<string name="settings_rounded_corner">圆角</string>
|
||||||
<string name="settings_rounded_corner_size">圆角大小</string>
|
<string name="settings_rounded_corner_size">圆角大小</string>
|
||||||
<string name="settings_rounded_corner_size_summary">设置圆角的大小。</string>
|
<string name="settings_rounded_corner_size_summary">设置圆角的大小。</string>
|
||||||
<string name="settings_rounded_corner_summary">如果启用,则为所有渲染的封面设置圆角。 更改将在应用重新启动后生效。</string>
|
<string name="settings_rounded_corner_summary">启用后则为所有渲染的封面设置圆角。更改将在应用重新启动后生效。</string>
|
||||||
|
<string name="settings_scan_result">正在扫描:已发现 %1$d 首歌曲</string>
|
||||||
<string name="settings_scan_title">扫描曲库</string>
|
<string name="settings_scan_title">扫描曲库</string>
|
||||||
<string name="settings_scrobble_title">启用音乐记录</string>
|
<string name="settings_scrobble_title">启用音乐记录</string>
|
||||||
|
<string name="settings_set_download_folder">设置下载文件夹</string>
|
||||||
<string name="settings_share_title">启用音乐共享</string>
|
<string name="settings_share_title">启用音乐共享</string>
|
||||||
|
<string name="settings_show_mini_shuffle_button">显示随机按钮</string>
|
||||||
|
<string name="settings_show_mini_shuffle_button_summary">启用后,在迷你播放器中显示随机播放按钮,并移除收藏按钮。</string>
|
||||||
|
<string name="settings_song_rating">显示歌曲评分</string>
|
||||||
|
<string name="settings_song_rating_summary">启用后歌曲详情页将显示五星评分。\n\n*需重启应用后生效</string>
|
||||||
<string name="settings_streaming_cache_size">播放缓存大小</string>
|
<string name="settings_streaming_cache_size">播放缓存大小</string>
|
||||||
|
<string name="settings_streaming_cache_storage_title">缓存目录设置</string>
|
||||||
<string name="settings_sub_summary_scrobble">请注意,音乐记录同时也依赖于服务器是否能够接收这些数据。</string>
|
<string name="settings_sub_summary_scrobble">请注意,音乐记录同时也依赖于服务器是否能够接收这些数据。</string>
|
||||||
<string name="settings_summary_skip_min_star_rating">收听电台,即时混合和随机播放时,低于特定评分的曲目将会被忽略。</string>
|
<string name="settings_summary_replay_gain">播放增益(Replay gain)允许您通过调整音轨的音量,以获得始终如一的聆听体验。 仅当歌曲标签包含必要的元数据时,此设置才有效。</string>
|
||||||
<string name="settings_summary_replay_gain">播放增益(Replay gain)允许您通过调整音轨的音量,以获得始终如一的聆听体验。 仅当曲目标签包含必要的元数据时,此设置才有效。</string>
|
|
||||||
<string name="settings_summary_scrobble">音乐记录(Scrobbling)允许您的设备将您收听的歌曲的相关信息发送到音乐服务器。 这些信息有助于基于您的音乐偏好生成个性化推荐。</string>
|
<string name="settings_summary_scrobble">音乐记录(Scrobbling)允许您的设备将您收听的歌曲的相关信息发送到音乐服务器。 这些信息有助于基于您的音乐偏好生成个性化推荐。</string>
|
||||||
<string name="settings_summary_share">允许用户通过链接共享音乐。 该功能需要服务器端支持并启用,并且仅限于单个曲目、专辑和队列。</string>
|
<string name="settings_summary_share">允许用户通过链接共享音乐。 该功能需要服务器端支持并启用,并且仅限于单首歌曲、专辑和队列。</string>
|
||||||
<string name="settings_summary_syncing">返回当前用户的播放队列状态。 这包括播放队列中的曲目、正在播放的曲目以及曲目播放进度。需要服务器支持此功能。</string>
|
<string name="settings_summary_skip_min_star_rating">收听电台,即时混合和随机播放时,低于特定评分的歌曲将会被忽略。</string>
|
||||||
<string name="settings_summary_streaming_cache_size">%1$s \n已使用: %2$s MiB</string>
|
<string name="settings_summary_streaming_cache_size">%1$s \n已使用: %2$s MiB</string>
|
||||||
<string name="settings_summary_transcoding">转码模式优先级设置。 如果设置为“播放原始”,文件的比特率将不会更改。</string>
|
<string name="settings_summary_syncing">返回当前用户的播放队列状态。 这包括播放队列中的歌曲、正在播放的歌曲以及歌曲播放进度。需要服务器支持此功能。</string>
|
||||||
<string name="settings_summary_transcoding_download">下载转码后的媒体。 如果启用,将不会下载原始数据,而是使用以下设置。\n如果“用于下载的转码格式”设置为“下载原始”,则文件的比特率不会更改。</string>
|
<string name="settings_summary_transcoding">转码模式优先级设置。 如果设置为“播放原始”,文件的码率将不会更改。</string>
|
||||||
<string name="settings_summary_transcoding_estimate_content_length">当文件即时转码时,客户端通常不会显示曲目长度。 可以向支持该功能的服务器发送请求,估计正在播放的曲目的持续时间,但可能响应变慢。</string>
|
<string name="settings_summary_transcoding_download">下载转码后的媒体。 启用后将不会下载原始数据,而是使用以下设置。\n如果“用于下载的转码格式”设置为“下载原始”,则文件的码率不会更改。</string>
|
||||||
<string name="settings_sync_starred_tracks_for_offline_use_summary">如果启用,将下载已收藏的曲目以供离线使用。</string>
|
<string name="settings_summary_transcoding_estimate_content_length">当文件即时转码时,客户端通常不会显示歌曲长度。 可以向支持该功能的服务器发送请求,估计正在播放的歌曲的持续时间,但可能响应变慢。</string>
|
||||||
<string name="settings_sync_starred_tracks_for_offline_use_title">同步已收藏的曲目以供离线使用</string>
|
<string name="settings_support_discussion_link">https://github.com/eddyizm/tempus/discussions</string>
|
||||||
|
<string name="settings_support_summary">加入社区讨论并获取帮助</string>
|
||||||
|
<string name="settings_support_title">用户支持</string>
|
||||||
|
<string name="settings_sync_starred_albums_for_offline_use_summary">启用后将下载收藏的专辑以供离线使用。</string>
|
||||||
|
<string name="settings_sync_starred_albums_for_offline_use_title">下载收藏的专辑以供离线使用</string>
|
||||||
|
<string name="settings_sync_starred_artists_for_offline_use_summary">启用后将自动下载收藏的艺术家以供离线使用。</string>
|
||||||
|
<string name="settings_sync_starred_artists_for_offline_use_title">同步收藏的艺术家以供离线使用</string>
|
||||||
|
<string name="settings_sync_starred_tracks_for_offline_use_summary">启用后将下载收藏的歌曲以供离线使用。</string>
|
||||||
|
<string name="settings_sync_starred_tracks_for_offline_use_title">同步收藏的歌曲以供离线使用</string>
|
||||||
|
<string name="settings_system_equalizer_summary">调整音频设置</string>
|
||||||
|
<string name="settings_system_equalizer_title">系统均衡器</string>
|
||||||
|
<string name="settings_system_language">系统语言</string>
|
||||||
<string name="settings_theme">主题</string>
|
<string name="settings_theme">主题</string>
|
||||||
<string name="settings_title_data">数据</string>
|
<string name="settings_title_data">数据</string>
|
||||||
<string name="settings_title_general">通用</string>
|
<string name="settings_title_general">通用</string>
|
||||||
|
<string name="settings_title_playlist">播放列表</string>
|
||||||
<string name="settings_title_rating">评分</string>
|
<string name="settings_title_rating">评分</string>
|
||||||
<string name="settings_title_replay_gain">播放增益</string>
|
<string name="settings_title_replay_gain">播放增益</string>
|
||||||
<string name="settings_title_scrobble">音乐记录</string>
|
<string name="settings_title_scrobble">音乐记录</string>
|
||||||
<string name="settings_title_skip_min_star_rating">根据评分忽略歌曲</string>
|
|
||||||
<string name="settings_title_share">分享</string>
|
<string name="settings_title_share">分享</string>
|
||||||
|
<string name="settings_title_skip_min_star_rating">根据评分忽略歌曲</string>
|
||||||
|
<string name="settings_title_skip_min_star_rating_dialog">根据歌曲评分筛选:</string>
|
||||||
<string name="settings_title_syncing">同步</string>
|
<string name="settings_title_syncing">同步</string>
|
||||||
<string name="settings_title_transcoding">转码</string>
|
<string name="settings_title_transcoding">转码</string>
|
||||||
<string name="settings_title_transcoding_download">转码下载</string>
|
<string name="settings_title_transcoding_download">转码下载</string>
|
||||||
|
|
@ -321,6 +461,7 @@
|
||||||
<string name="share_bottom_sheet_copy_link">复制链接</string>
|
<string name="share_bottom_sheet_copy_link">复制链接</string>
|
||||||
<string name="share_bottom_sheet_delete">删除分享</string>
|
<string name="share_bottom_sheet_delete">删除分享</string>
|
||||||
<string name="share_bottom_sheet_update">更新分享</string>
|
<string name="share_bottom_sheet_update">更新分享</string>
|
||||||
|
<string name="share_no_expiration">永不过期</string>
|
||||||
<string name="share_subtitle_item">到期日期:%1$s</string>
|
<string name="share_subtitle_item">到期日期:%1$s</string>
|
||||||
<string name="share_unsupported_error">不支持分享或未启用</string>
|
<string name="share_unsupported_error">不支持分享或未启用</string>
|
||||||
<string name="share_update_dialog_hint_description">描述</string>
|
<string name="share_update_dialog_hint_description">描述</string>
|
||||||
|
|
@ -341,63 +482,69 @@
|
||||||
<string name="song_bottom_sheet_remove">移除</string>
|
<string name="song_bottom_sheet_remove">移除</string>
|
||||||
<string name="song_bottom_sheet_share">分享</string>
|
<string name="song_bottom_sheet_share">分享</string>
|
||||||
<string name="song_list_page_downloaded">已下载</string>
|
<string name="song_list_page_downloaded">已下载</string>
|
||||||
<string name="song_list_page_most_played">最常播放的曲目</string>
|
<string name="song_list_page_most_played">最常播放的歌曲</string>
|
||||||
<string name="song_list_page_recently_added">最近添加的曲目</string>
|
<string name="song_list_page_recently_added">最近添加的歌曲</string>
|
||||||
<string name="song_list_page_recently_played">最近播放的曲目</string>
|
<string name="song_list_page_recently_played">最近播放的歌曲</string>
|
||||||
<string name="song_list_page_starred">已收藏的曲目</string>
|
<string name="song_list_page_starred">已收藏的歌曲</string>
|
||||||
<string name="song_list_page_top">%1$s 的热门曲目</string>
|
<string name="song_list_page_top">%1$s 的热门歌曲</string>
|
||||||
<string name="song_list_page_year">年份 %1$d</string>
|
<string name="song_list_page_year">年份 %1$d</string>
|
||||||
<string name="song_subtitle_formatter">%1$s • %2$s %3$s</string>
|
<string name="song_subtitle_formatter">%1$s • %2$s %3$s</string>
|
||||||
|
<plurals name="songs_download_started">
|
||||||
|
<item quantity="one">正在下载 %d 首歌曲</item>
|
||||||
|
<item quantity="other">正在下载 %d 首歌曲</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="starred_album_sync_dialog_summary">下载收藏的专辑可能会消耗大量移动数据流量。</string>
|
||||||
|
<string name="starred_album_sync_dialog_title">同步收藏的专辑</string>
|
||||||
|
<string name="starred_artist_sync_dialog_summary">下载收藏的艺术家可能会消耗大量移动数据流量。</string>
|
||||||
|
<string name="starred_artist_sync_dialog_title">同步收藏的艺术家</string>
|
||||||
<string name="starred_sync_dialog_negative_button">取消</string>
|
<string name="starred_sync_dialog_negative_button">取消</string>
|
||||||
<string name="starred_sync_dialog_neutral_button">继续</string>
|
<string name="starred_sync_dialog_neutral_button">继续</string>
|
||||||
<string name="starred_sync_dialog_positive_button">继续并下载</string>
|
<string name="starred_sync_dialog_positive_button">继续并下载</string>
|
||||||
<string name="starred_sync_dialog_summary">下载收藏曲目可能需要大量数据。</string>
|
<string name="starred_sync_dialog_summary">下载收藏的歌曲可能会消耗大量移动数据流量。</string>
|
||||||
<string name="starred_sync_dialog_title">同步已收藏的曲目</string>
|
<string name="starred_sync_dialog_title">同步收藏的歌曲</string>
|
||||||
|
<string name="streaming_cache_storage_dialog_sub_summary">要使更改生效,请重新启动应用程序。</string>
|
||||||
|
<string name="streaming_cache_storage_dialog_summary">切换缓存文件的存储路径可能会导致原位置存储的缓存文件被清空。</string>
|
||||||
|
<string name="streaming_cache_storage_dialog_title">选择存储位置</string>
|
||||||
|
<string name="streaming_cache_storage_external_dialog_positive_button">外部</string>
|
||||||
|
<string name="streaming_cache_storage_internal_dialog_negative_button">内部</string>
|
||||||
|
<string name="support_url">https://ko-fi.com/eddyizm</string>
|
||||||
<string name="track_info_album">专辑</string>
|
<string name="track_info_album">专辑</string>
|
||||||
<string name="track_info_artist">艺术家</string>
|
<string name="track_info_artist">艺术家</string>
|
||||||
<string name="track_info_bitrate">比特率</string>
|
<string name="track_info_bit_depth">位深</string>
|
||||||
|
<string name="track_info_bitrate">码率</string>
|
||||||
<string name="track_info_content_type">内容类型</string>
|
<string name="track_info_content_type">内容类型</string>
|
||||||
<string name="track_info_dialog_positive_button">确定</string>
|
<string name="track_info_dialog_positive_button">确定</string>
|
||||||
<string name="track_info_dialog_title">曲目信息</string>
|
<string name="track_info_dialog_title">歌曲信息</string>
|
||||||
<string name="track_info_disc_number">碟片编号</string>
|
<string name="track_info_disc_number">碟片编号</string>
|
||||||
<string name="track_info_duration">持续时间</string>
|
<string name="track_info_duration">持续时间</string>
|
||||||
<string name="track_info_genre">流派</string>
|
<string name="track_info_genre">流派</string>
|
||||||
<string name="track_info_path">路径</string>
|
<string name="track_info_path">路径</string>
|
||||||
|
<string name="track_info_sampling_rate">采样率</string>
|
||||||
<string name="track_info_size">大小</string>
|
<string name="track_info_size">大小</string>
|
||||||
<string name="track_info_suffix">后缀</string>
|
<string name="track_info_suffix">后缀</string>
|
||||||
<string name="track_info_summary_downloaded_file">该文件已使用 Subsonic API 下载。 文件的编码和比特率与源文件一致。</string>
|
<string name="track_info_summary_downloaded_file">该文件已使用 Subsonic API 下载。 文件的编码和码率与源文件一致。</string>
|
||||||
<string name="track_info_summary_full_transcode">本应用将请求服务器对文件进行转码并修改其比特率。 用户请求的编解码器是%1$s,比特率为%2$s。 对所选格式的文件的编码和比特率的任何潜在更改都将由服务器处理,服务器可能支持也可能不支持该操作。</string>
|
<string name="track_info_summary_full_transcode">本应用将请求服务器对文件进行转码并修改其码率。 用户请求的编解码器是%1$s,码率为%2$s。 对所选格式的文件的编码和码率的任何潜在更改都将由服务器处理,服务器可能支持也可能不支持该操作。</string>
|
||||||
<string name="track_info_summary_original_file">本应用只会读取服务器提供的原始文件。 本应用将明确向服务器请求具有原始源比特率的未转码文件。</string>
|
<string name="track_info_summary_original_file">本应用只会读取服务器提供的原始文件。 本应用将明确向服务器请求具有原始源码率的未转码文件。</string>
|
||||||
<string name="track_info_summary_server_prioritized">要播放的文件质量取决于服务器设置。 本应用不会强制选择任何用于潜在转码的编码和比特率。</string>
|
<string name="track_info_summary_server_prioritized">要播放的文件质量取决于服务器设置。 本应用不会强制选择任何用于潜在转码的编码和码率。</string>
|
||||||
<string name="track_info_summary_transcoding_bitrate">本应用将请求服务器修改文件的比特率。 用户请求的比特率为%1$s,而源文件的编码将保持不变。 对所选格式的文件比特率的任何更改都将由服务器完成,服务器可能支持也可能不支持该操作。</string>
|
<string name="track_info_summary_transcoding_bitrate">本应用将请求服务器修改文件的码率。 用户请求的码率为%1$s,而源文件的编码将保持不变。 对所选格式的文件码率的任何更改都将由服务器完成,服务器可能支持也可能不支持该操作。</string>
|
||||||
<string name="track_info_summary_transcoding_codec">本应用将请求服务器对文件进行转码。 用户请求的编解码器是%1$s,而比特率将与源文件相同。 将文件转码为所选格式的可能性取决于服务器,因为它可能支持也可能不支持该操作。</string>
|
<string name="track_info_summary_transcoding_codec">本应用将请求服务器对文件进行转码。 用户请求的编解码器是%1$s,而码率将与源文件相同。 将文件转码为所选格式的可能性取决于服务器,因为它可能支持也可能不支持该操作。</string>
|
||||||
<string name="track_info_title">标题</string>
|
<string name="track_info_title">标题</string>
|
||||||
<string name="track_info_track_number">曲目编号</string>
|
<string name="track_info_track_number">歌曲编号</string>
|
||||||
<string name="track_info_transcoded_content_type">转码内容类型</string>
|
<string name="track_info_transcoded_content_type">转码内容类型</string>
|
||||||
<string name="track_info_transcoded_suffix">转码后缀</string>
|
<string name="track_info_transcoded_suffix">转码后缀</string>
|
||||||
<string name="track_info_year">年份</string>
|
<string name="track_info_year">年份</string>
|
||||||
<string name="streaming_cache_storage_external_dialog_positive_button">外部</string>
|
|
||||||
<string name="streaming_cache_storage_internal_dialog_negative_button">内部</string>
|
|
||||||
<string name="undraw_page">unDraw</string>
|
<string name="undraw_page">unDraw</string>
|
||||||
<string name="undraw_thanks">特别感谢 unDraw,没有它提供的插图,我们的应用不可能会如此精美。</string>
|
<string name="undraw_thanks">特别感谢 unDraw,没有它提供的插图,我们的应用不可能会如此精美。</string>
|
||||||
<string name="undraw_url">https://undraw.co/</string>
|
<string name="undraw_url">https://undraw.co/</string>
|
||||||
<string name="album_page_release_date_label">发布于 %1$s</string>
|
<string name="widget_content_desc_album_art">专辑封面</string>
|
||||||
<string name="disc_titlefull">第 %1$s 张光盘 - %2$s</string>
|
<string name="widget_content_desc_next">下一首</string>
|
||||||
<string name="disc_titleless">第 %1$s 张光盘</string>
|
<string name="widget_content_desc_play_pause">播放或暂停</string>
|
||||||
<string name="download_shuffle_all_subtitle">随机播放</string>
|
<string name="widget_content_desc_prev">上一首</string>
|
||||||
<string name="github_update_dialog_negative_button">稍后提醒</string>
|
<string name="widget_content_desc_repeat">更改循环模式</string>
|
||||||
<string name="github_update_dialog_positive_button">现在下载</string>
|
<string name="widget_content_desc_shuffle">切换随机播放</string>
|
||||||
<string name="github_update_dialog_title">有可用更新</string>
|
<string name="widget_label">Tempus 小组件</string>
|
||||||
<string name="home_rearrangement_dialog_negative_button">取消</string>
|
<string name="widget_not_playing">未播放</string>
|
||||||
<string name="home_rearrangement_dialog_neutral_button">重置</string>
|
<string name="widget_placeholder_subtitle">打开 Tempus</string>
|
||||||
<string name="home_rearrangement_dialog_positive_button">保存</string>
|
<string name="widget_time_duration_placeholder">0:00</string>
|
||||||
<string name="home_title_last_month">上个月</string>
|
<string name="widget_time_elapsed_placeholder">0:00</string>
|
||||||
<string name="home_title_last_year">去年</string>
|
|
||||||
<string name="menu_last_week_name">上周</string>
|
|
||||||
<string name="menu_last_month_name">上个月</string>
|
|
||||||
<string name="menu_pin_button">添加到主屏幕</string>
|
|
||||||
<string name="menu_unpin_button">从主屏幕移除</string>
|
|
||||||
<string name="playlist_editor_dialog_action_delete_toast">长按删除</string>
|
|
||||||
<string name="server_signup_dialog_action_delete_toast">长按删除</string>
|
|
||||||
<string name="server_signup_dialog_hint_local_address">本地 URL</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,8 @@
|
||||||
<string name="battery_optimization_negative_button">Ignore</string>
|
<string name="battery_optimization_negative_button">Ignore</string>
|
||||||
<string name="battery_optimization_neutral_button">Don\'t ask again</string>
|
<string name="battery_optimization_neutral_button">Don\'t ask again</string>
|
||||||
<string name="battery_optimization_positive_button">Disable</string>
|
<string name="battery_optimization_positive_button">Disable</string>
|
||||||
|
<string name="bottom_sheet_generating_instant_mix">Generating instant mix...</string>
|
||||||
|
<string name="bottom_sheet_problem_generating_instant_mix">Could not retrieve tracks from subsonic server.</string>
|
||||||
<string name="connection_alert_dialog_negative_button">Cancel</string>
|
<string name="connection_alert_dialog_negative_button">Cancel</string>
|
||||||
<string name="connection_alert_dialog_neutral_button">Enable data saver</string>
|
<string name="connection_alert_dialog_neutral_button">Enable data saver</string>
|
||||||
<string name="connection_alert_dialog_positive_button">OK</string>
|
<string name="connection_alert_dialog_positive_button">OK</string>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue