From c7b875376c168d5ee30fec19e1a6328998a73401 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 11 May 2023 12:16:26 -0600 Subject: [PATCH] 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 --- .../auxio/detail/AlbumDetailFragment.kt | 2 +- .../auxio/detail/ArtistDetailFragment.kt | 2 +- .../oxycblt/auxio/detail/DetailViewModel.kt | 22 +- .../auxio/detail/GenreDetailFragment.kt | 2 +- .../auxio/detail/PlaylistDetailFragment.kt | 2 +- .../oxycblt/auxio/detail/SongDetailDialog.kt | 17 +- .../detail/header/AlbumDetailHeaderAdapter.kt | 2 +- .../header/ArtistDetailHeaderAdapter.kt | 2 +- .../detail/header/GenreDetailHeaderAdapter.kt | 2 +- .../header/PlaylistDetailHeaderAdapter.kt | 2 +- .../detail/list/AlbumDetailListAdapter.kt | 6 +- .../detail/list/ArtistDetailListAdapter.kt | 11 +- .../auxio/home/list/AlbumListFragment.kt | 4 +- .../auxio/home/list/ArtistListFragment.kt | 2 +- .../auxio/home/list/GenreListFragment.kt | 2 +- .../auxio/home/list/PlaylistListFragment.kt | 2 +- .../auxio/home/list/SongListFragment.kt | 6 +- .../oxycblt/auxio/image/StyledImageView.kt | 2 +- .../auxio/image/extractor/CoverExtractor.kt | 1 - .../org/oxycblt/auxio/list/ListFragment.kt | 10 +- .../main/java/org/oxycblt/auxio/list/Sort.kt | 15 +- .../auxio/list/recycler/ViewHolders.kt | 23 +- .../java/org/oxycblt/auxio/music/Music.kt | 107 +-------- .../auxio/music/cache/CacheDatabase.kt | 2 +- .../auxio/music/device/DeviceMusicImpl.kt | 36 ++- .../oxycblt/auxio/music/device/RawMusic.kt | 2 + .../auxio/music/fs/MediaStoreExtractor.kt | 2 +- .../auxio/music/{metadata => info}/Date.kt | 2 +- .../auxio/music/{metadata => info}/Disc.kt | 6 +- .../java/org/oxycblt/auxio/music/info/Name.kt | 217 ++++++++++++++++++ .../music/{metadata => info}/ReleaseType.kt | 2 +- .../{AudioInfo.kt => AudioProperties.kt} | 25 +- .../auxio/music/metadata/MetadataModule.kt | 2 +- .../oxycblt/auxio/music/metadata/TagWorker.kt | 1 + .../oxycblt/auxio/music/user/PlaylistImpl.kt | 17 +- .../oxycblt/auxio/music/user/UserLibrary.kt | 2 +- .../auxio/navigation/NavigationViewModel.kt | 4 +- .../org/oxycblt/auxio/picker/ChoiceAdapter.kt | 4 +- .../auxio/playback/PlaybackBarFragment.kt | 2 +- .../auxio/playback/PlaybackPanelFragment.kt | 6 +- .../auxio/playback/queue/QueueAdapter.kt | 2 +- .../playback/system/MediaSessionComponent.kt | 13 +- .../auxio/playback/system/PlaybackService.kt | 2 +- .../org/oxycblt/auxio/search/SearchEngine.kt | 23 +- .../oxycblt/auxio/widgets/WidgetProvider.kt | 5 +- app/src/main/res/values/strings.xml | 2 +- .../java/org/oxycblt/auxio/music/FakeMusic.kt | 6 +- .../auxio/music/device/DeviceMusicImplTest.kt | 2 +- .../music/{metadata => info}/DateTest.kt | 2 +- .../music/{metadata => info}/DiscTest.kt | 2 +- .../{metadata => info}/ReleaseTypeTest.kt | 2 +- 51 files changed, 384 insertions(+), 255 deletions(-) rename app/src/main/java/org/oxycblt/auxio/music/{metadata => info}/Date.kt (99%) rename app/src/main/java/org/oxycblt/auxio/music/{metadata => info}/Disc.kt (80%) create mode 100644 app/src/main/java/org/oxycblt/auxio/music/info/Name.kt rename app/src/main/java/org/oxycblt/auxio/music/{metadata => info}/ReleaseType.kt (99%) rename app/src/main/java/org/oxycblt/auxio/music/metadata/{AudioInfo.kt => AudioProperties.kt} (84%) rename app/src/test/java/org/oxycblt/auxio/music/{metadata => info}/DateTest.kt (98%) rename app/src/test/java/org/oxycblt/auxio/music/{metadata => info}/DiscTest.kt (97%) rename app/src/test/java/org/oxycblt/auxio/music/{metadata => info}/ReleaseTypeTest.kt (98%) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index 0e81847ad..d6992458d 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -194,7 +194,7 @@ class AlbumDetailFragment : findNavController().navigateUp() return } - requireBinding().detailToolbar.title = album.resolveName(requireContext()) + requireBinding().detailToolbar.title = album.name.resolve(requireContext()) albumHeaderAdapter.setParent(album) } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index 7e7b70816..23e1e3456 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -204,7 +204,7 @@ class ArtistDetailFragment : findNavController().navigateUp() return } - requireBinding().detailToolbar.title = artist.resolveName(requireContext()) + requireBinding().detailToolbar.title = artist.name.resolve(requireContext()) artistHeaderAdapter.setParent(artist) } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 8b2baac6c..f176d85a0 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -36,9 +36,9 @@ import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.adapter.UpdateInstructions import org.oxycblt.auxio.music.* -import org.oxycblt.auxio.music.metadata.AudioInfo -import org.oxycblt.auxio.music.metadata.Disc -import org.oxycblt.auxio.music.metadata.ReleaseType +import org.oxycblt.auxio.music.info.Disc +import org.oxycblt.auxio.music.info.ReleaseType +import org.oxycblt.auxio.music.metadata.AudioProperties import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.util.* @@ -53,7 +53,7 @@ class DetailViewModel @Inject constructor( private val musicRepository: MusicRepository, - private val audioInfoFactory: AudioInfo.Factory, + private val audioPropertiesFactory: AudioProperties.Factory, private val musicSettings: MusicSettings, private val playbackSettings: PlaybackSettings ) : ViewModel(), MusicRepository.UpdateListener { @@ -66,9 +66,9 @@ constructor( val currentSong: StateFlow get() = _currentSong - private val _songAudioInfo = MutableStateFlow(null) - /** The [AudioInfo] of the currently shown [Song]. Null if not loaded yet. */ - val songAudioInfo: StateFlow = _songAudioInfo + private val _songAudioProperties = MutableStateFlow(null) + /** The [AudioProperties] of the currently shown [Song]. Null if not loaded yet. */ + val songAudioProperties: StateFlow = _songAudioProperties // --- ALBUM --- @@ -225,7 +225,7 @@ constructor( /** * 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. */ @@ -305,12 +305,12 @@ constructor( private fun refreshAudioInfo(song: Song) { // Clear any previous job in order to avoid stale data from appearing in the UI. currentSongJob?.cancel() - _songAudioInfo.value = null + _songAudioProperties.value = null currentSongJob = viewModelScope.launch(Dispatchers.IO) { - val info = audioInfoFactory.extract(song) + val info = audioPropertiesFactory.extract(song) yield() - _songAudioInfo.value = info + _songAudioProperties.value = info } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index 3335e6068..302c3cfbf 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -196,7 +196,7 @@ class GenreDetailFragment : findNavController().navigateUp() return } - requireBinding().detailToolbar.title = genre.resolveName(requireContext()) + requireBinding().detailToolbar.title = genre.name.resolve(requireContext()) genreHeaderAdapter.setParent(genre) } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt index 635574c13..c8d9083d4 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt @@ -187,7 +187,7 @@ class PlaylistDetailFragment : findNavController().navigateUp() return } - requireBinding().detailToolbar.title = playlist.resolveName(requireContext()) + requireBinding().detailToolbar.title = playlist.name.resolve(requireContext()) playlistHeaderAdapter.setParent(playlist) } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt index 8de46fb72..615f256e4 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt @@ -34,7 +34,8 @@ import org.oxycblt.auxio.detail.list.SongPropertyAdapter import org.oxycblt.auxio.list.adapter.UpdateInstructions import org.oxycblt.auxio.music.Music 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.playback.formatDurationMs import org.oxycblt.auxio.ui.ViewBindingDialogFragment @@ -67,10 +68,10 @@ class SongDetailDialog : ViewBindingDialogFragment() { binding.detailProperties.adapter = detailAdapter // DetailViewModel handles most initialization from the navigation argument. 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) { // Song we were showing no longer exists. findNavController().navigateUp() @@ -123,12 +124,14 @@ class SongDetailDialog : ViewBindingDialogFragment() { } } - private fun T.zipName(context: Context) = - if (rawSortName != null) { - getString(R.string.fmt_zipped_names, resolveName(context), rawSortName) + private fun T.zipName(context: Context): String { + val name = name + return if (name is Name.Known && name.sort != null) { + getString(R.string.fmt_zipped_names, name.resolve(context), name.sort) } else { - resolveName(context) + name.resolve(context) } + } private fun List.zipNames(context: Context) = concatLocalized(context) { it.zipName(context) } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/header/AlbumDetailHeaderAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/header/AlbumDetailHeaderAdapter.kt index c7747ce2c..41c12d9d6 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/header/AlbumDetailHeaderAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/header/AlbumDetailHeaderAdapter.kt @@ -77,7 +77,7 @@ private constructor(private val binding: ItemDetailHeaderBinding) : // The type text depends on the release type (Album, EP, Single, etc.) 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 binding.detailSubhead.apply { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt index 0e0e0a691..813615fc3 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt @@ -63,7 +63,7 @@ private constructor(private val binding: ItemDetailHeaderBinding) : fun bind(artist: Artist, listener: DetailHeaderAdapter.Listener) { binding.detailCover.bind(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()) { // Information about the artist's genre(s) map to the sub-head text diff --git a/app/src/main/java/org/oxycblt/auxio/detail/header/GenreDetailHeaderAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/header/GenreDetailHeaderAdapter.kt index 99a816391..42e2b4f09 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/header/GenreDetailHeaderAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/header/GenreDetailHeaderAdapter.kt @@ -62,7 +62,7 @@ private constructor(private val binding: ItemDetailHeaderBinding) : fun bind(genre: Genre, listener: DetailHeaderAdapter.Listener) { binding.detailCover.bind(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. binding.detailSubhead.isVisible = false // The song and artist count of the genre maps to the info text. diff --git a/app/src/main/java/org/oxycblt/auxio/detail/header/PlaylistDetailHeaderAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/header/PlaylistDetailHeaderAdapter.kt index db6464f93..e0825af8b 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/header/PlaylistDetailHeaderAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/header/PlaylistDetailHeaderAdapter.kt @@ -62,7 +62,7 @@ private constructor(private val binding: ItemDetailHeaderBinding) : fun bind(playlist: Playlist, listener: DetailHeaderAdapter.Listener) { binding.detailCover.bind(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. binding.detailSubhead.isVisible = false // The song count of the playlist maps to the info text. diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt index 84c8683ad..b3d01e970 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt @@ -33,7 +33,7 @@ import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.adapter.SimpleDiffCallback import org.oxycblt.auxio.music.Album 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.util.context 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 // be homogenous otherwise. @@ -204,7 +204,7 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA val DIFF_CALLBACK = object : SimpleDiffCallback() { override fun areContentsTheSame(oldItem: Song, newItem: Song) = - oldItem.rawName == newItem.rawName && oldItem.durationMs == newItem.durationMs + oldItem.name == newItem.name && oldItem.durationMs == newItem.durationMs } } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/ArtistDetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/ArtistDetailListAdapter.kt index c23c7c20c..f27a11100 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/list/ArtistDetailListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/ArtistDetailListAdapter.kt @@ -106,7 +106,7 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite fun bind(album: Album, listener: SelectableListListener) { listener.bind(album, this, menuButton = binding.parentMenu) binding.parentImage.bind(album) - binding.parentName.text = album.resolveName(binding.context) + binding.parentName.text = album.name.resolve(binding.context) binding.parentInfo.text = // Fall back to a friendlier "No date" text if the album doesn't have date information album.dates?.resolveDate(binding.context) @@ -139,7 +139,7 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite val DIFF_CALLBACK = object : SimpleDiffCallback() { 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) { listener.bind(song, this, menuButton = binding.songMenu) binding.songAlbumCover.bind(song) - binding.songName.text = song.resolveName(binding.context) - binding.songInfo.text = song.album.resolveName(binding.context) + binding.songName.text = song.name.resolve(binding.context) + binding.songInfo.text = song.album.name.resolve(binding.context) } override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) { @@ -191,8 +191,7 @@ private class ArtistSongViewHolder private constructor(private val binding: Item val DIFF_CALLBACK = object : SimpleDiffCallback() { override fun areContentsTheSame(oldItem: Song, newItem: Song) = - oldItem.rawName == newItem.rawName && - oldItem.album.rawName == newItem.album.rawName + oldItem.name == newItem.name && oldItem.album.name == newItem.album.name } } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt index 2266f56a1..765a39154 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt @@ -94,10 +94,10 @@ class AlbumListFragment : // Change how we display the popup depending on the current sort mode. return when (homeModel.getSortForTab(MusicMode.ALBUMS).mode) { // 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 - 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) is Sort.Mode.ByDate -> album.dates?.run { min.resolveDate(requireContext()) } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt index 8b3a5c369..33de26ea3 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt @@ -93,7 +93,7 @@ class ArtistListFragment : // Change how we display the popup depending on the current sort mode. return when (homeModel.getSortForTab(MusicMode.ARTISTS).mode) { // By Name -> Use Name - is Sort.Mode.ByName -> artist.sortName?.thumbString + is Sort.Mode.ByName -> artist.name.thumb // Duration -> Use formatted duration is Sort.Mode.ByDuration -> artist.durationMs?.formatDurationMs(false) diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt index b6ba1d9cf..eca18c2a2 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt @@ -92,7 +92,7 @@ class GenreListFragment : // Change how we display the popup depending on the current sort mode. return when (homeModel.getSortForTab(MusicMode.GENRES).mode) { // By Name -> Use Name - is Sort.Mode.ByName -> genre.sortName?.thumbString + is Sort.Mode.ByName -> genre.name.thumb // Duration -> Use formatted duration is Sort.Mode.ByDuration -> genre.durationMs.formatDurationMs(false) diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt index c8df535f2..5afeb7dc6 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt @@ -85,7 +85,7 @@ class PlaylistListFragment : // Change how we display the popup depending on the current sort mode. return when (homeModel.getSortForTab(MusicMode.GENRES).mode) { // By Name -> Use Name - is Sort.Mode.ByName -> playlist.sortName?.thumbString + is Sort.Mode.ByName -> playlist.name.thumb // Duration -> Use formatted duration is Sort.Mode.ByDuration -> playlist.durationMs.formatDurationMs(false) diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt index d24f6abab..9b34f8a70 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt @@ -100,13 +100,13 @@ class SongListFragment : // based off the names of the parent objects and not the child objects. return when (homeModel.getSortForTab(MusicMode.SONGS).mode) { // Name -> Use name - is Sort.Mode.ByName -> song.sortName?.thumbString + is Sort.Mode.ByName -> song.name.thumb // 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 - is Sort.Mode.ByAlbum -> song.album.sortName?.thumbString + is Sort.Mode.ByAlbum -> song.album.name.thumb // Year -> Use Full Year is Sort.Mode.ByDate -> song.album.dates?.resolveDate(requireContext()) diff --git a/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt b/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt index 8523ae18b..3f9f58671 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt @@ -147,7 +147,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr CoilUtils.dispose(this) imageLoader.enqueue(request) // Update the content description to the specified resource. - contentDescription = context.getString(descRes, music.resolveName(context)) + contentDescription = context.getString(descRes, music.name.resolve(context)) } /** diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt index 83bd50fd5..edb62ddbd 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt @@ -46,7 +46,6 @@ constructor( private val imageSettings: ImageSettings, private val mediaSourceFactory: MediaSource.Factory ) { - suspend fun extract(album: Album): InputStream? = try { when (imageSettings.coverMode) { diff --git a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt index d9c48151a..8181fbee0 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt @@ -81,7 +81,7 @@ abstract class ListFragment : * @param song The [Song] to create the menu for. */ 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) { when (it.itemId) { @@ -120,7 +120,7 @@ abstract class ListFragment : * @param album The [Album] to create the menu for. */ 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) { when (it.itemId) { @@ -157,7 +157,7 @@ abstract class ListFragment : * @param artist The [Artist] to create the menu for. */ 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) { when (it.itemId) { @@ -191,7 +191,7 @@ abstract class ListFragment : * @param genre The [Genre] to create the menu for. */ 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) { when (it.itemId) { @@ -225,7 +225,7 @@ abstract class ListFragment : * @param playlist The [Playlist] to create the menu for. */ 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) { when (it.itemId) { diff --git a/app/src/main/java/org/oxycblt/auxio/list/Sort.kt b/app/src/main/java/org/oxycblt/auxio/list/Sort.kt index 3be7b0051..c0cce8a5c 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/Sort.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/Sort.kt @@ -24,8 +24,8 @@ import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.R import org.oxycblt.auxio.list.Sort.Mode import org.oxycblt.auxio.music.* -import org.oxycblt.auxio.music.metadata.Date -import org.oxycblt.auxio.music.metadata.Disc +import org.oxycblt.auxio.music.info.Date +import org.oxycblt.auxio.music.info.Disc /** * A sorting method. @@ -566,16 +566,7 @@ data class Sort(val mode: Mode, val direction: Direction) { * @see Music.collationKey */ private class BasicComparator private constructor() : Comparator { - override fun compare(a: T, b: T): Int { - 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 - } - } + override fun compare(a: T, b: T) = a.name.compareTo(b.name) companion object { /** A re-usable instance configured for [Song]s. */ diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt index 7526ad0c9..0bf991037 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt @@ -51,7 +51,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) : fun bind(song: Song, listener: SelectableListListener) { listener.bind(song, this, menuButton = binding.songMenu) 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) } @@ -80,8 +80,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) : val DIFF_CALLBACK = object : SimpleDiffCallback() { override fun areContentsTheSame(oldItem: Song, newItem: Song) = - oldItem.rawName == newItem.rawName && - oldItem.artists.areRawNamesTheSame(newItem.artists) + oldItem.name == newItem.name && oldItem.artists.areNamesTheSame(newItem.artists) } } } @@ -102,7 +101,7 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding fun bind(album: Album, listener: SelectableListListener) { listener.bind(album, this, menuButton = binding.parentMenu) 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) } @@ -131,8 +130,8 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding val DIFF_CALLBACK = object : SimpleDiffCallback() { override fun areContentsTheSame(oldItem: Album, newItem: Album) = - oldItem.rawName == newItem.rawName && - oldItem.artists.areRawNamesTheSame(newItem.artists) && + oldItem.name == newItem.name && + oldItem.artists.areNamesTheSame(newItem.artists) && oldItem.releaseType == newItem.releaseType } } @@ -154,7 +153,7 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin fun bind(artist: Artist, listener: SelectableListListener) { listener.bind(artist, this, menuButton = binding.parentMenu) binding.parentImage.bind(artist) - binding.parentName.text = artist.resolveName(binding.context) + binding.parentName.text = artist.name.resolve(binding.context) binding.parentInfo.text = if (artist.songs.isNotEmpty()) { binding.context.getString( @@ -193,7 +192,7 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin val DIFF_CALLBACK = object : SimpleDiffCallback() { override fun areContentsTheSame(oldItem: Artist, newItem: Artist) = - oldItem.rawName == newItem.rawName && + oldItem.name == newItem.name && oldItem.albums.size == newItem.albums.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) { listener.bind(genre, this, menuButton = binding.parentMenu) binding.parentImage.bind(genre) - binding.parentName.text = genre.resolveName(binding.context) + binding.parentName.text = genre.name.resolve(binding.context) binding.parentInfo.text = binding.context.getString( R.string.fmt_two, @@ -249,7 +248,7 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding val DIFF_CALLBACK = object : SimpleDiffCallback() { override fun areContentsTheSame(oldItem: Genre, newItem: Genre): Boolean = - oldItem.rawName == newItem.rawName && + oldItem.name == newItem.name && oldItem.artists.size == newItem.artists.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) { listener.bind(playlist, this, menuButton = binding.parentMenu) binding.parentImage.bind(playlist) - binding.parentName.text = playlist.resolveName(binding.context) + binding.parentName.text = playlist.name.resolve(binding.context) binding.parentInfo.text = 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 = object : SimpleDiffCallback() { 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 } } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index 072a55f3a..9c3d37a7e 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -22,8 +22,6 @@ import android.content.Context import android.net.Uri import android.os.Parcelable import androidx.room.TypeConverter -import java.text.CollationKey -import java.text.Collator import java.util.UUID import kotlin.math.max import kotlinx.parcelize.IgnoredOnParcel @@ -31,9 +29,10 @@ import kotlinx.parcelize.Parcelize import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.music.fs.MimeType import org.oxycblt.auxio.music.fs.Path -import org.oxycblt.auxio.music.metadata.Date -import org.oxycblt.auxio.music.metadata.Disc -import org.oxycblt.auxio.music.metadata.ReleaseType +import org.oxycblt.auxio.music.info.Date +import org.oxycblt.auxio.music.info.Disc +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.toUuidOrNull @@ -51,35 +50,8 @@ sealed interface Music : Item { */ val uid: UID - /** - * The raw name of this item as it was extracted from the file-system. Will be null if the - * 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? + /** The [Name] of the music item. */ + val name: Name /** * A unique identifier for a piece of music. @@ -342,61 +314,6 @@ interface Playlist : MusicParent { 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 { - 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] * in a localized manner. @@ -405,20 +322,20 @@ class SortName(name: String, musicSettings: MusicSettings) : Comparable List.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 - * display information of an item must be compared without a context. + * Returns if [Music.name] matches for each item in a list. Useful for scenarios where the display + * information of an item must be compared without a context. * * @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 List.areRawNamesTheSame(other: List): Boolean { +fun List.areNamesTheSame(other: List): Boolean { for (i in 0 until max(size, other.size)) { val a = getOrNull(i) ?: return false val b = other.getOrNull(i) ?: return false - if (a.rawName != b.rawName) { + if (a.name != b.name) { return false } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt index 86e9e4de4..2cf6d33c0 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt @@ -28,7 +28,7 @@ import androidx.room.RoomDatabase import androidx.room.TypeConverter import androidx.room.TypeConverters 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.splitEscaped diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt index 76f78c4e2..b0ddb0ca7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt @@ -18,7 +18,6 @@ package org.oxycblt.auxio.music.device -import android.content.Context import androidx.annotation.VisibleForTesting import java.security.MessageDigest 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.toAudioUri import org.oxycblt.auxio.music.fs.toCoverUri -import org.oxycblt.auxio.music.metadata.Date -import org.oxycblt.auxio.music.metadata.Disc -import org.oxycblt.auxio.music.metadata.ReleaseType +import org.oxycblt.auxio.music.info.* +import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.metadata.parseId3GenreNames import org.oxycblt.auxio.music.metadata.parseMultiValue import org.oxycblt.auxio.util.nonZeroOrNull @@ -63,10 +61,11 @@ class SongImpl(rawSong: RawSong, musicSettings: MusicSettings) : Song { update(rawSong.artistNames) update(rawSong.albumArtistNames) } - override val rawName = requireNotNull(rawSong.name) { "Invalid raw: No title" } - override val rawSortName = rawSong.sortName - override val sortName = SortName((rawSortName ?: rawName), musicSettings) - override fun resolveName(context: Context) = rawName + override val name = + Name.Known.from( + requireNotNull(rawSong.name) { "Invalid raw: No title" }, + rawSong.sortName, + musicSettings) override val track = rawSong.track override val disc = rawSong.disc?.let { Disc(it, rawSong.subtitle) } @@ -239,10 +238,7 @@ class AlbumImpl( update(rawAlbum.name) update(rawAlbum.rawArtists.map { it.name }) } - override val rawName = rawAlbum.name - override val rawSortName = rawAlbum.sortName - override val sortName = SortName((rawSortName ?: rawName), musicSettings) - override fun resolveName(context: Context) = rawName + override val name = Name.Known.from(rawAlbum.name, rawAlbum.sortName, musicSettings) override val dates = Date.Range.from(songs.mapNotNull { it.date }) 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. rawArtist.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ARTISTS, it) } ?: createHashedUid(MusicMode.ARTISTS) { update(rawArtist.name) } - override val rawName = rawArtist.name - override val rawSortName = rawArtist.sortName - override val sortName = (rawSortName ?: rawName)?.let { SortName(it, musicSettings) } - override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_artist) - override val songs: List + override val name = + rawArtist.name?.let { Name.Known.from(it, rawArtist.sortName, musicSettings) } + ?: Name.Unknown(R.string.def_artist) + override val songs: List override val albums: List override val durationMs: Long? override val isCollaborator: Boolean @@ -417,10 +412,9 @@ class GenreImpl( override val songs: List ) : Genre { override val uid = createHashedUid(MusicMode.GENRES) { update(rawGenre.name) } - override val rawName = rawGenre.name - override val rawSortName = rawName - override val sortName = (rawSortName ?: rawName)?.let { SortName(it, musicSettings) } - override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_genre) + override val name = + rawGenre.name?.let { Name.Known.from(it, rawGenre.name, musicSettings) } + ?: Name.Unknown(R.string.def_genre) override val albums: List override val artists: List diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt index 571e90ff5..6cb66f0ae 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt @@ -21,6 +21,8 @@ package org.oxycblt.auxio.music.device import java.util.UUID import org.oxycblt.auxio.music.* 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.* /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt index 9e3d1f2d5..4603af11e 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt @@ -32,7 +32,7 @@ import kotlinx.coroutines.yield import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.cache.Cache 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.transformPositionField import org.oxycblt.auxio.util.getSystemServiceCompat diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/Date.kt b/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt similarity index 99% rename from app/src/main/java/org/oxycblt/auxio/music/metadata/Date.kt rename to app/src/main/java/org/oxycblt/auxio/music/info/Date.kt index 388a81842..685a9e0c4 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/Date.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.metadata +package org.oxycblt.auxio.music.info import android.content.Context import java.text.ParseException diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/Disc.kt b/app/src/main/java/org/oxycblt/auxio/music/info/Disc.kt similarity index 80% rename from app/src/main/java/org/oxycblt/auxio/music/metadata/Disc.kt rename to app/src/main/java/org/oxycblt/auxio/music/info/Disc.kt index e75e517d1..df1cafea4 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/Disc.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/info/Disc.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.metadata +package org.oxycblt.auxio.music.info import org.oxycblt.auxio.list.Item @@ -26,8 +26,6 @@ import org.oxycblt.auxio.list.Item * @param number The disc number. * @param name The name of the disc group, if any. Null if not present. */ -class Disc(val number: Int, val name: String?) : Item, Comparable { - override fun hashCode() = number.hashCode() - override fun equals(other: Any?) = other is Disc && number == other.number +data class Disc(val number: Int, val name: String?) : Item, Comparable { override fun compareTo(other: Disc) = number.compareTo(other.number) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt b/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt new file mode 100644 index 000000000..e985a115c --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt @@ -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 . + */ + +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 { + /** + * 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 + + /** An individual part of a name string that can be compared intelligently. */ + protected data class SortToken(val collationKey: CollationKey, val type: Type) : + Comparable { + 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 { + 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+)") + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/ReleaseType.kt b/app/src/main/java/org/oxycblt/auxio/music/info/ReleaseType.kt similarity index 99% rename from app/src/main/java/org/oxycblt/auxio/music/metadata/ReleaseType.kt rename to app/src/main/java/org/oxycblt/auxio/music/info/ReleaseType.kt index a91966d56..bc676228b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/ReleaseType.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/info/ReleaseType.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.metadata +package org.oxycblt.auxio.music.info import org.oxycblt.auxio.R diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioInfo.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioProperties.kt similarity index 84% rename from app/src/main/java/org/oxycblt/auxio/music/metadata/AudioInfo.kt rename to app/src/main/java/org/oxycblt/auxio/music/metadata/AudioProperties.kt index 2dc906563..acea28744 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioInfo.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioProperties.kt @@ -1,6 +1,6 @@ /* * 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 * 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. * @author Alexander Capehart (OxygenCobalt) */ -data class AudioInfo( +data class AudioProperties( val bitrateKbps: Int?, val sampleRateHz: Int?, 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 { /** - * Extract the [AudioInfo] of a given [Song]. + * Extract the [AudioProperties] of a given [Song]. * * @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. */ -class AudioInfoFactoryImpl @Inject constructor(@ApplicationContext private val context: Context) : - AudioInfo.Factory { +class AudioPropertiesFactoryImpl +@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 // 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. @@ -76,7 +77,7 @@ class AudioInfoFactoryImpl @Inject constructor(@ApplicationContext private val c // that we can show. logW("Unable to extract song attributes.") 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 @@ -122,6 +123,6 @@ class AudioInfoFactoryImpl @Inject constructor(@ApplicationContext private val c extractor.release() - return AudioInfo(bitrate, sampleRate, resolvedMimeType) + return AudioProperties(bitrate, sampleRate, resolvedMimeType) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/MetadataModule.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/MetadataModule.kt index 650acbe60..96685a746 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/MetadataModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/MetadataModule.kt @@ -28,5 +28,5 @@ import dagger.hilt.components.SingletonComponent interface MetadataModule { @Binds fun tagExtractor(extractor: TagExtractorImpl): TagExtractor @Binds fun tagWorkerFactory(factory: TagWorkerFactoryImpl): TagWorker.Factory - @Binds fun audioInfoProvider(factory: AudioInfoFactoryImpl): AudioInfo.Factory + @Binds fun audioPropertiesFactory(factory: AudioPropertiesFactoryImpl): AudioProperties.Factory } diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt index 2a096c789..53bacbf08 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt @@ -27,6 +27,7 @@ import java.util.concurrent.Future import javax.inject.Inject import org.oxycblt.auxio.music.device.RawSong 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.logW diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt index 814f12b0c..fa02f2b99 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt @@ -18,20 +18,17 @@ package org.oxycblt.auxio.music.user -import android.content.Context import java.util.* import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.device.DeviceLibrary +import org.oxycblt.auxio.music.info.Name class PlaylistImpl private constructor( override val uid: Music.UID, - override val rawName: String, - override val sortName: SortName, + override val name: Name, override val songs: List ) : Playlist { - override fun resolveName(context: Context) = rawName - override val rawSortName = null override val durationMs = songs.sumOf { it.durationMs } override val albums = 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. */ - fun edit(songs: List) = PlaylistImpl(uid, rawName, sortName, songs) + fun edit(songs: List) = PlaylistImpl(uid, name, songs) /** * 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 musicSettings [MusicSettings] required for name configuration. */ - fun new(name: String, songs: List, musicSettings: MusicSettings) = + fun from(name: String, songs: List, musicSettings: MusicSettings) = PlaylistImpl( Music.UID.auxio(MusicMode.PLAYLISTS, UUID.randomUUID()), - name, - SortName(name, musicSettings), + Name.Known.from(name, null, musicSettings), songs) /** @@ -79,8 +75,7 @@ private constructor( ) = PlaylistImpl( rawPlaylist.playlistInfo.playlistUid, - rawPlaylist.playlistInfo.name, - SortName(rawPlaylist.playlistInfo.name, musicSettings), + Name.Known.from(rawPlaylist.playlistInfo.name, null, musicSettings), rawPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.songUid) }) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt index f34ae6648..34717d77e 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt @@ -106,7 +106,7 @@ private class UserLibraryImpl( @Synchronized override fun createPlaylist(name: String, songs: List) { - val playlistImpl = PlaylistImpl.new(name, songs, musicSettings) + val playlistImpl = PlaylistImpl.from(name, songs, musicSettings) playlistMap[playlistImpl.uid] = playlistImpl } diff --git a/app/src/main/java/org/oxycblt/auxio/navigation/NavigationViewModel.kt b/app/src/main/java/org/oxycblt/auxio/navigation/NavigationViewModel.kt index 27125123f..6e2f43f83 100644 --- a/app/src/main/java/org/oxycblt/auxio/navigation/NavigationViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/navigation/NavigationViewModel.kt @@ -85,7 +85,7 @@ class NavigationViewModel : ViewModel() { logD("Already navigating, not doing explore action") return } - logD("Navigating to ${music.rawName}") + logD("Navigating to ${music.name}") _exploreNavigationItem.put(music) } @@ -118,7 +118,7 @@ class NavigationViewModel : ViewModel() { if (artists.size == 1) { exploreNavigateTo(artists[0]) } else { - logD("Navigating to a choice of ${artists.map { it.rawName }}") + logD("Navigating to a choice of ${artists.map { it.name }}") _exploreArtistNavigationItem.put(item) } } diff --git a/app/src/main/java/org/oxycblt/auxio/picker/ChoiceAdapter.kt b/app/src/main/java/org/oxycblt/auxio/picker/ChoiceAdapter.kt index 31cd2c085..723777018 100644 --- a/app/src/main/java/org/oxycblt/auxio/picker/ChoiceAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/picker/ChoiceAdapter.kt @@ -63,7 +63,7 @@ private constructor(private val binding: ItemPickerChoiceBinding) : is Genre -> 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 { @@ -81,7 +81,7 @@ private constructor(private val binding: ItemPickerChoiceBinding) : fun diffCallback() = object : SimpleDiffCallback() { override fun areContentsTheSame(oldItem: T, newItem: T) = - oldItem.rawName == newItem.rawName + oldItem.name == newItem.name } } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt index b33a973ff..f49d6df44 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt @@ -124,7 +124,7 @@ class PlaybackBarFragment : ViewBindingFragment() { val context = requireContext() val binding = requireBinding() 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.playbackProgressBar.max = song.durationMs.msToDs().toInt() } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt index f76a5da9a..636e2e2ca 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -188,9 +188,9 @@ class PlaybackPanelFragment : val binding = requireBinding() val context = requireContext() 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.playbackAlbum.text = song.album.resolveName(context) + binding.playbackAlbum.text = song.album.name.resolve(context) binding.playbackSeekBar.durationDs = song.durationMs.msToDs() } @@ -198,7 +198,7 @@ class PlaybackPanelFragment : val binding = requireBinding() val context = requireContext() 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) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt index 2230fe7a2..df4ac8c1d 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt @@ -150,7 +150,7 @@ class QueueSongViewHolder private constructor(private val binding: ItemQueueSong fun bind(song: Song, listener: EditableListListener) { listener.bind(song, this, bodyView, binding.songDragHandle) 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) // Not swiping this ViewHolder if it's being re-bound, ensure that the background is // not visible. See QueueDragCallback for why this is done. diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt index 6194839f5..1227b637c 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt @@ -289,12 +289,12 @@ constructor( // Populate MediaMetadataCompat. For efficiency, cache some fields that are re-used // several times. - val title = song.resolveName(context) + val title = song.name.resolve(context) val artist = song.artists.resolveNames(context) val builder = MediaMetadataCompat.Builder() .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 // consumers handle it, but that would break the notification display. .putText(MediaMetadataCompat.METADATA_KEY_ARTIST, artist) @@ -305,14 +305,17 @@ constructor( .putText(MediaMetadataCompat.METADATA_KEY_COMPOSER, artist) .putText(MediaMetadataCompat.METADATA_KEY_WRITER, artist) .putText( + // TODO: Remove in favor of METADATA_KEY_DISPLAY_DESCRIPTION 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_DISPLAY_TITLE, title) .putText(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, artist) .putText( 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) // These fields are nullable and so we must check first before adding them to the fields. song.track?.let { @@ -353,7 +356,7 @@ constructor( // 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. .setMediaId(song.uid.toString()) - .setTitle(song.resolveName(context)) + .setTitle(song.name.resolve(context)) .setSubtitle(song.artists.resolveNames(context)) // Since we usually have to load many songs into the queue, use the // MediaStore URI instead of loading a bitmap. diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt index e6ae6a8d0..18c948326 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt @@ -227,7 +227,7 @@ class PlaybackService : return } - logD("Loading ${song.rawName}") + logD("Loading ${song.name}") player.setMediaItem(MediaItem.fromUri(song.uri)) player.prepare() player.playWhenReady = play diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt index e3733549e..a04fb9e70 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt @@ -27,6 +27,7 @@ import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.music.info.Name /** * Implements the fuzzy-ish searching algorithm used in the search view. @@ -63,7 +64,11 @@ class SearchEngineImpl @Inject constructor(@ApplicationContext private val conte SearchEngine { override suspend fun search(items: SearchEngine.Items, query: String) = 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), artists = items.artists?.searchListImpl(query), genres = items.genres?.searchListImpl(query)) @@ -84,17 +89,21 @@ class SearchEngineImpl @Inject constructor(@ApplicationContext private val conte filter { // See if the plain resolved name matches the query. This works for most // situations. - val name = it.resolveName(context) - if (name.contains(query, ignoreCase = true)) { + val name = it.name + + val resolvedName = name.resolve(context) + if (resolvedName.contains(query, ignoreCase = true)) { return@filter true } // See if the sort name matches. This can sometimes be helpful as certain // libraries // will tag sort names to have a alphabetized version of the title. - val sortName = it.rawSortName - if (sortName != null && sortName.contains(query, ignoreCase = true)) { - return@filter true + if (name is Name.Known) { + val sortName = name.sort + 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 @@ -103,7 +112,7 @@ class SearchEngineImpl @Inject constructor(@ApplicationContext private val conte // could make it match the query. val normalizedName = NORMALIZE_POST_PROCESSING_REGEX.replace( - Normalizer.normalize(name, Normalizer.Form.NFKD), "") + Normalizer.normalize(resolvedName, Normalizer.Form.NFKD), "") if (normalizedName.contains(query, ignoreCase = true)) { return@filter true } diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt index 3e9726263..88e3dffa0 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt @@ -248,7 +248,8 @@ class WidgetProvider : AppWidgetProvider() { setImageViewBitmap(R.id.widget_cover, state.cover) setContentDescription( 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 { // 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 @@ -272,7 +273,7 @@ class WidgetProvider : AppWidgetProvider() { state: WidgetComponent.PlaybackState ): RemoteViews { 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)) return this } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c0de9a5c8..811c2c573 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -53,7 +53,7 @@ Compilation Live compilation - Remix compilations + Remix compilation Soundtracks Soundtrack diff --git a/app/src/test/java/org/oxycblt/auxio/music/FakeMusic.kt b/app/src/test/java/org/oxycblt/auxio/music/FakeMusic.kt index 23a55bc1f..e835294e4 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/FakeMusic.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/FakeMusic.kt @@ -22,9 +22,9 @@ import android.content.Context import android.net.Uri import org.oxycblt.auxio.music.fs.MimeType import org.oxycblt.auxio.music.fs.Path -import org.oxycblt.auxio.music.metadata.Date -import org.oxycblt.auxio.music.metadata.Disc -import org.oxycblt.auxio.music.metadata.ReleaseType +import org.oxycblt.auxio.music.info.Date +import org.oxycblt.auxio.music.info.Disc +import org.oxycblt.auxio.music.info.ReleaseType open class FakeSong : Song { override val rawName: String? diff --git a/app/src/test/java/org/oxycblt/auxio/music/device/DeviceMusicImplTest.kt b/app/src/test/java/org/oxycblt/auxio/music/device/DeviceMusicImplTest.kt index f548a4719..77076670d 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/device/DeviceMusicImplTest.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/device/DeviceMusicImplTest.kt @@ -24,7 +24,7 @@ import org.junit.Assert.assertTrue import org.junit.Test import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicMode -import org.oxycblt.auxio.music.metadata.Date +import org.oxycblt.auxio.music.info.Date class DeviceMusicImplTest { @Test diff --git a/app/src/test/java/org/oxycblt/auxio/music/metadata/DateTest.kt b/app/src/test/java/org/oxycblt/auxio/music/info/DateTest.kt similarity index 98% rename from app/src/test/java/org/oxycblt/auxio/music/metadata/DateTest.kt rename to app/src/test/java/org/oxycblt/auxio/music/info/DateTest.kt index 4f9a48240..075df1b1c 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/metadata/DateTest.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/info/DateTest.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.metadata +package org.oxycblt.auxio.music.info import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue diff --git a/app/src/test/java/org/oxycblt/auxio/music/metadata/DiscTest.kt b/app/src/test/java/org/oxycblt/auxio/music/info/DiscTest.kt similarity index 97% rename from app/src/test/java/org/oxycblt/auxio/music/metadata/DiscTest.kt rename to app/src/test/java/org/oxycblt/auxio/music/info/DiscTest.kt index 086c4ab57..260ca67cb 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/metadata/DiscTest.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/info/DiscTest.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.metadata +package org.oxycblt.auxio.music.info import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue diff --git a/app/src/test/java/org/oxycblt/auxio/music/metadata/ReleaseTypeTest.kt b/app/src/test/java/org/oxycblt/auxio/music/info/ReleaseTypeTest.kt similarity index 98% rename from app/src/test/java/org/oxycblt/auxio/music/metadata/ReleaseTypeTest.kt rename to app/src/test/java/org/oxycblt/auxio/music/info/ReleaseTypeTest.kt index eb6475430..9ca019a40 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/metadata/ReleaseTypeTest.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/info/ReleaseTypeTest.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.metadata +package org.oxycblt.auxio.music.info import org.junit.Assert.assertEquals import org.junit.Test