music: refactor name implementation
Refactor the music name implementation to do the following: 1. Unify normal and sort names under a single datatype 2. Handle arbitrary-length digit strings 3. Ignore puncutation regardless of the intelligent sort configuration, as it is trivially localizable. Resolves #423. Co-authored by: ChatGPT-3.5
This commit is contained in:
parent
ca349dea18
commit
c7b875376c
51 changed files with 384 additions and 255 deletions
|
@ -194,7 +194,7 @@ class AlbumDetailFragment :
|
||||||
findNavController().navigateUp()
|
findNavController().navigateUp()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
requireBinding().detailToolbar.title = album.resolveName(requireContext())
|
requireBinding().detailToolbar.title = album.name.resolve(requireContext())
|
||||||
albumHeaderAdapter.setParent(album)
|
albumHeaderAdapter.setParent(album)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -204,7 +204,7 @@ class ArtistDetailFragment :
|
||||||
findNavController().navigateUp()
|
findNavController().navigateUp()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
requireBinding().detailToolbar.title = artist.resolveName(requireContext())
|
requireBinding().detailToolbar.title = artist.name.resolve(requireContext())
|
||||||
artistHeaderAdapter.setParent(artist)
|
artistHeaderAdapter.setParent(artist)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,9 +36,9 @@ import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.list.Sort
|
import org.oxycblt.auxio.list.Sort
|
||||||
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.*
|
||||||
import org.oxycblt.auxio.music.metadata.AudioInfo
|
import org.oxycblt.auxio.music.info.Disc
|
||||||
import org.oxycblt.auxio.music.metadata.Disc
|
import org.oxycblt.auxio.music.info.ReleaseType
|
||||||
import org.oxycblt.auxio.music.metadata.ReleaseType
|
import org.oxycblt.auxio.music.metadata.AudioProperties
|
||||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||||
import org.oxycblt.auxio.util.*
|
import org.oxycblt.auxio.util.*
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ class DetailViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val musicRepository: MusicRepository,
|
private val musicRepository: MusicRepository,
|
||||||
private val audioInfoFactory: AudioInfo.Factory,
|
private val audioPropertiesFactory: AudioProperties.Factory,
|
||||||
private val musicSettings: MusicSettings,
|
private val musicSettings: MusicSettings,
|
||||||
private val playbackSettings: PlaybackSettings
|
private val playbackSettings: PlaybackSettings
|
||||||
) : ViewModel(), MusicRepository.UpdateListener {
|
) : ViewModel(), MusicRepository.UpdateListener {
|
||||||
|
@ -66,9 +66,9 @@ constructor(
|
||||||
val currentSong: StateFlow<Song?>
|
val currentSong: StateFlow<Song?>
|
||||||
get() = _currentSong
|
get() = _currentSong
|
||||||
|
|
||||||
private val _songAudioInfo = MutableStateFlow<AudioInfo?>(null)
|
private val _songAudioProperties = MutableStateFlow<AudioProperties?>(null)
|
||||||
/** The [AudioInfo] of the currently shown [Song]. Null if not loaded yet. */
|
/** The [AudioProperties] of the currently shown [Song]. Null if not loaded yet. */
|
||||||
val songAudioInfo: StateFlow<AudioInfo?> = _songAudioInfo
|
val songAudioProperties: StateFlow<AudioProperties?> = _songAudioProperties
|
||||||
|
|
||||||
// --- ALBUM ---
|
// --- ALBUM ---
|
||||||
|
|
||||||
|
@ -225,7 +225,7 @@ constructor(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a new [currentSong] from it's [Music.UID]. If the [Music.UID] differs, [currentSong] and
|
* Set a new [currentSong] from it's [Music.UID]. If the [Music.UID] differs, [currentSong] and
|
||||||
* [songAudioInfo] will be updated to align with the new [Song].
|
* [songAudioProperties] will be updated to align with the new [Song].
|
||||||
*
|
*
|
||||||
* @param uid The UID of the [Song] to load. Must be valid.
|
* @param uid The UID of the [Song] to load. Must be valid.
|
||||||
*/
|
*/
|
||||||
|
@ -305,12 +305,12 @@ constructor(
|
||||||
private fun refreshAudioInfo(song: Song) {
|
private fun refreshAudioInfo(song: Song) {
|
||||||
// Clear any previous job in order to avoid stale data from appearing in the UI.
|
// Clear any previous job in order to avoid stale data from appearing in the UI.
|
||||||
currentSongJob?.cancel()
|
currentSongJob?.cancel()
|
||||||
_songAudioInfo.value = null
|
_songAudioProperties.value = null
|
||||||
currentSongJob =
|
currentSongJob =
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val info = audioInfoFactory.extract(song)
|
val info = audioPropertiesFactory.extract(song)
|
||||||
yield()
|
yield()
|
||||||
_songAudioInfo.value = info
|
_songAudioProperties.value = info
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -196,7 +196,7 @@ class GenreDetailFragment :
|
||||||
findNavController().navigateUp()
|
findNavController().navigateUp()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
requireBinding().detailToolbar.title = genre.resolveName(requireContext())
|
requireBinding().detailToolbar.title = genre.name.resolve(requireContext())
|
||||||
genreHeaderAdapter.setParent(genre)
|
genreHeaderAdapter.setParent(genre)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -187,7 +187,7 @@ class PlaylistDetailFragment :
|
||||||
findNavController().navigateUp()
|
findNavController().navigateUp()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
requireBinding().detailToolbar.title = playlist.resolveName(requireContext())
|
requireBinding().detailToolbar.title = playlist.name.resolve(requireContext())
|
||||||
playlistHeaderAdapter.setParent(playlist)
|
playlistHeaderAdapter.setParent(playlist)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,8 @@ import org.oxycblt.auxio.detail.list.SongPropertyAdapter
|
||||||
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.metadata.AudioInfo
|
import org.oxycblt.auxio.music.info.Name
|
||||||
|
import org.oxycblt.auxio.music.metadata.AudioProperties
|
||||||
import org.oxycblt.auxio.music.resolveNames
|
import org.oxycblt.auxio.music.resolveNames
|
||||||
import org.oxycblt.auxio.playback.formatDurationMs
|
import org.oxycblt.auxio.playback.formatDurationMs
|
||||||
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||||
|
@ -67,10 +68,10 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
|
||||||
binding.detailProperties.adapter = detailAdapter
|
binding.detailProperties.adapter = detailAdapter
|
||||||
// DetailViewModel handles most initialization from the navigation argument.
|
// DetailViewModel handles most initialization from the navigation argument.
|
||||||
detailModel.setSongUid(args.songUid)
|
detailModel.setSongUid(args.songUid)
|
||||||
collectImmediately(detailModel.currentSong, detailModel.songAudioInfo, ::updateSong)
|
collectImmediately(detailModel.currentSong, detailModel.songAudioProperties, ::updateSong)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSong(song: Song?, info: AudioInfo?) {
|
private fun updateSong(song: Song?, info: AudioProperties?) {
|
||||||
if (song == null) {
|
if (song == null) {
|
||||||
// Song we were showing no longer exists.
|
// Song we were showing no longer exists.
|
||||||
findNavController().navigateUp()
|
findNavController().navigateUp()
|
||||||
|
@ -123,12 +124,14 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T : Music> T.zipName(context: Context) =
|
private fun <T : Music> T.zipName(context: Context): String {
|
||||||
if (rawSortName != null) {
|
val name = name
|
||||||
getString(R.string.fmt_zipped_names, resolveName(context), rawSortName)
|
return if (name is Name.Known && name.sort != null) {
|
||||||
|
getString(R.string.fmt_zipped_names, name.resolve(context), name.sort)
|
||||||
} else {
|
} else {
|
||||||
resolveName(context)
|
name.resolve(context)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun <T : Music> List<T>.zipNames(context: Context) =
|
private fun <T : Music> List<T>.zipNames(context: Context) =
|
||||||
concatLocalized(context) { it.zipName(context) }
|
concatLocalized(context) { it.zipName(context) }
|
||||||
|
|
|
@ -77,7 +77,7 @@ private constructor(private val binding: ItemDetailHeaderBinding) :
|
||||||
// The type text depends on the release type (Album, EP, Single, etc.)
|
// The type text depends on the release type (Album, EP, Single, etc.)
|
||||||
binding.detailType.text = binding.context.getString(album.releaseType.stringRes)
|
binding.detailType.text = binding.context.getString(album.releaseType.stringRes)
|
||||||
|
|
||||||
binding.detailName.text = album.resolveName(binding.context)
|
binding.detailName.text = album.name.resolve(binding.context)
|
||||||
|
|
||||||
// Artist name maps to the subhead text
|
// Artist name maps to the subhead text
|
||||||
binding.detailSubhead.apply {
|
binding.detailSubhead.apply {
|
||||||
|
|
|
@ -63,7 +63,7 @@ private constructor(private val binding: ItemDetailHeaderBinding) :
|
||||||
fun bind(artist: Artist, listener: DetailHeaderAdapter.Listener) {
|
fun bind(artist: Artist, listener: DetailHeaderAdapter.Listener) {
|
||||||
binding.detailCover.bind(artist)
|
binding.detailCover.bind(artist)
|
||||||
binding.detailType.text = binding.context.getString(R.string.lbl_artist)
|
binding.detailType.text = binding.context.getString(R.string.lbl_artist)
|
||||||
binding.detailName.text = artist.resolveName(binding.context)
|
binding.detailName.text = artist.name.resolve(binding.context)
|
||||||
|
|
||||||
if (artist.songs.isNotEmpty()) {
|
if (artist.songs.isNotEmpty()) {
|
||||||
// Information about the artist's genre(s) map to the sub-head text
|
// Information about the artist's genre(s) map to the sub-head text
|
||||||
|
|
|
@ -62,7 +62,7 @@ private constructor(private val binding: ItemDetailHeaderBinding) :
|
||||||
fun bind(genre: Genre, listener: DetailHeaderAdapter.Listener) {
|
fun bind(genre: Genre, listener: DetailHeaderAdapter.Listener) {
|
||||||
binding.detailCover.bind(genre)
|
binding.detailCover.bind(genre)
|
||||||
binding.detailType.text = binding.context.getString(R.string.lbl_genre)
|
binding.detailType.text = binding.context.getString(R.string.lbl_genre)
|
||||||
binding.detailName.text = genre.resolveName(binding.context)
|
binding.detailName.text = genre.name.resolve(binding.context)
|
||||||
// Nothing about a genre is applicable to the sub-head text.
|
// Nothing about a genre is applicable to the sub-head text.
|
||||||
binding.detailSubhead.isVisible = false
|
binding.detailSubhead.isVisible = false
|
||||||
// The song and artist count of the genre maps to the info text.
|
// The song and artist count of the genre maps to the info text.
|
||||||
|
|
|
@ -62,7 +62,7 @@ private constructor(private val binding: ItemDetailHeaderBinding) :
|
||||||
fun bind(playlist: Playlist, listener: DetailHeaderAdapter.Listener) {
|
fun bind(playlist: Playlist, listener: DetailHeaderAdapter.Listener) {
|
||||||
binding.detailCover.bind(playlist)
|
binding.detailCover.bind(playlist)
|
||||||
binding.detailType.text = binding.context.getString(R.string.lbl_playlist)
|
binding.detailType.text = binding.context.getString(R.string.lbl_playlist)
|
||||||
binding.detailName.text = playlist.resolveName(binding.context)
|
binding.detailName.text = playlist.name.resolve(binding.context)
|
||||||
// Nothing about a playlist is applicable to the sub-head text.
|
// Nothing about a playlist is applicable to the sub-head text.
|
||||||
binding.detailSubhead.isVisible = false
|
binding.detailSubhead.isVisible = false
|
||||||
// The song count of the playlist maps to the info text.
|
// The song count of the playlist maps to the info text.
|
||||||
|
|
|
@ -33,7 +33,7 @@ import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
|
||||||
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
|
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.metadata.Disc
|
import org.oxycblt.auxio.music.info.Disc
|
||||||
import org.oxycblt.auxio.playback.formatDurationMs
|
import org.oxycblt.auxio.playback.formatDurationMs
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
import org.oxycblt.auxio.util.inflater
|
import org.oxycblt.auxio.util.inflater
|
||||||
|
@ -171,7 +171,7 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.songName.text = song.resolveName(binding.context)
|
binding.songName.text = song.name.resolve(binding.context)
|
||||||
|
|
||||||
// Use duration instead of album or artist for each song, as this text would
|
// Use duration instead of album or artist for each song, as this text would
|
||||||
// be homogenous otherwise.
|
// be homogenous otherwise.
|
||||||
|
@ -204,7 +204,7 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
|
||||||
val DIFF_CALLBACK =
|
val DIFF_CALLBACK =
|
||||||
object : SimpleDiffCallback<Song>() {
|
object : SimpleDiffCallback<Song>() {
|
||||||
override fun areContentsTheSame(oldItem: Song, newItem: Song) =
|
override fun areContentsTheSame(oldItem: Song, newItem: Song) =
|
||||||
oldItem.rawName == newItem.rawName && oldItem.durationMs == newItem.durationMs
|
oldItem.name == newItem.name && oldItem.durationMs == newItem.durationMs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,7 +106,7 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite
|
||||||
fun bind(album: Album, listener: SelectableListListener<Album>) {
|
fun bind(album: Album, listener: SelectableListListener<Album>) {
|
||||||
listener.bind(album, this, menuButton = binding.parentMenu)
|
listener.bind(album, this, menuButton = binding.parentMenu)
|
||||||
binding.parentImage.bind(album)
|
binding.parentImage.bind(album)
|
||||||
binding.parentName.text = album.resolveName(binding.context)
|
binding.parentName.text = album.name.resolve(binding.context)
|
||||||
binding.parentInfo.text =
|
binding.parentInfo.text =
|
||||||
// Fall back to a friendlier "No date" text if the album doesn't have date information
|
// Fall back to a friendlier "No date" text if the album doesn't have date information
|
||||||
album.dates?.resolveDate(binding.context)
|
album.dates?.resolveDate(binding.context)
|
||||||
|
@ -139,7 +139,7 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite
|
||||||
val DIFF_CALLBACK =
|
val DIFF_CALLBACK =
|
||||||
object : SimpleDiffCallback<Album>() {
|
object : SimpleDiffCallback<Album>() {
|
||||||
override fun areContentsTheSame(oldItem: Album, newItem: Album) =
|
override fun areContentsTheSame(oldItem: Album, newItem: Album) =
|
||||||
oldItem.rawName == newItem.rawName && oldItem.dates == newItem.dates
|
oldItem.name == newItem.name && oldItem.dates == newItem.dates
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -161,8 +161,8 @@ private class ArtistSongViewHolder private constructor(private val binding: Item
|
||||||
fun bind(song: Song, listener: SelectableListListener<Song>) {
|
fun bind(song: Song, listener: SelectableListListener<Song>) {
|
||||||
listener.bind(song, this, menuButton = binding.songMenu)
|
listener.bind(song, this, menuButton = binding.songMenu)
|
||||||
binding.songAlbumCover.bind(song)
|
binding.songAlbumCover.bind(song)
|
||||||
binding.songName.text = song.resolveName(binding.context)
|
binding.songName.text = song.name.resolve(binding.context)
|
||||||
binding.songInfo.text = song.album.resolveName(binding.context)
|
binding.songInfo.text = song.album.name.resolve(binding.context)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||||
|
@ -191,8 +191,7 @@ private class ArtistSongViewHolder private constructor(private val binding: Item
|
||||||
val DIFF_CALLBACK =
|
val DIFF_CALLBACK =
|
||||||
object : SimpleDiffCallback<Song>() {
|
object : SimpleDiffCallback<Song>() {
|
||||||
override fun areContentsTheSame(oldItem: Song, newItem: Song) =
|
override fun areContentsTheSame(oldItem: Song, newItem: Song) =
|
||||||
oldItem.rawName == newItem.rawName &&
|
oldItem.name == newItem.name && oldItem.album.name == newItem.album.name
|
||||||
oldItem.album.rawName == newItem.album.rawName
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,10 +94,10 @@ class AlbumListFragment :
|
||||||
// Change how we display the popup depending on the current sort mode.
|
// Change how we display the popup depending on the current sort mode.
|
||||||
return when (homeModel.getSortForTab(MusicMode.ALBUMS).mode) {
|
return when (homeModel.getSortForTab(MusicMode.ALBUMS).mode) {
|
||||||
// By Name -> Use Name
|
// By Name -> Use Name
|
||||||
is Sort.Mode.ByName -> album.sortName?.thumbString
|
is Sort.Mode.ByName -> album.name.thumb
|
||||||
|
|
||||||
// By Artist -> Use name of first artist
|
// By Artist -> Use name of first artist
|
||||||
is Sort.Mode.ByArtist -> album.artists[0].sortName?.thumbString
|
is Sort.Mode.ByArtist -> album.artists[0].name.thumb
|
||||||
|
|
||||||
// Date -> Use minimum date (Maximum dates are not sorted by, so showing them is odd)
|
// Date -> Use minimum date (Maximum dates are not sorted by, so showing them is odd)
|
||||||
is Sort.Mode.ByDate -> album.dates?.run { min.resolveDate(requireContext()) }
|
is Sort.Mode.ByDate -> album.dates?.run { min.resolveDate(requireContext()) }
|
||||||
|
|
|
@ -93,7 +93,7 @@ class ArtistListFragment :
|
||||||
// Change how we display the popup depending on the current sort mode.
|
// Change how we display the popup depending on the current sort mode.
|
||||||
return when (homeModel.getSortForTab(MusicMode.ARTISTS).mode) {
|
return when (homeModel.getSortForTab(MusicMode.ARTISTS).mode) {
|
||||||
// By Name -> Use Name
|
// By Name -> Use Name
|
||||||
is Sort.Mode.ByName -> artist.sortName?.thumbString
|
is Sort.Mode.ByName -> artist.name.thumb
|
||||||
|
|
||||||
// Duration -> Use formatted duration
|
// Duration -> Use formatted duration
|
||||||
is Sort.Mode.ByDuration -> artist.durationMs?.formatDurationMs(false)
|
is Sort.Mode.ByDuration -> artist.durationMs?.formatDurationMs(false)
|
||||||
|
|
|
@ -92,7 +92,7 @@ class GenreListFragment :
|
||||||
// Change how we display the popup depending on the current sort mode.
|
// Change how we display the popup depending on the current sort mode.
|
||||||
return when (homeModel.getSortForTab(MusicMode.GENRES).mode) {
|
return when (homeModel.getSortForTab(MusicMode.GENRES).mode) {
|
||||||
// By Name -> Use Name
|
// By Name -> Use Name
|
||||||
is Sort.Mode.ByName -> genre.sortName?.thumbString
|
is Sort.Mode.ByName -> genre.name.thumb
|
||||||
|
|
||||||
// Duration -> Use formatted duration
|
// Duration -> Use formatted duration
|
||||||
is Sort.Mode.ByDuration -> genre.durationMs.formatDurationMs(false)
|
is Sort.Mode.ByDuration -> genre.durationMs.formatDurationMs(false)
|
||||||
|
|
|
@ -85,7 +85,7 @@ class PlaylistListFragment :
|
||||||
// Change how we display the popup depending on the current sort mode.
|
// Change how we display the popup depending on the current sort mode.
|
||||||
return when (homeModel.getSortForTab(MusicMode.GENRES).mode) {
|
return when (homeModel.getSortForTab(MusicMode.GENRES).mode) {
|
||||||
// By Name -> Use Name
|
// By Name -> Use Name
|
||||||
is Sort.Mode.ByName -> playlist.sortName?.thumbString
|
is Sort.Mode.ByName -> playlist.name.thumb
|
||||||
|
|
||||||
// Duration -> Use formatted duration
|
// Duration -> Use formatted duration
|
||||||
is Sort.Mode.ByDuration -> playlist.durationMs.formatDurationMs(false)
|
is Sort.Mode.ByDuration -> playlist.durationMs.formatDurationMs(false)
|
||||||
|
|
|
@ -100,13 +100,13 @@ class SongListFragment :
|
||||||
// based off the names of the parent objects and not the child objects.
|
// based off the names of the parent objects and not the child objects.
|
||||||
return when (homeModel.getSortForTab(MusicMode.SONGS).mode) {
|
return when (homeModel.getSortForTab(MusicMode.SONGS).mode) {
|
||||||
// Name -> Use name
|
// Name -> Use name
|
||||||
is Sort.Mode.ByName -> song.sortName?.thumbString
|
is Sort.Mode.ByName -> song.name.thumb
|
||||||
|
|
||||||
// Artist -> Use name of first artist
|
// Artist -> Use name of first artist
|
||||||
is Sort.Mode.ByArtist -> song.album.artists[0].sortName?.thumbString
|
is Sort.Mode.ByArtist -> song.album.artists[0].name.thumb
|
||||||
|
|
||||||
// Album -> Use Album Name
|
// Album -> Use Album Name
|
||||||
is Sort.Mode.ByAlbum -> song.album.sortName?.thumbString
|
is Sort.Mode.ByAlbum -> song.album.name.thumb
|
||||||
|
|
||||||
// Year -> Use Full Year
|
// Year -> Use Full Year
|
||||||
is Sort.Mode.ByDate -> song.album.dates?.resolveDate(requireContext())
|
is Sort.Mode.ByDate -> song.album.dates?.resolveDate(requireContext())
|
||||||
|
|
|
@ -147,7 +147,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
CoilUtils.dispose(this)
|
CoilUtils.dispose(this)
|
||||||
imageLoader.enqueue(request)
|
imageLoader.enqueue(request)
|
||||||
// Update the content description to the specified resource.
|
// Update the content description to the specified resource.
|
||||||
contentDescription = context.getString(descRes, music.resolveName(context))
|
contentDescription = context.getString(descRes, music.name.resolve(context))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -46,7 +46,6 @@ constructor(
|
||||||
private val imageSettings: ImageSettings,
|
private val imageSettings: ImageSettings,
|
||||||
private val mediaSourceFactory: MediaSource.Factory
|
private val mediaSourceFactory: MediaSource.Factory
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun extract(album: Album): InputStream? =
|
suspend fun extract(album: Album): InputStream? =
|
||||||
try {
|
try {
|
||||||
when (imageSettings.coverMode) {
|
when (imageSettings.coverMode) {
|
||||||
|
|
|
@ -81,7 +81,7 @@ abstract class ListFragment<in T : Music, VB : ViewBinding> :
|
||||||
* @param song The [Song] to create the menu for.
|
* @param song The [Song] to create the menu for.
|
||||||
*/
|
*/
|
||||||
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, song: Song) {
|
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, song: Song) {
|
||||||
logD("Launching new song menu: ${song.rawName}")
|
logD("Launching new song menu: ${song.name}")
|
||||||
|
|
||||||
openMusicMenuImpl(anchor, menuRes) {
|
openMusicMenuImpl(anchor, menuRes) {
|
||||||
when (it.itemId) {
|
when (it.itemId) {
|
||||||
|
@ -120,7 +120,7 @@ abstract class ListFragment<in T : Music, VB : ViewBinding> :
|
||||||
* @param album The [Album] to create the menu for.
|
* @param album The [Album] to create the menu for.
|
||||||
*/
|
*/
|
||||||
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, album: Album) {
|
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, album: Album) {
|
||||||
logD("Launching new album menu: ${album.rawName}")
|
logD("Launching new album menu: ${album.name}")
|
||||||
|
|
||||||
openMusicMenuImpl(anchor, menuRes) {
|
openMusicMenuImpl(anchor, menuRes) {
|
||||||
when (it.itemId) {
|
when (it.itemId) {
|
||||||
|
@ -157,7 +157,7 @@ abstract class ListFragment<in T : Music, VB : ViewBinding> :
|
||||||
* @param artist The [Artist] to create the menu for.
|
* @param artist The [Artist] to create the menu for.
|
||||||
*/
|
*/
|
||||||
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, artist: Artist) {
|
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, artist: Artist) {
|
||||||
logD("Launching new artist menu: ${artist.rawName}")
|
logD("Launching new artist menu: ${artist.name}")
|
||||||
|
|
||||||
openMusicMenuImpl(anchor, menuRes) {
|
openMusicMenuImpl(anchor, menuRes) {
|
||||||
when (it.itemId) {
|
when (it.itemId) {
|
||||||
|
@ -191,7 +191,7 @@ abstract class ListFragment<in T : Music, VB : ViewBinding> :
|
||||||
* @param genre The [Genre] to create the menu for.
|
* @param genre The [Genre] to create the menu for.
|
||||||
*/
|
*/
|
||||||
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, genre: Genre) {
|
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, genre: Genre) {
|
||||||
logD("Launching new genre menu: ${genre.rawName}")
|
logD("Launching new genre menu: ${genre.name}")
|
||||||
|
|
||||||
openMusicMenuImpl(anchor, menuRes) {
|
openMusicMenuImpl(anchor, menuRes) {
|
||||||
when (it.itemId) {
|
when (it.itemId) {
|
||||||
|
@ -225,7 +225,7 @@ abstract class ListFragment<in T : Music, VB : ViewBinding> :
|
||||||
* @param playlist The [Playlist] to create the menu for.
|
* @param playlist The [Playlist] to create the menu for.
|
||||||
*/
|
*/
|
||||||
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, playlist: Playlist) {
|
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, playlist: Playlist) {
|
||||||
logD("Launching new playlist menu: ${playlist.rawName}")
|
logD("Launching new playlist menu: ${playlist.name}")
|
||||||
|
|
||||||
openMusicMenuImpl(anchor, menuRes) {
|
openMusicMenuImpl(anchor, menuRes) {
|
||||||
when (it.itemId) {
|
when (it.itemId) {
|
||||||
|
|
|
@ -24,8 +24,8 @@ import org.oxycblt.auxio.IntegerTable
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.list.Sort.Mode
|
import org.oxycblt.auxio.list.Sort.Mode
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.*
|
||||||
import org.oxycblt.auxio.music.metadata.Date
|
import org.oxycblt.auxio.music.info.Date
|
||||||
import org.oxycblt.auxio.music.metadata.Disc
|
import org.oxycblt.auxio.music.info.Disc
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A sorting method.
|
* A sorting method.
|
||||||
|
@ -566,16 +566,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
|
||||||
* @see Music.collationKey
|
* @see Music.collationKey
|
||||||
*/
|
*/
|
||||||
private class BasicComparator<T : Music> private constructor() : Comparator<T> {
|
private class BasicComparator<T : Music> private constructor() : Comparator<T> {
|
||||||
override fun compare(a: T, b: T): Int {
|
override fun compare(a: T, b: T) = a.name.compareTo(b.name)
|
||||||
val aKey = a.sortName
|
|
||||||
val bKey = b.sortName
|
|
||||||
return when {
|
|
||||||
aKey != null && bKey != null -> aKey.compareTo(bKey)
|
|
||||||
aKey == null && bKey != null -> -1 // a < b
|
|
||||||
aKey == null && bKey == null -> 0 // a = b
|
|
||||||
else -> 1 // a < b
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** A re-usable instance configured for [Song]s. */
|
/** A re-usable instance configured for [Song]s. */
|
||||||
|
|
|
@ -51,7 +51,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
|
||||||
fun bind(song: Song, listener: SelectableListListener<Song>) {
|
fun bind(song: Song, listener: SelectableListListener<Song>) {
|
||||||
listener.bind(song, this, menuButton = binding.songMenu)
|
listener.bind(song, this, menuButton = binding.songMenu)
|
||||||
binding.songAlbumCover.bind(song)
|
binding.songAlbumCover.bind(song)
|
||||||
binding.songName.text = song.resolveName(binding.context)
|
binding.songName.text = song.name.resolve(binding.context)
|
||||||
binding.songInfo.text = song.artists.resolveNames(binding.context)
|
binding.songInfo.text = song.artists.resolveNames(binding.context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,8 +80,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
|
||||||
val DIFF_CALLBACK =
|
val DIFF_CALLBACK =
|
||||||
object : SimpleDiffCallback<Song>() {
|
object : SimpleDiffCallback<Song>() {
|
||||||
override fun areContentsTheSame(oldItem: Song, newItem: Song) =
|
override fun areContentsTheSame(oldItem: Song, newItem: Song) =
|
||||||
oldItem.rawName == newItem.rawName &&
|
oldItem.name == newItem.name && oldItem.artists.areNamesTheSame(newItem.artists)
|
||||||
oldItem.artists.areRawNamesTheSame(newItem.artists)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -102,7 +101,7 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding
|
||||||
fun bind(album: Album, listener: SelectableListListener<Album>) {
|
fun bind(album: Album, listener: SelectableListListener<Album>) {
|
||||||
listener.bind(album, this, menuButton = binding.parentMenu)
|
listener.bind(album, this, menuButton = binding.parentMenu)
|
||||||
binding.parentImage.bind(album)
|
binding.parentImage.bind(album)
|
||||||
binding.parentName.text = album.resolveName(binding.context)
|
binding.parentName.text = album.name.resolve(binding.context)
|
||||||
binding.parentInfo.text = album.artists.resolveNames(binding.context)
|
binding.parentInfo.text = album.artists.resolveNames(binding.context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,8 +130,8 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding
|
||||||
val DIFF_CALLBACK =
|
val DIFF_CALLBACK =
|
||||||
object : SimpleDiffCallback<Album>() {
|
object : SimpleDiffCallback<Album>() {
|
||||||
override fun areContentsTheSame(oldItem: Album, newItem: Album) =
|
override fun areContentsTheSame(oldItem: Album, newItem: Album) =
|
||||||
oldItem.rawName == newItem.rawName &&
|
oldItem.name == newItem.name &&
|
||||||
oldItem.artists.areRawNamesTheSame(newItem.artists) &&
|
oldItem.artists.areNamesTheSame(newItem.artists) &&
|
||||||
oldItem.releaseType == newItem.releaseType
|
oldItem.releaseType == newItem.releaseType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,7 +153,7 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
|
||||||
fun bind(artist: Artist, listener: SelectableListListener<Artist>) {
|
fun bind(artist: Artist, listener: SelectableListListener<Artist>) {
|
||||||
listener.bind(artist, this, menuButton = binding.parentMenu)
|
listener.bind(artist, this, menuButton = binding.parentMenu)
|
||||||
binding.parentImage.bind(artist)
|
binding.parentImage.bind(artist)
|
||||||
binding.parentName.text = artist.resolveName(binding.context)
|
binding.parentName.text = artist.name.resolve(binding.context)
|
||||||
binding.parentInfo.text =
|
binding.parentInfo.text =
|
||||||
if (artist.songs.isNotEmpty()) {
|
if (artist.songs.isNotEmpty()) {
|
||||||
binding.context.getString(
|
binding.context.getString(
|
||||||
|
@ -193,7 +192,7 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
|
||||||
val DIFF_CALLBACK =
|
val DIFF_CALLBACK =
|
||||||
object : SimpleDiffCallback<Artist>() {
|
object : SimpleDiffCallback<Artist>() {
|
||||||
override fun areContentsTheSame(oldItem: Artist, newItem: Artist) =
|
override fun areContentsTheSame(oldItem: Artist, newItem: Artist) =
|
||||||
oldItem.rawName == newItem.rawName &&
|
oldItem.name == newItem.name &&
|
||||||
oldItem.albums.size == newItem.albums.size &&
|
oldItem.albums.size == newItem.albums.size &&
|
||||||
oldItem.songs.size == newItem.songs.size
|
oldItem.songs.size == newItem.songs.size
|
||||||
}
|
}
|
||||||
|
@ -216,7 +215,7 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding
|
||||||
fun bind(genre: Genre, listener: SelectableListListener<Genre>) {
|
fun bind(genre: Genre, listener: SelectableListListener<Genre>) {
|
||||||
listener.bind(genre, this, menuButton = binding.parentMenu)
|
listener.bind(genre, this, menuButton = binding.parentMenu)
|
||||||
binding.parentImage.bind(genre)
|
binding.parentImage.bind(genre)
|
||||||
binding.parentName.text = genre.resolveName(binding.context)
|
binding.parentName.text = genre.name.resolve(binding.context)
|
||||||
binding.parentInfo.text =
|
binding.parentInfo.text =
|
||||||
binding.context.getString(
|
binding.context.getString(
|
||||||
R.string.fmt_two,
|
R.string.fmt_two,
|
||||||
|
@ -249,7 +248,7 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding
|
||||||
val DIFF_CALLBACK =
|
val DIFF_CALLBACK =
|
||||||
object : SimpleDiffCallback<Genre>() {
|
object : SimpleDiffCallback<Genre>() {
|
||||||
override fun areContentsTheSame(oldItem: Genre, newItem: Genre): Boolean =
|
override fun areContentsTheSame(oldItem: Genre, newItem: Genre): Boolean =
|
||||||
oldItem.rawName == newItem.rawName &&
|
oldItem.name == newItem.name &&
|
||||||
oldItem.artists.size == newItem.artists.size &&
|
oldItem.artists.size == newItem.artists.size &&
|
||||||
oldItem.songs.size == newItem.songs.size
|
oldItem.songs.size == newItem.songs.size
|
||||||
}
|
}
|
||||||
|
@ -272,7 +271,7 @@ class PlaylistViewHolder private constructor(private val binding: ItemParentBind
|
||||||
fun bind(playlist: Playlist, listener: SelectableListListener<Playlist>) {
|
fun bind(playlist: Playlist, listener: SelectableListListener<Playlist>) {
|
||||||
listener.bind(playlist, this, menuButton = binding.parentMenu)
|
listener.bind(playlist, this, menuButton = binding.parentMenu)
|
||||||
binding.parentImage.bind(playlist)
|
binding.parentImage.bind(playlist)
|
||||||
binding.parentName.text = playlist.resolveName(binding.context)
|
binding.parentName.text = playlist.name.resolve(binding.context)
|
||||||
binding.parentInfo.text =
|
binding.parentInfo.text =
|
||||||
binding.context.getPlural(R.plurals.fmt_song_count, playlist.songs.size)
|
binding.context.getPlural(R.plurals.fmt_song_count, playlist.songs.size)
|
||||||
}
|
}
|
||||||
|
@ -303,7 +302,7 @@ class PlaylistViewHolder private constructor(private val binding: ItemParentBind
|
||||||
val DIFF_CALLBACK =
|
val DIFF_CALLBACK =
|
||||||
object : SimpleDiffCallback<Playlist>() {
|
object : SimpleDiffCallback<Playlist>() {
|
||||||
override fun areContentsTheSame(oldItem: Playlist, newItem: Playlist): Boolean =
|
override fun areContentsTheSame(oldItem: Playlist, newItem: Playlist): Boolean =
|
||||||
oldItem.rawName == newItem.rawName && oldItem.songs.size == newItem.songs.size
|
oldItem.name == newItem.name && oldItem.songs.size == newItem.songs.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,8 +22,6 @@ import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import java.text.CollationKey
|
|
||||||
import java.text.Collator
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
|
@ -31,9 +29,10 @@ import kotlinx.parcelize.Parcelize
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.music.fs.MimeType
|
import org.oxycblt.auxio.music.fs.MimeType
|
||||||
import org.oxycblt.auxio.music.fs.Path
|
import org.oxycblt.auxio.music.fs.Path
|
||||||
import org.oxycblt.auxio.music.metadata.Date
|
import org.oxycblt.auxio.music.info.Date
|
||||||
import org.oxycblt.auxio.music.metadata.Disc
|
import org.oxycblt.auxio.music.info.Disc
|
||||||
import org.oxycblt.auxio.music.metadata.ReleaseType
|
import org.oxycblt.auxio.music.info.Name
|
||||||
|
import org.oxycblt.auxio.music.info.ReleaseType
|
||||||
import org.oxycblt.auxio.util.concatLocalized
|
import org.oxycblt.auxio.util.concatLocalized
|
||||||
import org.oxycblt.auxio.util.toUuidOrNull
|
import org.oxycblt.auxio.util.toUuidOrNull
|
||||||
|
|
||||||
|
@ -51,35 +50,8 @@ sealed interface Music : Item {
|
||||||
*/
|
*/
|
||||||
val uid: UID
|
val uid: UID
|
||||||
|
|
||||||
/**
|
/** The [Name] of the music item. */
|
||||||
* The raw name of this item as it was extracted from the file-system. Will be null if the
|
val name: Name
|
||||||
* item's name is unknown. When showing this item in a UI, avoid this in favor of [resolveName].
|
|
||||||
*/
|
|
||||||
val rawName: String?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a name suitable for use in the app UI. This should be favored over [rawName] in
|
|
||||||
* nearly all cases.
|
|
||||||
*
|
|
||||||
* @param context [Context] required to obtain placeholder text or formatting information.
|
|
||||||
* @return A human-readable string representing the name of this music. In the case that the
|
|
||||||
* item does not have a name, an analogous "Unknown X" name is returned.
|
|
||||||
*/
|
|
||||||
fun resolveName(context: Context): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The raw sort name of this item as it was extracted from the file-system. This can be used not
|
|
||||||
* only when sorting music, but also trying to locate music based on a fuzzy search by the user.
|
|
||||||
* Will be null if the item has no known sort name.
|
|
||||||
*/
|
|
||||||
val rawSortName: String?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A black-box value derived from [rawSortName] and [rawName] that can be used for user-friendly
|
|
||||||
* sorting in the context of music. This should be preferred over [rawSortName] in most cases.
|
|
||||||
* Null if there are no [rawName] or [rawSortName] values to build on.
|
|
||||||
*/
|
|
||||||
val sortName: SortName?
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A unique identifier for a piece of music.
|
* A unique identifier for a piece of music.
|
||||||
|
@ -342,61 +314,6 @@ interface Playlist : MusicParent {
|
||||||
val durationMs: Long
|
val durationMs: Long
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A black-box datatype for a variation of music names that is suitable for music-oriented sorting.
|
|
||||||
* It will automatically handle articles like "The" and numeric components like "An".
|
|
||||||
*
|
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
|
||||||
*/
|
|
||||||
class SortName(name: String, musicSettings: MusicSettings) : Comparable<SortName> {
|
|
||||||
private val collationKey: CollationKey
|
|
||||||
val thumbString: String?
|
|
||||||
|
|
||||||
init {
|
|
||||||
var sortName = name
|
|
||||||
if (musicSettings.intelligentSorting) {
|
|
||||||
sortName = sortName.replace(LEADING_PUNCTUATION_REGEX, "")
|
|
||||||
|
|
||||||
sortName =
|
|
||||||
sortName.run {
|
|
||||||
when {
|
|
||||||
length > 5 && startsWith("the ", ignoreCase = true) -> substring(4)
|
|
||||||
length > 4 && startsWith("an ", ignoreCase = true) -> substring(3)
|
|
||||||
length > 3 && startsWith("a ", ignoreCase = true) -> substring(2)
|
|
||||||
else -> this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sortName = sortName.replace(CONSECUTIVE_DIGITS_REGEX) { it.value.padStart(6, '0') }
|
|
||||||
}
|
|
||||||
|
|
||||||
collationKey = COLLATOR.getCollationKey(sortName)
|
|
||||||
|
|
||||||
// Keep track of a string to use in the thumb view.
|
|
||||||
// Simply show '#' for everything before 'A'
|
|
||||||
// TODO: This needs to be moved elsewhere.
|
|
||||||
thumbString =
|
|
||||||
collationKey?.run {
|
|
||||||
val thumbChar = sourceString.firstOrNull()
|
|
||||||
if (thumbChar?.isLetter() == true) thumbChar.uppercase() else "#"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String = collationKey.sourceString
|
|
||||||
|
|
||||||
override fun compareTo(other: SortName) = collationKey.compareTo(other.collationKey)
|
|
||||||
|
|
||||||
override fun equals(other: Any?) = other is SortName && collationKey == other.collationKey
|
|
||||||
|
|
||||||
override fun hashCode(): Int = collationKey.hashCode()
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
val COLLATOR: Collator = Collator.getInstance().apply { strength = Collator.PRIMARY }
|
|
||||||
val LEADING_PUNCTUATION_REGEX = Regex("[\\p{Punct}+]")
|
|
||||||
val CONSECUTIVE_DIGITS_REGEX = Regex("\\d+")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run [Music.resolveName] on each instance in the given list and concatenate them into a [String]
|
* Run [Music.resolveName] on each instance in the given list and concatenate them into a [String]
|
||||||
* in a localized manner.
|
* in a localized manner.
|
||||||
|
@ -405,20 +322,20 @@ class SortName(name: String, musicSettings: MusicSettings) : Comparable<SortName
|
||||||
* @return A concatenated string.
|
* @return A concatenated string.
|
||||||
*/
|
*/
|
||||||
fun <T : Music> List<T>.resolveNames(context: Context) =
|
fun <T : Music> List<T>.resolveNames(context: Context) =
|
||||||
concatLocalized(context) { it.resolveName(context) }
|
concatLocalized(context) { it.name.resolve(context) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns if [Music.rawName] matches for each item in a list. Useful for scenarios where the
|
* Returns if [Music.name] matches for each item in a list. Useful for scenarios where the display
|
||||||
* display information of an item must be compared without a context.
|
* information of an item must be compared without a context.
|
||||||
*
|
*
|
||||||
* @param other The list of items to compare to.
|
* @param other The list of items to compare to.
|
||||||
* @return True if they are the same (by [Music.rawName]), false otherwise.
|
* @return True if they are the same (by [Music.name]), false otherwise.
|
||||||
*/
|
*/
|
||||||
fun <T : Music> List<T>.areRawNamesTheSame(other: List<T>): Boolean {
|
fun <T : Music> List<T>.areNamesTheSame(other: List<T>): Boolean {
|
||||||
for (i in 0 until max(size, other.size)) {
|
for (i in 0 until max(size, other.size)) {
|
||||||
val a = getOrNull(i) ?: return false
|
val a = getOrNull(i) ?: return false
|
||||||
val b = other.getOrNull(i) ?: return false
|
val b = other.getOrNull(i) ?: return false
|
||||||
if (a.rawName != b.rawName) {
|
if (a.name != b.name) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ import androidx.room.RoomDatabase
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import org.oxycblt.auxio.music.device.RawSong
|
import org.oxycblt.auxio.music.device.RawSong
|
||||||
import org.oxycblt.auxio.music.metadata.Date
|
import org.oxycblt.auxio.music.info.Date
|
||||||
import org.oxycblt.auxio.music.metadata.correctWhitespace
|
import org.oxycblt.auxio.music.metadata.correctWhitespace
|
||||||
import org.oxycblt.auxio.music.metadata.splitEscaped
|
import org.oxycblt.auxio.music.metadata.splitEscaped
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.device
|
package org.oxycblt.auxio.music.device
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -29,9 +28,8 @@ import org.oxycblt.auxio.music.fs.MimeType
|
||||||
import org.oxycblt.auxio.music.fs.Path
|
import org.oxycblt.auxio.music.fs.Path
|
||||||
import org.oxycblt.auxio.music.fs.toAudioUri
|
import org.oxycblt.auxio.music.fs.toAudioUri
|
||||||
import org.oxycblt.auxio.music.fs.toCoverUri
|
import org.oxycblt.auxio.music.fs.toCoverUri
|
||||||
import org.oxycblt.auxio.music.metadata.Date
|
import org.oxycblt.auxio.music.info.*
|
||||||
import org.oxycblt.auxio.music.metadata.Disc
|
import org.oxycblt.auxio.music.info.Date
|
||||||
import org.oxycblt.auxio.music.metadata.ReleaseType
|
|
||||||
import org.oxycblt.auxio.music.metadata.parseId3GenreNames
|
import org.oxycblt.auxio.music.metadata.parseId3GenreNames
|
||||||
import org.oxycblt.auxio.music.metadata.parseMultiValue
|
import org.oxycblt.auxio.music.metadata.parseMultiValue
|
||||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||||
|
@ -63,10 +61,11 @@ class SongImpl(rawSong: RawSong, musicSettings: MusicSettings) : Song {
|
||||||
update(rawSong.artistNames)
|
update(rawSong.artistNames)
|
||||||
update(rawSong.albumArtistNames)
|
update(rawSong.albumArtistNames)
|
||||||
}
|
}
|
||||||
override val rawName = requireNotNull(rawSong.name) { "Invalid raw: No title" }
|
override val name =
|
||||||
override val rawSortName = rawSong.sortName
|
Name.Known.from(
|
||||||
override val sortName = SortName((rawSortName ?: rawName), musicSettings)
|
requireNotNull(rawSong.name) { "Invalid raw: No title" },
|
||||||
override fun resolveName(context: Context) = rawName
|
rawSong.sortName,
|
||||||
|
musicSettings)
|
||||||
|
|
||||||
override val track = rawSong.track
|
override val track = rawSong.track
|
||||||
override val disc = rawSong.disc?.let { Disc(it, rawSong.subtitle) }
|
override val disc = rawSong.disc?.let { Disc(it, rawSong.subtitle) }
|
||||||
|
@ -239,10 +238,7 @@ class AlbumImpl(
|
||||||
update(rawAlbum.name)
|
update(rawAlbum.name)
|
||||||
update(rawAlbum.rawArtists.map { it.name })
|
update(rawAlbum.rawArtists.map { it.name })
|
||||||
}
|
}
|
||||||
override val rawName = rawAlbum.name
|
override val name = Name.Known.from(rawAlbum.name, rawAlbum.sortName, musicSettings)
|
||||||
override val rawSortName = rawAlbum.sortName
|
|
||||||
override val sortName = SortName((rawSortName ?: rawName), musicSettings)
|
|
||||||
override fun resolveName(context: Context) = rawName
|
|
||||||
|
|
||||||
override val dates = Date.Range.from(songs.mapNotNull { it.date })
|
override val dates = Date.Range.from(songs.mapNotNull { it.date })
|
||||||
override val releaseType = rawAlbum.releaseType ?: ReleaseType.Album(null)
|
override val releaseType = rawAlbum.releaseType ?: ReleaseType.Album(null)
|
||||||
|
@ -332,12 +328,11 @@ class ArtistImpl(
|
||||||
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
|
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
|
||||||
rawArtist.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ARTISTS, it) }
|
rawArtist.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ARTISTS, it) }
|
||||||
?: createHashedUid(MusicMode.ARTISTS) { update(rawArtist.name) }
|
?: createHashedUid(MusicMode.ARTISTS) { update(rawArtist.name) }
|
||||||
override val rawName = rawArtist.name
|
override val name =
|
||||||
override val rawSortName = rawArtist.sortName
|
rawArtist.name?.let { Name.Known.from(it, rawArtist.sortName, musicSettings) }
|
||||||
override val sortName = (rawSortName ?: rawName)?.let { SortName(it, musicSettings) }
|
?: Name.Unknown(R.string.def_artist)
|
||||||
override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_artist)
|
|
||||||
override val songs: List<Song>
|
|
||||||
|
|
||||||
|
override val songs: List<Song>
|
||||||
override val albums: List<Album>
|
override val albums: List<Album>
|
||||||
override val durationMs: Long?
|
override val durationMs: Long?
|
||||||
override val isCollaborator: Boolean
|
override val isCollaborator: Boolean
|
||||||
|
@ -417,10 +412,9 @@ class GenreImpl(
|
||||||
override val songs: List<SongImpl>
|
override val songs: List<SongImpl>
|
||||||
) : Genre {
|
) : Genre {
|
||||||
override val uid = createHashedUid(MusicMode.GENRES) { update(rawGenre.name) }
|
override val uid = createHashedUid(MusicMode.GENRES) { update(rawGenre.name) }
|
||||||
override val rawName = rawGenre.name
|
override val name =
|
||||||
override val rawSortName = rawName
|
rawGenre.name?.let { Name.Known.from(it, rawGenre.name, musicSettings) }
|
||||||
override val sortName = (rawSortName ?: rawName)?.let { SortName(it, musicSettings) }
|
?: Name.Unknown(R.string.def_genre)
|
||||||
override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_genre)
|
|
||||||
|
|
||||||
override val albums: List<Album>
|
override val albums: List<Album>
|
||||||
override val artists: List<Artist>
|
override val artists: List<Artist>
|
||||||
|
|
|
@ -21,6 +21,8 @@ package org.oxycblt.auxio.music.device
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.*
|
||||||
import org.oxycblt.auxio.music.fs.Directory
|
import org.oxycblt.auxio.music.fs.Directory
|
||||||
|
import org.oxycblt.auxio.music.info.Date
|
||||||
|
import org.oxycblt.auxio.music.info.ReleaseType
|
||||||
import org.oxycblt.auxio.music.metadata.*
|
import org.oxycblt.auxio.music.metadata.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -32,7 +32,7 @@ import kotlinx.coroutines.yield
|
||||||
import org.oxycblt.auxio.music.MusicSettings
|
import org.oxycblt.auxio.music.MusicSettings
|
||||||
import org.oxycblt.auxio.music.cache.Cache
|
import org.oxycblt.auxio.music.cache.Cache
|
||||||
import org.oxycblt.auxio.music.device.RawSong
|
import org.oxycblt.auxio.music.device.RawSong
|
||||||
import org.oxycblt.auxio.music.metadata.Date
|
import org.oxycblt.auxio.music.info.Date
|
||||||
import org.oxycblt.auxio.music.metadata.parseId3v2PositionField
|
import org.oxycblt.auxio.music.metadata.parseId3v2PositionField
|
||||||
import org.oxycblt.auxio.music.metadata.transformPositionField
|
import org.oxycblt.auxio.music.metadata.transformPositionField
|
||||||
import org.oxycblt.auxio.util.getSystemServiceCompat
|
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.metadata
|
package org.oxycblt.auxio.music.info
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.metadata
|
package org.oxycblt.auxio.music.info
|
||||||
|
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
|
|
||||||
|
@ -26,8 +26,6 @@ import org.oxycblt.auxio.list.Item
|
||||||
* @param number The disc number.
|
* @param number The disc number.
|
||||||
* @param name The name of the disc group, if any. Null if not present.
|
* @param name The name of the disc group, if any. Null if not present.
|
||||||
*/
|
*/
|
||||||
class Disc(val number: Int, val name: String?) : Item, Comparable<Disc> {
|
data class Disc(val number: Int, val name: String?) : Item, Comparable<Disc> {
|
||||||
override fun hashCode() = number.hashCode()
|
|
||||||
override fun equals(other: Any?) = other is Disc && number == other.number
|
|
||||||
override fun compareTo(other: Disc) = number.compareTo(other.number)
|
override fun compareTo(other: Disc) = number.compareTo(other.number)
|
||||||
}
|
}
|
217
app/src/main/java/org/oxycblt/auxio/music/info/Name.kt
Normal file
217
app/src/main/java/org/oxycblt/auxio/music/info/Name.kt
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* Name.kt is part of Auxio.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.oxycblt.auxio.music.info
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import java.text.CollationKey
|
||||||
|
import java.text.Collator
|
||||||
|
import org.oxycblt.auxio.music.MusicSettings
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of a music item.
|
||||||
|
*
|
||||||
|
* This class automatically implements
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart
|
||||||
|
*/
|
||||||
|
sealed interface Name : Comparable<Name> {
|
||||||
|
/**
|
||||||
|
* A logical first character that can be used to collate a sorted list of music.
|
||||||
|
*
|
||||||
|
* TODO: Move this to the home view
|
||||||
|
*/
|
||||||
|
val thumb: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a human-readable string representation of this instance.
|
||||||
|
*
|
||||||
|
* @param context [Context] required.
|
||||||
|
*/
|
||||||
|
fun resolve(context: Context): String
|
||||||
|
|
||||||
|
/** A name that could be obtained for the music item. */
|
||||||
|
sealed class Known : Name {
|
||||||
|
/** The raw name string obtained. Should be ignored in favor of [resolve]. */
|
||||||
|
abstract val raw: String
|
||||||
|
/** The raw sort name string obtained. */
|
||||||
|
abstract val sort: String?
|
||||||
|
|
||||||
|
/** A tokenized version of the name that will be compared. */
|
||||||
|
protected abstract val sortTokens: List<SortToken>
|
||||||
|
|
||||||
|
/** An individual part of a name string that can be compared intelligently. */
|
||||||
|
protected data class SortToken(val collationKey: CollationKey, val type: Type) :
|
||||||
|
Comparable<SortToken> {
|
||||||
|
override fun compareTo(other: SortToken): Int {
|
||||||
|
// Numeric tokens should always be lower than lexicographic tokens.
|
||||||
|
val modeComp = type.compareTo(other.type)
|
||||||
|
if (modeComp != 0) {
|
||||||
|
return modeComp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Numeric strings must be ordered by magnitude, thus immediately short-circuit
|
||||||
|
// the comparison if the lengths do not match.
|
||||||
|
if (type == Type.NUMERIC &&
|
||||||
|
collationKey.sourceString.length != other.collationKey.sourceString.length) {
|
||||||
|
return collationKey.sourceString.length - other.collationKey.sourceString.length
|
||||||
|
}
|
||||||
|
|
||||||
|
return collationKey.compareTo(other.collationKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Denotes the type of comparison to be performed with this token. */
|
||||||
|
enum class Type {
|
||||||
|
/** Compare as a digit string, like "65". */
|
||||||
|
NUMERIC,
|
||||||
|
/** Compare as a standard alphanumeric string, like "65daysofstatic" */
|
||||||
|
LEXICOGRAPHIC
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final override val thumb: String
|
||||||
|
get() =
|
||||||
|
// TODO: Remove these checks once you have real unit testing
|
||||||
|
sortTokens
|
||||||
|
.firstOrNull()
|
||||||
|
?.run { collationKey.sourceString.firstOrNull() }
|
||||||
|
?.let { if (it.isDigit()) "#" else it.uppercase() }
|
||||||
|
?: "?"
|
||||||
|
|
||||||
|
final override fun resolve(context: Context) = raw
|
||||||
|
|
||||||
|
final override fun compareTo(other: Name) =
|
||||||
|
when (other) {
|
||||||
|
is Known -> {
|
||||||
|
// Progressively compare the sort tokens between each known name.
|
||||||
|
sortTokens.zip(other.sortTokens).fold(0) { acc, (token, otherToken) ->
|
||||||
|
acc.takeIf { it != 0 } ?: token.compareTo(otherToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Unknown names always come before known names.
|
||||||
|
is Unknown -> 1
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Create a new instance of [Name.Known]
|
||||||
|
* @param raw The raw name obtained from the music item
|
||||||
|
* @param sort The raw sort name obtained from the music item
|
||||||
|
* @param musicSettings [MusicSettings] required to obtain user-preferred sorting
|
||||||
|
* configurations
|
||||||
|
*/
|
||||||
|
fun from(raw: String, sort: String?, musicSettings: MusicSettings): Known =
|
||||||
|
if (musicSettings.intelligentSorting) {
|
||||||
|
IntelligentKnownName(raw, sort)
|
||||||
|
} else {
|
||||||
|
SimpleKnownName(raw, sort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A placeholder name that is used when a [Known] name could not be obtained for the item.
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart
|
||||||
|
*/
|
||||||
|
data class Unknown(@StringRes val stringRes: Int) : Name {
|
||||||
|
override val thumb = "?"
|
||||||
|
override fun resolve(context: Context) = context.getString(stringRes)
|
||||||
|
override fun compareTo(other: Name) =
|
||||||
|
when (other) {
|
||||||
|
// Unknown names do not need any direct comparison right now.
|
||||||
|
is Unknown -> 0
|
||||||
|
// Unknown names always come before known names.
|
||||||
|
is Known -> -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val COLLATOR: Collator = Collator.getInstance().apply { strength = Collator.PRIMARY }
|
||||||
|
private val PUNCT_REGEX = Regex("[\\p{Punct}+]")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plain [Name.Known] implementation that is internationalization-safe.
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
private data class SimpleKnownName(override val raw: String, override val sort: String?) :
|
||||||
|
Name.Known() {
|
||||||
|
override val sortTokens = listOf(parseToken(sort ?: raw))
|
||||||
|
|
||||||
|
private fun parseToken(name: String): SortToken {
|
||||||
|
// Remove excess punctuation from the string, as those usually aren't considered in sorting.
|
||||||
|
val stripped = name.replace(PUNCT_REGEX, "").ifEmpty { name }
|
||||||
|
val collationKey = COLLATOR.getCollationKey(stripped)
|
||||||
|
// Always use lexicographic mode since we aren't parsing any numeric components
|
||||||
|
return SortToken(collationKey, SortToken.Type.LEXICOGRAPHIC)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Name.Known] implementation that adds advanced sorting behavior at the cost of localization.
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
private data class IntelligentKnownName(override val raw: String, override val sort: String?) :
|
||||||
|
Name.Known() {
|
||||||
|
override val sortTokens = parseTokens(sort ?: raw)
|
||||||
|
|
||||||
|
private fun parseTokens(name: String): List<SortToken> {
|
||||||
|
val stripped =
|
||||||
|
name
|
||||||
|
// Remove excess punctuation from the string, as those u
|
||||||
|
.replace(PUNCT_REGEX, "")
|
||||||
|
.ifEmpty { name }
|
||||||
|
.run {
|
||||||
|
// Strip any english articles like "the" or "an" from the start, as music
|
||||||
|
// sorting should ignore such when possible.
|
||||||
|
when {
|
||||||
|
length > 5 && startsWith("the ", ignoreCase = true) -> substring(4)
|
||||||
|
length > 4 && startsWith("an ", ignoreCase = true) -> substring(3)
|
||||||
|
length > 3 && startsWith("a ", ignoreCase = true) -> substring(2)
|
||||||
|
else -> this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// To properly compare numeric components in names, we have to split them up into
|
||||||
|
// individual lexicographic and numeric tokens and then individually compare them
|
||||||
|
// with special logic.
|
||||||
|
return TOKEN_REGEX.findAll(stripped).mapTo(mutableListOf()) { match ->
|
||||||
|
// Remove excess whitespace where possible
|
||||||
|
val token = match.value.trim().ifEmpty { match.value }
|
||||||
|
val collationKey: CollationKey
|
||||||
|
val type: SortToken.Type
|
||||||
|
// Separate each token into their numeric and lexicographic counterparts.
|
||||||
|
if (token.first().isDigit()) {
|
||||||
|
// The digit string comparison breaks with preceding zero digits, remove those
|
||||||
|
val digits = token.trimStart('0').ifEmpty { token }
|
||||||
|
// Other languages have other types of digit strings, still use collation keys
|
||||||
|
collationKey = COLLATOR.getCollationKey(digits)
|
||||||
|
type = SortToken.Type.NUMERIC
|
||||||
|
} else {
|
||||||
|
collationKey = COLLATOR.getCollationKey(token)
|
||||||
|
type = SortToken.Type.LEXICOGRAPHIC
|
||||||
|
}
|
||||||
|
SortToken(collationKey, type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TOKEN_REGEX = Regex("(\\d+)|(\\D+)")
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.metadata
|
package org.oxycblt.auxio.music.info
|
||||||
|
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2023 Auxio Project
|
* Copyright (c) 2023 Auxio Project
|
||||||
* AudioInfo.kt is part of Auxio.
|
* AudioProperties.kt is part of Auxio.
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -37,32 +37,33 @@ import org.oxycblt.auxio.util.logW
|
||||||
* @param resolvedMimeType The known mime type of the [Song] after it's file format was determined.
|
* @param resolvedMimeType The known mime type of the [Song] after it's file format was determined.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
data class AudioInfo(
|
data class AudioProperties(
|
||||||
val bitrateKbps: Int?,
|
val bitrateKbps: Int?,
|
||||||
val sampleRateHz: Int?,
|
val sampleRateHz: Int?,
|
||||||
val resolvedMimeType: MimeType
|
val resolvedMimeType: MimeType
|
||||||
) {
|
) {
|
||||||
/** Implements the process of extracting [AudioInfo] from a given [Song]. */
|
/** Implements the process of extracting [AudioProperties] from a given [Song]. */
|
||||||
interface Factory {
|
interface Factory {
|
||||||
/**
|
/**
|
||||||
* Extract the [AudioInfo] of a given [Song].
|
* Extract the [AudioProperties] of a given [Song].
|
||||||
*
|
*
|
||||||
* @param song The [Song] to read.
|
* @param song The [Song] to read.
|
||||||
* @return The [AudioInfo] of the [Song], if possible to obtain.
|
* @return The [AudioProperties] of the [Song], if possible to obtain.
|
||||||
*/
|
*/
|
||||||
suspend fun extract(song: Song): AudioInfo
|
suspend fun extract(song: Song): AudioProperties
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A framework-backed implementation of [AudioInfo.Factory].
|
* A framework-backed implementation of [AudioProperties.Factory].
|
||||||
*
|
*
|
||||||
* @param context [Context] required to read audio files.
|
* @param context [Context] required to read audio files.
|
||||||
*/
|
*/
|
||||||
class AudioInfoFactoryImpl @Inject constructor(@ApplicationContext private val context: Context) :
|
class AudioPropertiesFactoryImpl
|
||||||
AudioInfo.Factory {
|
@Inject
|
||||||
|
constructor(@ApplicationContext private val context: Context) : AudioProperties.Factory {
|
||||||
|
|
||||||
override suspend fun extract(song: Song): AudioInfo {
|
override suspend fun extract(song: Song): AudioProperties {
|
||||||
// While we would use ExoPlayer to extract this information, it doesn't support
|
// While we would use ExoPlayer to extract this information, it doesn't support
|
||||||
// common data like bit rate in progressive data sources due to there being no
|
// common data like bit rate in progressive data sources due to there being no
|
||||||
// demand. Thus, we are stuck with the inferior OS-provided MediaExtractor.
|
// demand. Thus, we are stuck with the inferior OS-provided MediaExtractor.
|
||||||
|
@ -76,7 +77,7 @@ class AudioInfoFactoryImpl @Inject constructor(@ApplicationContext private val c
|
||||||
// that we can show.
|
// that we can show.
|
||||||
logW("Unable to extract song attributes.")
|
logW("Unable to extract song attributes.")
|
||||||
logW(e.stackTraceToString())
|
logW(e.stackTraceToString())
|
||||||
return AudioInfo(null, null, song.mimeType)
|
return AudioProperties(null, null, song.mimeType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the first track from the extractor (This is basically always the only
|
// Get the first track from the extractor (This is basically always the only
|
||||||
|
@ -122,6 +123,6 @@ class AudioInfoFactoryImpl @Inject constructor(@ApplicationContext private val c
|
||||||
|
|
||||||
extractor.release()
|
extractor.release()
|
||||||
|
|
||||||
return AudioInfo(bitrate, sampleRate, resolvedMimeType)
|
return AudioProperties(bitrate, sampleRate, resolvedMimeType)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -28,5 +28,5 @@ import dagger.hilt.components.SingletonComponent
|
||||||
interface MetadataModule {
|
interface MetadataModule {
|
||||||
@Binds fun tagExtractor(extractor: TagExtractorImpl): TagExtractor
|
@Binds fun tagExtractor(extractor: TagExtractorImpl): TagExtractor
|
||||||
@Binds fun tagWorkerFactory(factory: TagWorkerFactoryImpl): TagWorker.Factory
|
@Binds fun tagWorkerFactory(factory: TagWorkerFactoryImpl): TagWorker.Factory
|
||||||
@Binds fun audioInfoProvider(factory: AudioInfoFactoryImpl): AudioInfo.Factory
|
@Binds fun audioPropertiesFactory(factory: AudioPropertiesFactoryImpl): AudioProperties.Factory
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import java.util.concurrent.Future
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import org.oxycblt.auxio.music.device.RawSong
|
import org.oxycblt.auxio.music.device.RawSong
|
||||||
import org.oxycblt.auxio.music.fs.toAudioUri
|
import org.oxycblt.auxio.music.fs.toAudioUri
|
||||||
|
import org.oxycblt.auxio.music.info.Date
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
|
|
||||||
|
|
|
@ -18,20 +18,17 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.user
|
package org.oxycblt.auxio.music.user
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.*
|
||||||
import org.oxycblt.auxio.music.device.DeviceLibrary
|
import org.oxycblt.auxio.music.device.DeviceLibrary
|
||||||
|
import org.oxycblt.auxio.music.info.Name
|
||||||
|
|
||||||
class PlaylistImpl
|
class PlaylistImpl
|
||||||
private constructor(
|
private constructor(
|
||||||
override val uid: Music.UID,
|
override val uid: Music.UID,
|
||||||
override val rawName: String,
|
override val name: Name,
|
||||||
override val sortName: SortName,
|
|
||||||
override val songs: List<Song>
|
override val songs: List<Song>
|
||||||
) : Playlist {
|
) : Playlist {
|
||||||
override fun resolveName(context: Context) = rawName
|
|
||||||
override val rawSortName = null
|
|
||||||
override val durationMs = songs.sumOf { it.durationMs }
|
override val durationMs = songs.sumOf { it.durationMs }
|
||||||
override val albums =
|
override val albums =
|
||||||
songs.groupBy { it.album }.entries.sortedByDescending { it.value.size }.map { it.key }
|
songs.groupBy { it.album }.entries.sortedByDescending { it.value.size }.map { it.key }
|
||||||
|
@ -41,7 +38,7 @@ private constructor(
|
||||||
*
|
*
|
||||||
* @param songs The new [Song]s to use.
|
* @param songs The new [Song]s to use.
|
||||||
*/
|
*/
|
||||||
fun edit(songs: List<Song>) = PlaylistImpl(uid, rawName, sortName, songs)
|
fun edit(songs: List<Song>) = PlaylistImpl(uid, name, songs)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clone the data in this instance to a new [PlaylistImpl] with the given [edits].
|
* Clone the data in this instance to a new [PlaylistImpl] with the given [edits].
|
||||||
|
@ -58,11 +55,10 @@ private constructor(
|
||||||
* @param songs The songs to initially populate the playlist with.
|
* @param songs The songs to initially populate the playlist with.
|
||||||
* @param musicSettings [MusicSettings] required for name configuration.
|
* @param musicSettings [MusicSettings] required for name configuration.
|
||||||
*/
|
*/
|
||||||
fun new(name: String, songs: List<Song>, musicSettings: MusicSettings) =
|
fun from(name: String, songs: List<Song>, musicSettings: MusicSettings) =
|
||||||
PlaylistImpl(
|
PlaylistImpl(
|
||||||
Music.UID.auxio(MusicMode.PLAYLISTS, UUID.randomUUID()),
|
Music.UID.auxio(MusicMode.PLAYLISTS, UUID.randomUUID()),
|
||||||
name,
|
Name.Known.from(name, null, musicSettings),
|
||||||
SortName(name, musicSettings),
|
|
||||||
songs)
|
songs)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -79,8 +75,7 @@ private constructor(
|
||||||
) =
|
) =
|
||||||
PlaylistImpl(
|
PlaylistImpl(
|
||||||
rawPlaylist.playlistInfo.playlistUid,
|
rawPlaylist.playlistInfo.playlistUid,
|
||||||
rawPlaylist.playlistInfo.name,
|
Name.Known.from(rawPlaylist.playlistInfo.name, null, musicSettings),
|
||||||
SortName(rawPlaylist.playlistInfo.name, musicSettings),
|
|
||||||
rawPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.songUid) })
|
rawPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.songUid) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,7 +106,7 @@ private class UserLibraryImpl(
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun createPlaylist(name: String, songs: List<Song>) {
|
override fun createPlaylist(name: String, songs: List<Song>) {
|
||||||
val playlistImpl = PlaylistImpl.new(name, songs, musicSettings)
|
val playlistImpl = PlaylistImpl.from(name, songs, musicSettings)
|
||||||
playlistMap[playlistImpl.uid] = playlistImpl
|
playlistMap[playlistImpl.uid] = playlistImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -85,7 +85,7 @@ class NavigationViewModel : ViewModel() {
|
||||||
logD("Already navigating, not doing explore action")
|
logD("Already navigating, not doing explore action")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logD("Navigating to ${music.rawName}")
|
logD("Navigating to ${music.name}")
|
||||||
_exploreNavigationItem.put(music)
|
_exploreNavigationItem.put(music)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +118,7 @@ class NavigationViewModel : ViewModel() {
|
||||||
if (artists.size == 1) {
|
if (artists.size == 1) {
|
||||||
exploreNavigateTo(artists[0])
|
exploreNavigateTo(artists[0])
|
||||||
} else {
|
} else {
|
||||||
logD("Navigating to a choice of ${artists.map { it.rawName }}")
|
logD("Navigating to a choice of ${artists.map { it.name }}")
|
||||||
_exploreArtistNavigationItem.put(item)
|
_exploreArtistNavigationItem.put(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,7 @@ private constructor(private val binding: ItemPickerChoiceBinding) :
|
||||||
is Genre -> binding.pickerImage.bind(music)
|
is Genre -> binding.pickerImage.bind(music)
|
||||||
is Playlist -> binding.pickerImage.bind(music)
|
is Playlist -> binding.pickerImage.bind(music)
|
||||||
}
|
}
|
||||||
binding.pickerName.text = music.resolveName(binding.context)
|
binding.pickerName.text = music.name.resolve(binding.context)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -81,7 +81,7 @@ private constructor(private val binding: ItemPickerChoiceBinding) :
|
||||||
fun <T : Music> diffCallback() =
|
fun <T : Music> diffCallback() =
|
||||||
object : SimpleDiffCallback<T>() {
|
object : SimpleDiffCallback<T>() {
|
||||||
override fun areContentsTheSame(oldItem: T, newItem: T) =
|
override fun areContentsTheSame(oldItem: T, newItem: T) =
|
||||||
oldItem.rawName == newItem.rawName
|
oldItem.name == newItem.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,7 +124,7 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
|
||||||
val context = requireContext()
|
val context = requireContext()
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
binding.playbackCover.bind(song)
|
binding.playbackCover.bind(song)
|
||||||
binding.playbackSong.text = song.resolveName(context)
|
binding.playbackSong.text = song.name.resolve(context)
|
||||||
binding.playbackInfo.text = song.artists.resolveNames(context)
|
binding.playbackInfo.text = song.artists.resolveNames(context)
|
||||||
binding.playbackProgressBar.max = song.durationMs.msToDs().toInt()
|
binding.playbackProgressBar.max = song.durationMs.msToDs().toInt()
|
||||||
}
|
}
|
||||||
|
|
|
@ -188,9 +188,9 @@ class PlaybackPanelFragment :
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
val context = requireContext()
|
val context = requireContext()
|
||||||
binding.playbackCover.bind(song)
|
binding.playbackCover.bind(song)
|
||||||
binding.playbackSong.text = song.resolveName(context)
|
binding.playbackSong.text = song.name.resolve(context)
|
||||||
binding.playbackArtist.text = song.artists.resolveNames(context)
|
binding.playbackArtist.text = song.artists.resolveNames(context)
|
||||||
binding.playbackAlbum.text = song.album.resolveName(context)
|
binding.playbackAlbum.text = song.album.name.resolve(context)
|
||||||
binding.playbackSeekBar.durationDs = song.durationMs.msToDs()
|
binding.playbackSeekBar.durationDs = song.durationMs.msToDs()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,7 +198,7 @@ class PlaybackPanelFragment :
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
val context = requireContext()
|
val context = requireContext()
|
||||||
binding.playbackToolbar.subtitle =
|
binding.playbackToolbar.subtitle =
|
||||||
parent?.resolveName(context) ?: context.getString(R.string.lbl_all_songs)
|
parent?.run { name.resolve(context) } ?: context.getString(R.string.lbl_all_songs)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePosition(positionDs: Long) {
|
private fun updatePosition(positionDs: Long) {
|
||||||
|
|
|
@ -150,7 +150,7 @@ class QueueSongViewHolder private constructor(private val binding: ItemQueueSong
|
||||||
fun bind(song: Song, listener: EditableListListener<Song>) {
|
fun bind(song: Song, listener: EditableListListener<Song>) {
|
||||||
listener.bind(song, this, bodyView, binding.songDragHandle)
|
listener.bind(song, this, bodyView, binding.songDragHandle)
|
||||||
binding.songAlbumCover.bind(song)
|
binding.songAlbumCover.bind(song)
|
||||||
binding.songName.text = song.resolveName(binding.context)
|
binding.songName.text = song.name.resolve(binding.context)
|
||||||
binding.songInfo.text = song.artists.resolveNames(binding.context)
|
binding.songInfo.text = song.artists.resolveNames(binding.context)
|
||||||
// Not swiping this ViewHolder if it's being re-bound, ensure that the background is
|
// Not swiping this ViewHolder if it's being re-bound, ensure that the background is
|
||||||
// not visible. See QueueDragCallback for why this is done.
|
// not visible. See QueueDragCallback for why this is done.
|
||||||
|
|
|
@ -289,12 +289,12 @@ constructor(
|
||||||
|
|
||||||
// Populate MediaMetadataCompat. For efficiency, cache some fields that are re-used
|
// Populate MediaMetadataCompat. For efficiency, cache some fields that are re-used
|
||||||
// several times.
|
// several times.
|
||||||
val title = song.resolveName(context)
|
val title = song.name.resolve(context)
|
||||||
val artist = song.artists.resolveNames(context)
|
val artist = song.artists.resolveNames(context)
|
||||||
val builder =
|
val builder =
|
||||||
MediaMetadataCompat.Builder()
|
MediaMetadataCompat.Builder()
|
||||||
.putText(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
.putText(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
||||||
.putText(MediaMetadataCompat.METADATA_KEY_ALBUM, song.album.resolveName(context))
|
.putText(MediaMetadataCompat.METADATA_KEY_ALBUM, song.album.name.resolve(context))
|
||||||
// Note: We would leave the artist field null if it didn't exist and let downstream
|
// Note: We would leave the artist field null if it didn't exist and let downstream
|
||||||
// consumers handle it, but that would break the notification display.
|
// consumers handle it, but that would break the notification display.
|
||||||
.putText(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
|
.putText(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
|
||||||
|
@ -305,14 +305,17 @@ constructor(
|
||||||
.putText(MediaMetadataCompat.METADATA_KEY_COMPOSER, artist)
|
.putText(MediaMetadataCompat.METADATA_KEY_COMPOSER, artist)
|
||||||
.putText(MediaMetadataCompat.METADATA_KEY_WRITER, artist)
|
.putText(MediaMetadataCompat.METADATA_KEY_WRITER, artist)
|
||||||
.putText(
|
.putText(
|
||||||
|
// TODO: Remove in favor of METADATA_KEY_DISPLAY_DESCRIPTION
|
||||||
METADATA_KEY_PARENT,
|
METADATA_KEY_PARENT,
|
||||||
parent?.resolveName(context) ?: context.getString(R.string.lbl_all_songs))
|
parent?.run { name.resolve(context) }
|
||||||
|
?: context.getString(R.string.lbl_all_songs))
|
||||||
.putText(MediaMetadataCompat.METADATA_KEY_GENRE, song.genres.resolveNames(context))
|
.putText(MediaMetadataCompat.METADATA_KEY_GENRE, song.genres.resolveNames(context))
|
||||||
.putText(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
|
.putText(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
|
||||||
.putText(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, artist)
|
.putText(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, artist)
|
||||||
.putText(
|
.putText(
|
||||||
MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION,
|
MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION,
|
||||||
parent?.resolveName(context) ?: context.getString(R.string.lbl_all_songs))
|
parent?.run { name.resolve(context) }
|
||||||
|
?: context.getString(R.string.lbl_all_songs))
|
||||||
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.durationMs)
|
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.durationMs)
|
||||||
// These fields are nullable and so we must check first before adding them to the fields.
|
// These fields are nullable and so we must check first before adding them to the fields.
|
||||||
song.track?.let {
|
song.track?.let {
|
||||||
|
@ -353,7 +356,7 @@ constructor(
|
||||||
// Media ID should not be the item index but rather the UID,
|
// Media ID should not be the item index but rather the UID,
|
||||||
// as it's used to request a song to be played from the queue.
|
// as it's used to request a song to be played from the queue.
|
||||||
.setMediaId(song.uid.toString())
|
.setMediaId(song.uid.toString())
|
||||||
.setTitle(song.resolveName(context))
|
.setTitle(song.name.resolve(context))
|
||||||
.setSubtitle(song.artists.resolveNames(context))
|
.setSubtitle(song.artists.resolveNames(context))
|
||||||
// Since we usually have to load many songs into the queue, use the
|
// Since we usually have to load many songs into the queue, use the
|
||||||
// MediaStore URI instead of loading a bitmap.
|
// MediaStore URI instead of loading a bitmap.
|
||||||
|
|
|
@ -227,7 +227,7 @@ class PlaybackService :
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logD("Loading ${song.rawName}")
|
logD("Loading ${song.name}")
|
||||||
player.setMediaItem(MediaItem.fromUri(song.uri))
|
player.setMediaItem(MediaItem.fromUri(song.uri))
|
||||||
player.prepare()
|
player.prepare()
|
||||||
player.playWhenReady = play
|
player.playWhenReady = play
|
||||||
|
|
|
@ -27,6 +27,7 @@ import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.music.info.Name
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements the fuzzy-ish searching algorithm used in the search view.
|
* Implements the fuzzy-ish searching algorithm used in the search view.
|
||||||
|
@ -63,7 +64,11 @@ class SearchEngineImpl @Inject constructor(@ApplicationContext private val conte
|
||||||
SearchEngine {
|
SearchEngine {
|
||||||
override suspend fun search(items: SearchEngine.Items, query: String) =
|
override suspend fun search(items: SearchEngine.Items, query: String) =
|
||||||
SearchEngine.Items(
|
SearchEngine.Items(
|
||||||
songs = items.songs?.searchListImpl(query) { q, song -> song.path.name.contains(q) },
|
songs =
|
||||||
|
items.songs?.searchListImpl(query) { q, song ->
|
||||||
|
// FIXME: Match case-insensitively
|
||||||
|
song.path.name.contains(q)
|
||||||
|
},
|
||||||
albums = items.albums?.searchListImpl(query),
|
albums = items.albums?.searchListImpl(query),
|
||||||
artists = items.artists?.searchListImpl(query),
|
artists = items.artists?.searchListImpl(query),
|
||||||
genres = items.genres?.searchListImpl(query))
|
genres = items.genres?.searchListImpl(query))
|
||||||
|
@ -84,17 +89,21 @@ class SearchEngineImpl @Inject constructor(@ApplicationContext private val conte
|
||||||
filter {
|
filter {
|
||||||
// See if the plain resolved name matches the query. This works for most
|
// See if the plain resolved name matches the query. This works for most
|
||||||
// situations.
|
// situations.
|
||||||
val name = it.resolveName(context)
|
val name = it.name
|
||||||
if (name.contains(query, ignoreCase = true)) {
|
|
||||||
|
val resolvedName = name.resolve(context)
|
||||||
|
if (resolvedName.contains(query, ignoreCase = true)) {
|
||||||
return@filter true
|
return@filter true
|
||||||
}
|
}
|
||||||
|
|
||||||
// See if the sort name matches. This can sometimes be helpful as certain
|
// See if the sort name matches. This can sometimes be helpful as certain
|
||||||
// libraries
|
// libraries
|
||||||
// will tag sort names to have a alphabetized version of the title.
|
// will tag sort names to have a alphabetized version of the title.
|
||||||
val sortName = it.rawSortName
|
if (name is Name.Known) {
|
||||||
if (sortName != null && sortName.contains(query, ignoreCase = true)) {
|
val sortName = name.sort
|
||||||
return@filter true
|
if (sortName != null && sortName.contains(query, ignoreCase = true)) {
|
||||||
|
return@filter true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// As a last-ditch effort, see if the normalized name matches. This will replace
|
// As a last-ditch effort, see if the normalized name matches. This will replace
|
||||||
|
@ -103,7 +112,7 @@ class SearchEngineImpl @Inject constructor(@ApplicationContext private val conte
|
||||||
// could make it match the query.
|
// could make it match the query.
|
||||||
val normalizedName =
|
val normalizedName =
|
||||||
NORMALIZE_POST_PROCESSING_REGEX.replace(
|
NORMALIZE_POST_PROCESSING_REGEX.replace(
|
||||||
Normalizer.normalize(name, Normalizer.Form.NFKD), "")
|
Normalizer.normalize(resolvedName, Normalizer.Form.NFKD), "")
|
||||||
if (normalizedName.contains(query, ignoreCase = true)) {
|
if (normalizedName.contains(query, ignoreCase = true)) {
|
||||||
return@filter true
|
return@filter true
|
||||||
}
|
}
|
||||||
|
|
|
@ -248,7 +248,8 @@ class WidgetProvider : AppWidgetProvider() {
|
||||||
setImageViewBitmap(R.id.widget_cover, state.cover)
|
setImageViewBitmap(R.id.widget_cover, state.cover)
|
||||||
setContentDescription(
|
setContentDescription(
|
||||||
R.id.widget_cover,
|
R.id.widget_cover,
|
||||||
context.getString(R.string.desc_album_cover, state.song.album.resolveName(context)))
|
context.getString(
|
||||||
|
R.string.desc_album_cover, state.song.album.name.resolve(context)))
|
||||||
} else {
|
} else {
|
||||||
// We are unable to use the typical placeholder cover with the song item due to
|
// We are unable to use the typical placeholder cover with the song item due to
|
||||||
// limitations with the corner radius. Instead use a custom-made album icon as the
|
// limitations with the corner radius. Instead use a custom-made album icon as the
|
||||||
|
@ -272,7 +273,7 @@ class WidgetProvider : AppWidgetProvider() {
|
||||||
state: WidgetComponent.PlaybackState
|
state: WidgetComponent.PlaybackState
|
||||||
): RemoteViews {
|
): RemoteViews {
|
||||||
setupCover(context, state)
|
setupCover(context, state)
|
||||||
setTextViewText(R.id.widget_song, state.song.resolveName(context))
|
setTextViewText(R.id.widget_song, state.song.name.resolve(context))
|
||||||
setTextViewText(R.id.widget_artist, state.song.artists.resolveNames(context))
|
setTextViewText(R.id.widget_artist, state.song.artists.resolveNames(context))
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@
|
||||||
<string name="lbl_compilation">Compilation</string>
|
<string name="lbl_compilation">Compilation</string>
|
||||||
<!-- As in a compilation of live music -->
|
<!-- As in a compilation of live music -->
|
||||||
<string name="lbl_compilation_live">Live compilation</string>
|
<string name="lbl_compilation_live">Live compilation</string>
|
||||||
<string name="lbl_compilation_remix">Remix compilations</string>
|
<string name="lbl_compilation_remix">Remix compilation</string>
|
||||||
<string name="lbl_soundtracks">Soundtracks</string>
|
<string name="lbl_soundtracks">Soundtracks</string>
|
||||||
<string name="lbl_soundtrack">Soundtrack</string>
|
<string name="lbl_soundtrack">Soundtrack</string>
|
||||||
<!-- As in the collection of music -->
|
<!-- As in the collection of music -->
|
||||||
|
|
|
@ -22,9 +22,9 @@ import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import org.oxycblt.auxio.music.fs.MimeType
|
import org.oxycblt.auxio.music.fs.MimeType
|
||||||
import org.oxycblt.auxio.music.fs.Path
|
import org.oxycblt.auxio.music.fs.Path
|
||||||
import org.oxycblt.auxio.music.metadata.Date
|
import org.oxycblt.auxio.music.info.Date
|
||||||
import org.oxycblt.auxio.music.metadata.Disc
|
import org.oxycblt.auxio.music.info.Disc
|
||||||
import org.oxycblt.auxio.music.metadata.ReleaseType
|
import org.oxycblt.auxio.music.info.ReleaseType
|
||||||
|
|
||||||
open class FakeSong : Song {
|
open class FakeSong : Song {
|
||||||
override val rawName: String?
|
override val rawName: String?
|
||||||
|
|
|
@ -24,7 +24,7 @@ import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
import org.oxycblt.auxio.music.MusicMode
|
||||||
import org.oxycblt.auxio.music.metadata.Date
|
import org.oxycblt.auxio.music.info.Date
|
||||||
|
|
||||||
class DeviceMusicImplTest {
|
class DeviceMusicImplTest {
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.metadata
|
package org.oxycblt.auxio.music.info
|
||||||
|
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.metadata
|
package org.oxycblt.auxio.music.info
|
||||||
|
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.metadata
|
package org.oxycblt.auxio.music.info
|
||||||
|
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
Loading…
Reference in a new issue