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 2c378e158..4e03c910f 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -31,6 +31,7 @@ import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.menu.Menu import org.oxycblt.auxio.music.PlaylistDecision import org.oxycblt.auxio.music.PlaylistMessage +import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.playback.PlaybackDecision import org.oxycblt.auxio.playback.formatDurationMs 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 ba66f34ee..533a1cd0f 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -31,6 +31,7 @@ import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.menu.Menu import org.oxycblt.auxio.music.PlaylistDecision import org.oxycblt.auxio.music.PlaylistMessage +import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.playback.PlaybackDecision import org.oxycblt.auxio.util.collect 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 5ec60d085..e94e9f18a 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -31,6 +31,7 @@ import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.menu.Menu import org.oxycblt.auxio.music.PlaylistDecision import org.oxycblt.auxio.music.PlaylistMessage +import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.playback.PlaybackDecision import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately 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 d729ffa0e..1ac3e1b8d 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt @@ -37,6 +37,7 @@ import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.menu.Menu import org.oxycblt.auxio.music.PlaylistDecision import org.oxycblt.auxio.music.PlaylistMessage +import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.playback.PlaybackDecision import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.ui.DialogAwareNavigationListener 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 cf6c1af92..76dfc2606 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt @@ -32,6 +32,7 @@ import org.oxycblt.auxio.databinding.DialogSongDetailBinding import org.oxycblt.auxio.detail.list.SongProperty import org.oxycblt.auxio.detail.list.SongPropertyAdapter import org.oxycblt.auxio.list.adapter.UpdateInstructions +import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.playback.replaygain.formatDb diff --git a/app/src/main/java/org/oxycblt/auxio/detail/decision/ArtistShowChoice.kt b/app/src/main/java/org/oxycblt/auxio/detail/decision/ArtistShowChoice.kt index a1d86fae2..1749a22ab 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/decision/ArtistShowChoice.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/decision/ArtistShowChoice.kt @@ -25,6 +25,7 @@ import org.oxycblt.auxio.list.ClickableListListener import org.oxycblt.auxio.list.adapter.FlexibleListAdapter import org.oxycblt.auxio.list.adapter.SimpleDiffCallback import org.oxycblt.auxio.list.recycler.DialogRecyclerView +import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.inflater import org.oxycblt.musikr.Artist 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 de7fa331a..a7cc77d15 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 @@ -35,6 +35,7 @@ import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.adapter.SimpleDiffCallback +import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.getAttrColorCompat 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 5dc948707..79db903f3 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 @@ -29,6 +29,7 @@ import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.adapter.SimpleDiffCallback +import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.inflater import org.oxycblt.musikr.Album diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt index cb939a587..b125ed558 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt @@ -40,6 +40,7 @@ import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.adapter.SimpleDiffCallback import org.oxycblt.auxio.list.recycler.MaterialDragCallback import org.oxycblt.auxio.list.recycler.SongViewHolder +import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.getAttrColorCompat 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 e283e7fd5..4d97d355f 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 @@ -105,10 +105,10 @@ class AlbumListFragment : // Change how we display the popup depending on the current sort mode. return when (homeModel.albumSort.mode) { // By Name -> Use Name - is Sort.Mode.ByName -> album.name.thumb + is Sort.Mode.ByName -> album.name.thumb() // By Artist -> Use name of first artist - is Sort.Mode.ByArtist -> album.artists[0].name.thumb + 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.resolve(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 ecb1266a3..91cbbe86c 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 @@ -100,7 +100,7 @@ class ArtistListFragment : // Change how we display the popup depending on the current sort mode. return when (homeModel.artistSort.mode) { // By Name -> Use Name - is Sort.Mode.ByName -> artist.name.thumb + 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 5d9c472d0..3a6483831 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 @@ -99,7 +99,7 @@ class GenreListFragment : // Change how we display the popup depending on the current sort mode. return when (homeModel.genreSort.mode) { // By Name -> Use Name - is Sort.Mode.ByName -> genre.name.thumb + 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/ListUtil.kt b/app/src/main/java/org/oxycblt/auxio/home/list/ListUtil.kt new file mode 100644 index 000000000..2e2b1a92d --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/home/list/ListUtil.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 Auxio Project + * ListUtil.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.home.list + +import androidx.core.text.isDigitsOnly +import org.oxycblt.musikr.tag.Name + +fun Name.thumb() = + when (this) { + is Name.Known -> + tokens.firstOrNull()?.let { + val value = it.collationKey.sourceString + if (value.isDigitsOnly()) "#" else value + } + is Name.Unknown -> "?" + } 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 80b658812..6b176dc54 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 @@ -97,7 +97,7 @@ class PlaylistListFragment : // Change how we display the popup depending on the current sort mode. return when (homeModel.playlistSort.mode) { // By Name -> Use Name - is Sort.Mode.ByName -> playlist.name.thumb + 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 03d1b91dc..3fd75e808 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 @@ -105,13 +105,13 @@ class SongListFragment : // based off the names of the parent objects and not the child objects. return when (homeModel.songSort.mode) { // Name -> Use name - is Sort.Mode.ByName -> song.name.thumb + is Sort.Mode.ByName -> song.name.thumb() // Artist -> Use name of first artist - is Sort.Mode.ByArtist -> song.album.artists[0].name.thumb + is Sort.Mode.ByArtist -> song.album.artists[0].name.thumb() // Album -> Use Album Name - is Sort.Mode.ByAlbum -> song.album.name.thumb + 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/list/menu/MenuDialogFragmentImpl.kt b/app/src/main/java/org/oxycblt/auxio/list/menu/MenuDialogFragmentImpl.kt index e3ee9b213..e847e88bf 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/menu/MenuDialogFragmentImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/menu/MenuDialogFragmentImpl.kt @@ -28,6 +28,7 @@ import org.oxycblt.auxio.databinding.DialogMenuBinding import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.list.ListViewModel import org.oxycblt.auxio.music.MusicViewModel +import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.formatDurationMs 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 29e5ef101..51c247f85 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 @@ -33,6 +33,7 @@ import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.adapter.SimpleDiffCallback import org.oxycblt.auxio.music.areNamesTheSame +import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.getPlural diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index d98a54c45..0ee54e513 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -39,7 +39,7 @@ import org.oxycblt.musikr.Storage import org.oxycblt.musikr.cache.Cache import org.oxycblt.musikr.cache.CacheDatabase import org.oxycblt.musikr.cover.StoredCovers -import org.oxycblt.musikr.tag.Name +import org.oxycblt.musikr.tag.interpret.Naming import org.oxycblt.musikr.tag.interpret.Separators import timber.log.Timber as L @@ -353,9 +353,9 @@ constructor( val separators = Separators.from(musicSettings.separators) val nameFactory = if (musicSettings.intelligentSorting) { - Name.Known.IntelligentFactory + Naming.intelligent() } else { - Name.Known.SimpleFactory + Naming.simple() } val locations = musicSettings.musicLocations diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt index 9b316161c..f4ab3a905 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt @@ -20,8 +20,22 @@ package org.oxycblt.auxio.music import android.content.Context import kotlin.math.max +import org.oxycblt.auxio.R import org.oxycblt.auxio.util.concatLocalized import org.oxycblt.musikr.Music +import org.oxycblt.musikr.tag.Name +import org.oxycblt.musikr.tag.Placeholder + +fun Name.resolve(context: Context) = + when (this) { + is Name.Known -> raw + is Name.Unknown -> + when (placeholder) { + Placeholder.ALBUM -> context.getString(R.string.def_album) + Placeholder.ARTIST -> context.getString(R.string.def_artist) + Placeholder.GENRE -> context.getString(R.string.def_genre) + } + } /** * Run [Name.resolve] on each instance in the given list and concatenate them into a [String] in a diff --git a/app/src/main/java/org/oxycblt/auxio/music/decision/DeletePlaylistDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/decision/DeletePlaylistDialog.kt index 322c9cd60..1cb30b66f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/decision/DeletePlaylistDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/decision/DeletePlaylistDialog.kt @@ -29,6 +29,7 @@ import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogDeletePlaylistBinding import org.oxycblt.auxio.music.MusicViewModel +import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.unlikelyToBeNull diff --git a/app/src/main/java/org/oxycblt/auxio/music/decision/ExportPlaylistDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/decision/ExportPlaylistDialog.kt index 74f40c38a..f1da40031 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/decision/ExportPlaylistDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/decision/ExportPlaylistDialog.kt @@ -31,6 +31,7 @@ import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogPlaylistExportBinding import org.oxycblt.auxio.music.MusicViewModel +import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.unlikelyToBeNull diff --git a/app/src/main/java/org/oxycblt/auxio/music/decision/PlaylistChoiceAdapter.kt b/app/src/main/java/org/oxycblt/auxio/music/decision/PlaylistChoiceAdapter.kt index b45df2cdf..f40137935 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/decision/PlaylistChoiceAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/decision/PlaylistChoiceAdapter.kt @@ -25,6 +25,7 @@ import org.oxycblt.auxio.list.ClickableListListener import org.oxycblt.auxio.list.adapter.FlexibleListAdapter import org.oxycblt.auxio.list.adapter.SimpleDiffCallback import org.oxycblt.auxio.list.recycler.DialogRecyclerView +import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.inflater diff --git a/app/src/main/java/org/oxycblt/auxio/music/decision/PlaylistPickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/decision/PlaylistPickerViewModel.kt index df2c13ee8..799ca1667 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/decision/PlaylistPickerViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/decision/PlaylistPickerViewModel.kt @@ -28,6 +28,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.PlaylistDecision +import org.oxycblt.auxio.music.resolve import org.oxycblt.musikr.Music import org.oxycblt.musikr.Playlist import org.oxycblt.musikr.Song diff --git a/app/src/main/java/org/oxycblt/auxio/music/decision/RenamePlaylistDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/decision/RenamePlaylistDialog.kt index 8e103671b..bc0da2b65 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/decision/RenamePlaylistDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/decision/RenamePlaylistDialog.kt @@ -30,6 +30,7 @@ import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogPlaylistNameBinding import org.oxycblt.auxio.music.MusicViewModel +import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.unlikelyToBeNull diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt b/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt index a4bf9d5d6..b983d81aa 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt @@ -27,6 +27,7 @@ import androidx.annotation.StringRes import androidx.media.utils.MediaConstants import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R +import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.playback.formatDurationDs import org.oxycblt.auxio.util.getPlural 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 36cb21d3f..6e488f6b5 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt @@ -26,6 +26,7 @@ import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentPlaybackBarBinding import org.oxycblt.auxio.detail.DetailViewModel +import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.ui.ViewBindingFragment 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 ad918ff63..8df3ed148 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -35,6 +35,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.list.ListViewModel +import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.ui.StyledSeekBar diff --git a/app/src/main/java/org/oxycblt/auxio/playback/decision/ArtistPlaybackChoiceAdapter.kt b/app/src/main/java/org/oxycblt/auxio/playback/decision/ArtistPlaybackChoiceAdapter.kt index 96067a8b8..2d1f785f4 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/decision/ArtistPlaybackChoiceAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/decision/ArtistPlaybackChoiceAdapter.kt @@ -25,6 +25,7 @@ import org.oxycblt.auxio.list.ClickableListListener import org.oxycblt.auxio.list.adapter.FlexibleListAdapter import org.oxycblt.auxio.list.adapter.SimpleDiffCallback import org.oxycblt.auxio.list.recycler.DialogRecyclerView +import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.inflater import org.oxycblt.musikr.Artist diff --git a/app/src/main/java/org/oxycblt/auxio/playback/decision/GenrePlaybackChoiceAdapter.kt b/app/src/main/java/org/oxycblt/auxio/playback/decision/GenrePlaybackChoiceAdapter.kt index b79614d8f..7eb31fd06 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/decision/GenrePlaybackChoiceAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/decision/GenrePlaybackChoiceAdapter.kt @@ -25,6 +25,7 @@ import org.oxycblt.auxio.list.ClickableListListener import org.oxycblt.auxio.list.adapter.FlexibleListAdapter import org.oxycblt.auxio.list.adapter.SimpleDiffCallback import org.oxycblt.auxio.list.recycler.DialogRecyclerView +import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.inflater import org.oxycblt.musikr.Genre 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 b4ef78f7e..d1b630c7f 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 @@ -32,6 +32,7 @@ import org.oxycblt.auxio.list.adapter.FlexibleListAdapter import org.oxycblt.auxio.list.adapter.PlayingIndicatorAdapter import org.oxycblt.auxio.list.recycler.MaterialDragCallback import org.oxycblt.auxio.list.recycler.SongViewHolder +import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.getAttrColorCompat diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionHolder.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionHolder.kt index a7ed418e3..0cba44c5c 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionHolder.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionHolder.kt @@ -36,6 +36,7 @@ import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.R import org.oxycblt.auxio.image.BitmapProvider import org.oxycblt.auxio.image.ImageSettings +import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.music.service.MediaSessionUID import org.oxycblt.auxio.music.service.toMediaDescription diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt index 0620bd55a..f90db46a8 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt @@ -30,6 +30,7 @@ import javax.inject.Inject import org.apache.commons.text.similarity.JaroWinklerSimilarity import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.music.MusicRepository +import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.music.service.MediaSessionUID import org.oxycblt.auxio.music.service.MusicBrowser import org.oxycblt.auxio.playback.state.PlaybackCommand 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 5ff40a0c1..c107a5c99 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt @@ -22,6 +22,7 @@ import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import java.text.Normalizer import javax.inject.Inject +import org.oxycblt.auxio.music.resolve import org.oxycblt.musikr.Album import org.oxycblt.musikr.Artist import org.oxycblt.musikr.Genre 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 9dc85edc8..54976c042 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt @@ -30,6 +30,7 @@ import android.view.View import android.widget.RemoteViews import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R +import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.playback.service.PlaybackActions import org.oxycblt.auxio.playback.state.RepeatMode diff --git a/app/src/main/java/org/oxycblt/musikr/Config.kt b/app/src/main/java/org/oxycblt/musikr/Config.kt index 741b29cfc..3885eb54c 100644 --- a/app/src/main/java/org/oxycblt/musikr/Config.kt +++ b/app/src/main/java/org/oxycblt/musikr/Config.kt @@ -20,9 +20,9 @@ package org.oxycblt.musikr import org.oxycblt.musikr.cache.Cache import org.oxycblt.musikr.cover.StoredCovers -import org.oxycblt.musikr.tag.Name +import org.oxycblt.musikr.tag.interpret.Naming import org.oxycblt.musikr.tag.interpret.Separators data class Storage(val cache: Cache, val storedCovers: StoredCovers) -data class Interpretation(val nameFactory: Name.Known.Factory, val separators: Separators) +data class Interpretation(val naming: Naming, val separators: Separators) diff --git a/app/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt b/app/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt index b5dfea7ab..99df6f3e1 100644 --- a/app/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt +++ b/app/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt @@ -115,7 +115,7 @@ private class MusicGraphBuilderImpl : MusicGraph.Builder { simplifyArtistCluster(cluster) } - val albumClusters = albumVertices.values.groupBy { it.preAlbum.rawName.lowercase() } + val albumClusters = albumVertices.values.groupBy { it.preAlbum.rawName?.lowercase() } for (cluster in albumClusters.values) { simplifyAlbumCluster(cluster) } diff --git a/app/src/main/java/org/oxycblt/musikr/playlist/m3u/M3U.kt b/app/src/main/java/org/oxycblt/musikr/playlist/m3u/M3U.kt index 2967d90fd..d11fc8f99 100644 --- a/app/src/main/java/org/oxycblt/musikr/playlist/m3u/M3U.kt +++ b/app/src/main/java/org/oxycblt/musikr/playlist/m3u/M3U.kt @@ -25,6 +25,7 @@ import java.io.BufferedWriter import java.io.InputStream import java.io.InputStreamReader import java.io.OutputStream +import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.music.resolveNames import org.oxycblt.musikr.Playlist import org.oxycblt.musikr.fs.Components diff --git a/app/src/main/java/org/oxycblt/musikr/tag/Name.kt b/app/src/main/java/org/oxycblt/musikr/tag/Name.kt index 2b021be57..ee852d3da 100644 --- a/app/src/main/java/org/oxycblt/musikr/tag/Name.kt +++ b/app/src/main/java/org/oxycblt/musikr/tag/Name.kt @@ -18,11 +18,7 @@ package org.oxycblt.musikr.tag -import android.content.Context -import androidx.annotation.StringRes -import androidx.annotation.VisibleForTesting -import java.text.CollationKey -import java.text.Collator +import org.oxycblt.musikr.tag.interpret.Token /** * The name of a music item. @@ -32,71 +28,26 @@ import java.text.Collator * @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 package - */ - 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 { + abstract 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. */ - @VisibleForTesting(VisibleForTesting.PROTECTED) abstract val sortTokens: List - - final override val thumb: String - get() = - // TODO: Remove these safety 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 + abstract val tokens: List final override fun compareTo(other: Name) = when (other) { is Known -> { val result = - sortTokens.zip(other.sortTokens).fold(0) { acc, (token, otherToken) -> + tokens.zip(other.tokens).fold(0) { acc, (token, otherToken) -> acc.takeIf { it != 0 } ?: token.compareTo(otherToken) } - if (result != 0) result else sortTokens.size.compareTo(other.sortTokens.size) + if (result != 0) result else tokens.size.compareTo(other.tokens.size) } is Unknown -> 1 } - - sealed interface Factory { - /** - * 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 - */ - fun parse(raw: String, sort: String?): Known - } - - /** Produces a simple [Known] with basic sorting heuristics that are locale-independent. */ - data object SimpleFactory : Factory { - override fun parse(raw: String, sort: String?) = SimpleKnownName(raw, sort) - } - - /** Produces an intelligent [Known] with advanced, but more fragile heuristics. */ - data object IntelligentFactory : Factory { - override fun parse(raw: String, sort: String?) = IntelligentKnownName(raw, sort) - } } /** @@ -104,11 +55,7 @@ sealed interface Name : Comparable { * * @author Alexander Capehart */ - data class Unknown(@StringRes val stringRes: Int) : Name { - override val thumb = "?" - - override fun resolve(context: Context) = context.getString(stringRes) - + data class Unknown(val placeholder: Placeholder) : Name { override fun compareTo(other: Name) = when (other) { // Unknown names do not need any direct comparison right now. @@ -119,111 +66,8 @@ sealed interface Name : Comparable { } } -private val collator: Collator = Collator.getInstance().apply { strength = Collator.PRIMARY } -private val punctRegex by lazy { Regex("[\\p{Punct}+]") } - -// TODO: Consider how you want to handle whitespace and "gaps" in names. - -/** - * Plain [Name.Known] implementation that is internationalization-safe. - * - * @author Alexander Capehart (OxygenCobalt) - */ -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(punctRegex, "").trim().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) - */ -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 { - // TODO: This routine is consuming much of the song building runtime, find a way to - // optimize it - val stripped = - name - // Remove excess punctuation from the string, as those usually aren't - // considered in sorting. - .replace(punctRegex, "") - .ifEmpty { name } - .run { - // Strip any english articles like "the" or "an" from the start, as music - // sorting should ignore such when possible. - when { - length > 4 && startsWith("the ", ignoreCase = true) -> substring(4) - length > 3 && startsWith("an ", ignoreCase = true) -> substring(3) - length > 2 && 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 { Character.getNumericValue(it) == 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 by lazy { Regex("(\\d+)|(\\D+)") } - } -} - -/** An individual part of a name string that can be compared intelligently. */ -@VisibleForTesting(VisibleForTesting.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 - } +enum class Placeholder { + ALBUM, + ARTIST, + GENRE } diff --git a/app/src/main/java/org/oxycblt/musikr/tag/interpret/Naming.kt b/app/src/main/java/org/oxycblt/musikr/tag/interpret/Naming.kt new file mode 100644 index 000000000..79300de45 --- /dev/null +++ b/app/src/main/java/org/oxycblt/musikr/tag/interpret/Naming.kt @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2024 Auxio Project + * Naming.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.musikr.tag.interpret + +import java.text.CollationKey +import java.text.Collator +import org.oxycblt.musikr.tag.Name +import org.oxycblt.musikr.tag.Placeholder + +abstract class Naming { + fun name(raw: String?, sort: String?, placeholder: Placeholder): Name = + if (raw != null) { + name(raw, sort) + } else { + Name.Unknown(placeholder) + } + + abstract fun name(raw: String, sort: String?): Name.Known + + companion object { + fun intelligent(): Naming = IntelligentNaming + + fun simple(): Naming = SimpleNaming + } +} + +private data object IntelligentNaming : Naming() { + override fun name(raw: String, sort: String?) = IntelligentKnownName(raw, sort) +} + +private data object SimpleNaming : Naming() { + override fun name(raw: String, sort: String?) = SimpleKnownName(raw, sort) +} + +private val collator: Collator = Collator.getInstance().apply { strength = Collator.PRIMARY } +private val punctRegex by lazy { Regex("[\\p{Punct}+]") } + +// TODO: Consider how you want to handle whitespace and "gaps" in names. + +/** + * 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 tokens = listOf(parseToken(sort ?: raw)) + + private fun parseToken(name: String): Token { + // Remove excess punctuation from the string, as those usually aren't considered in sorting. + val stripped = name.replace(punctRegex, "").trim().ifEmpty { name } + val collationKey = collator.getCollationKey(stripped) + // Always use lexicographic mode since we aren't parsing any numeric components + return Token(collationKey, Token.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 tokens = parseTokens(sort ?: raw) + + private fun parseTokens(name: String): List { + // TODO: This routine is consuming much of the song building runtime, find a way to + // optimize it + val stripped = + name + // Remove excess punctuation from the string, as those usually aren't + // considered in sorting. + .replace(punctRegex, "") + .ifEmpty { name } + .run { + // Strip any english articles like "the" or "an" from the start, as music + // sorting should ignore such when possible. + when { + length > 4 && startsWith("the ", ignoreCase = true) -> substring(4) + length > 3 && startsWith("an ", ignoreCase = true) -> substring(3) + length > 2 && 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: Token.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 { Character.getNumericValue(it) == 0 }.ifEmpty { token } + // Other languages have other types of digit strings, still use collation keys + collationKey = collator.getCollationKey(digits) + type = Token.Type.NUMERIC + } else { + collationKey = collator.getCollationKey(token) + type = Token.Type.LEXICOGRAPHIC + } + Token(collationKey, type) + } + } + + companion object { + private val TOKEN_REGEX by lazy { Regex("(\\d+)|(\\D+)") } + } +} diff --git a/app/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt b/app/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt index 0cbd2ad11..832668d6a 100644 --- a/app/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt +++ b/app/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt @@ -73,7 +73,7 @@ data class PreSong( data class PreAlbum( val musicBrainzId: UUID?, val name: Name, - val rawName: String, + val rawName: String?, val releaseType: ReleaseType, val preArtists: List ) diff --git a/app/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt b/app/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt index 997fe9310..de1e67abe 100644 --- a/app/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt +++ b/app/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt @@ -18,13 +18,12 @@ package org.oxycblt.musikr.tag.interpret -import org.oxycblt.auxio.R import org.oxycblt.musikr.Interpretation import org.oxycblt.musikr.fs.Format -import org.oxycblt.musikr.fs.query.DeviceFile import org.oxycblt.musikr.pipeline.RawSong import org.oxycblt.musikr.tag.Disc import org.oxycblt.musikr.tag.Name +import org.oxycblt.musikr.tag.Placeholder import org.oxycblt.musikr.tag.ReleaseType import org.oxycblt.musikr.tag.ReplayGainAdjustment import org.oxycblt.musikr.tag.parse.ParsedTags @@ -54,8 +53,7 @@ private data object TagInterpreterImpl : TagInterpreter { song.tags.albumArtistSortNames, interpretation) val preAlbum = - makePreAlbum( - song.file, song.tags, individualPreArtists, albumPreArtists, interpretation) + makePreAlbum(song.tags, individualPreArtists, albumPreArtists, interpretation) val rawArtists = individualPreArtists.ifEmpty { albumPreArtists }.ifEmpty { listOf(unknownPreArtist()) } val rawGenres = @@ -70,7 +68,7 @@ private data object TagInterpreterImpl : TagInterpreter { // TODO: Figure out what to do with date added dateAdded = song.file.lastModified, musicBrainzId = song.tags.musicBrainzId?.toUuidOrNull(), - name = interpretation.nameFactory.parse(song.tags.name, song.tags.sortName), + name = interpretation.naming.name(song.tags.name, song.tags.sortName), rawName = song.tags.name, track = song.tags.track, disc = song.tags.disc?.let { Disc(it, song.tags.subtitle) }, @@ -88,18 +86,17 @@ private data object TagInterpreterImpl : TagInterpreter { } private fun makePreAlbum( - file: DeviceFile, parsedTags: ParsedTags, individualPreArtists: List, albumPreArtists: List, interpretation: Interpretation ): PreAlbum { - // TODO: Make fallbacks for this! - val rawAlbumName = requireNotNull(parsedTags.albumName) return PreAlbum( musicBrainzId = parsedTags.albumMusicBrainzId?.toUuidOrNull(), - name = interpretation.nameFactory.parse(rawAlbumName, parsedTags.albumSortName), - rawName = rawAlbumName, + name = + interpretation.naming.name( + parsedTags.albumName, parsedTags.albumSortName, Placeholder.ALBUM), + rawName = parsedTags.albumName, releaseType = ReleaseType.parse(interpretation.separators.split(parsedTags.releaseTypes)) ?: ReleaseType.Album(null), @@ -129,14 +126,12 @@ private data object TagInterpreterImpl : TagInterpreter { sortName: String?, interpretation: Interpretation ): PreArtist { - val name = - rawName?.let { interpretation.nameFactory.parse(it, sortName) } - ?: Name.Unknown(R.string.def_artist) + val name = interpretation.naming.name(rawName, null, Placeholder.ARTIST) val musicBrainzId = musicBrainzId?.toUuidOrNull() return PreArtist(musicBrainzId, name, rawName) } - private fun unknownPreArtist() = PreArtist(null, Name.Unknown(R.string.def_artist), null) + private fun unknownPreArtist() = PreArtist(null, Name.Unknown(Placeholder.GENRE), null) private fun makePreGenres( parsedTags: ParsedTags, @@ -149,10 +144,7 @@ private data object TagInterpreterImpl : TagInterpreter { } private fun makePreGenre(rawName: String?, interpretation: Interpretation) = - PreGenre( - rawName?.let { interpretation.nameFactory.parse(it, null) } - ?: Name.Unknown(R.string.def_genre), - rawName) + PreGenre(interpretation.naming.name(rawName, null, Placeholder.GENRE), rawName) - private fun unknownPreGenre() = PreGenre(Name.Unknown(R.string.def_genre), null) + private fun unknownPreGenre() = PreGenre(Name.Unknown(Placeholder.GENRE), null) } diff --git a/app/src/main/java/org/oxycblt/musikr/tag/interpret/Token.kt b/app/src/main/java/org/oxycblt/musikr/tag/interpret/Token.kt new file mode 100644 index 000000000..749b6c5e5 --- /dev/null +++ b/app/src/main/java/org/oxycblt/musikr/tag/interpret/Token.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024 Auxio Project + * Token.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.musikr.tag.interpret + +import java.text.CollationKey + +/** An individual part of a name string that can be compared intelligently. */ +data class Token(val collationKey: CollationKey, val type: Type) : Comparable { + override fun compareTo(other: Token): 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 + } +} diff --git a/app/src/main/java/org/oxycblt/musikr/util/LangUtil.kt b/app/src/main/java/org/oxycblt/musikr/util/LangUtil.kt index 3ebd60c6c..dc804164f 100644 --- a/app/src/main/java/org/oxycblt/musikr/util/LangUtil.kt +++ b/app/src/main/java/org/oxycblt/musikr/util/LangUtil.kt @@ -20,9 +20,9 @@ package org.oxycblt.musikr.util import java.security.MessageDigest import java.util.UUID +import kotlin.reflect.KClass import org.oxycblt.auxio.BuildConfig import org.oxycblt.musikr.tag.Date -import kotlin.reflect.KClass /** * Sanitizes a value that is unlikely to be null. On debug builds, this aliases to [requireNotNull], @@ -130,4 +130,4 @@ fun lazyReflectedMethod(clazz: KClass<*>, method: String, vararg params: KClass< clazz.java.getDeclaredMethod(method, *params.map { it.java }.toTypedArray()).also { it.isAccessible = true } -} \ No newline at end of file +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 21ecb31a3..8ce492580 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -361,6 +361,7 @@ + Unknown album Unknown artist Unknown genre No date diff --git a/app/src/test/java/org/oxycblt/musikr/tag/NameTest.kt b/app/src/test/java/org/oxycblt/musikr/tag/NameTest.kt index d3c2e7bec..044c762f1 100644 --- a/app/src/test/java/org/oxycblt/musikr/tag/NameTest.kt +++ b/app/src/test/java/org/oxycblt/musikr/tag/NameTest.kt @@ -31,7 +31,7 @@ class NameTest { assertEquals("L", name.thumb) val only = name.sortTokens.single() assertEquals("Loveless", only.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type) + assertEquals(Token.Type.LEXICOGRAPHIC, only.type) } @Test @@ -42,7 +42,7 @@ class NameTest { assertEquals("A", name.thumb) val only = name.sortTokens.single() assertEquals("altJ", only.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type) + assertEquals(Token.Type.LEXICOGRAPHIC, only.type) } @Test @@ -53,7 +53,7 @@ class NameTest { assertEquals("!", name.thumb) val only = name.sortTokens.single() assertEquals("!!!", only.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type) + assertEquals(Token.Type.LEXICOGRAPHIC, only.type) } @Test @@ -64,7 +64,7 @@ class NameTest { assertEquals("Y", name.thumb) val first = name.sortTokens[0] assertEquals("Yet Yet", first.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) + assertEquals(Token.Type.LEXICOGRAPHIC, first.type) } @Test @@ -75,7 +75,7 @@ class NameTest { assertEquals("S", name.thumb) val only = name.sortTokens.single() assertEquals("Smile", only.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type) + assertEquals(Token.Type.LEXICOGRAPHIC, only.type) } @Test @@ -86,7 +86,7 @@ class NameTest { assertEquals("L", name.thumb) val only = name.sortTokens.single() assertEquals("Loveless", only.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type) + assertEquals(Token.Type.LEXICOGRAPHIC, only.type) } @Test @@ -97,10 +97,10 @@ class NameTest { assertEquals("#", name.thumb) val first = name.sortTokens[0] assertEquals("15", first.collationKey.sourceString) - assertEquals(SortToken.Type.NUMERIC, first.type) + assertEquals(Token.Type.NUMERIC, first.type) val second = name.sortTokens[1] assertEquals("Step", second.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, second.type) + assertEquals(Token.Type.LEXICOGRAPHIC, second.type) } @Test @@ -111,10 +111,10 @@ class NameTest { assertEquals("#", name.thumb) val first = name.sortTokens[0] assertEquals("23", first.collationKey.sourceString) - assertEquals(SortToken.Type.NUMERIC, first.type) + assertEquals(Token.Type.NUMERIC, first.type) val second = name.sortTokens[1] assertEquals("Kid", second.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, second.type) + assertEquals(Token.Type.LEXICOGRAPHIC, second.type) } @Test @@ -125,19 +125,19 @@ class NameTest { assertEquals("F", name.thumb) val first = name.sortTokens[0] assertEquals("Foo", first.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) + assertEquals(Token.Type.LEXICOGRAPHIC, first.type) val second = name.sortTokens[1] assertEquals("1", second.collationKey.sourceString) - assertEquals(SortToken.Type.NUMERIC, second.type) + assertEquals(Token.Type.NUMERIC, second.type) val third = name.sortTokens[2] assertEquals(" ", third.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, third.type) + assertEquals(Token.Type.LEXICOGRAPHIC, third.type) val fourth = name.sortTokens[3] assertEquals("2", fourth.collationKey.sourceString) - assertEquals(SortToken.Type.NUMERIC, fourth.type) + assertEquals(Token.Type.NUMERIC, fourth.type) val fifth = name.sortTokens[4] assertEquals("Bar", fifth.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, fifth.type) + assertEquals(Token.Type.LEXICOGRAPHIC, fifth.type) } @Test @@ -148,13 +148,13 @@ class NameTest { assertEquals("F", name.thumb) val first = name.sortTokens[0] assertEquals("Foo", first.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) + assertEquals(Token.Type.LEXICOGRAPHIC, first.type) val second = name.sortTokens[1] assertEquals("12", second.collationKey.sourceString) - assertEquals(SortToken.Type.NUMERIC, second.type) + assertEquals(Token.Type.NUMERIC, second.type) val third = name.sortTokens[2] assertEquals("Bar", third.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, third.type) + assertEquals(Token.Type.LEXICOGRAPHIC, third.type) } @Test @@ -165,10 +165,10 @@ class NameTest { assertEquals("F", name.thumb) val first = name.sortTokens[0] assertEquals("Foo", first.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) + assertEquals(Token.Type.LEXICOGRAPHIC, first.type) val second = name.sortTokens[1] assertEquals("1", second.collationKey.sourceString) - assertEquals(SortToken.Type.NUMERIC, second.type) + assertEquals(Token.Type.NUMERIC, second.type) } @Test @@ -179,10 +179,10 @@ class NameTest { assertEquals("E", name.thumb) val first = name.sortTokens[0] assertEquals("Error", first.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) + assertEquals(Token.Type.LEXICOGRAPHIC, first.type) val second = name.sortTokens[1] assertEquals("404", second.collationKey.sourceString) - assertEquals(SortToken.Type.NUMERIC, second.type) + assertEquals(Token.Type.NUMERIC, second.type) } @Test @@ -193,7 +193,7 @@ class NameTest { assertEquals("N", name.thumb) val first = name.sortTokens[0] assertEquals("National Anthem", first.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) + assertEquals(Token.Type.LEXICOGRAPHIC, first.type) } @Test @@ -204,7 +204,7 @@ class NameTest { assertEquals("E", name.thumb) val first = name.sortTokens[0] assertEquals("Eagle in Your Mind", first.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) + assertEquals(Token.Type.LEXICOGRAPHIC, first.type) } @Test @@ -215,7 +215,7 @@ class NameTest { assertEquals("S", name.thumb) val first = name.sortTokens[0] assertEquals("Song For Our Fathers", first.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) + assertEquals(Token.Type.LEXICOGRAPHIC, first.type) } @Test @@ -226,7 +226,7 @@ class NameTest { assertEquals("A", name.thumb) val only = name.sortTokens.single() assertEquals("altJ", only.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type) + assertEquals(Token.Type.LEXICOGRAPHIC, only.type) } @Test @@ -237,7 +237,7 @@ class NameTest { assertEquals("!", name.thumb) val only = name.sortTokens.single() assertEquals("!!!", only.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type) + assertEquals(Token.Type.LEXICOGRAPHIC, only.type) } @Test @@ -248,7 +248,7 @@ class NameTest { assertEquals("#", name.thumb) val first = name.sortTokens[0] assertEquals("1", first.collationKey.sourceString) - assertEquals(SortToken.Type.NUMERIC, first.type) + assertEquals(Token.Type.NUMERIC, first.type) } @Test @@ -259,7 +259,7 @@ class NameTest { assertEquals("Y", name.thumb) val first = name.sortTokens[0] assertEquals("Yet Yet", first.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) + assertEquals(Token.Type.LEXICOGRAPHIC, first.type) } @Test @@ -270,16 +270,16 @@ class NameTest { assertEquals("D", name.thumb) val first = name.sortTokens[0] assertEquals("Design", first.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) + assertEquals(Token.Type.LEXICOGRAPHIC, first.type) val second = name.sortTokens[1] assertEquals("2", second.collationKey.sourceString) - assertEquals(SortToken.Type.NUMERIC, second.type) + assertEquals(Token.Type.NUMERIC, second.type) val third = name.sortTokens[2] assertEquals(" ", third.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, third.type) + assertEquals(Token.Type.LEXICOGRAPHIC, third.type) val fourth = name.sortTokens[3] assertEquals("3", fourth.collationKey.sourceString) - assertEquals(SortToken.Type.NUMERIC, fourth.type) + assertEquals(Token.Type.NUMERIC, fourth.type) } @Test @@ -290,19 +290,19 @@ class NameTest { assertEquals("#", name.thumb) val first = name.sortTokens[0] assertEquals("2", first.collationKey.sourceString) - assertEquals(SortToken.Type.NUMERIC, first.type) + assertEquals(Token.Type.NUMERIC, first.type) val second = name.sortTokens[1] assertEquals(" ", second.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, second.type) + assertEquals(Token.Type.LEXICOGRAPHIC, second.type) val third = name.sortTokens[2] assertEquals("2", third.collationKey.sourceString) - assertEquals(SortToken.Type.NUMERIC, third.type) + assertEquals(Token.Type.NUMERIC, third.type) val fourth = name.sortTokens[3] assertEquals(" ", fourth.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, fourth.type) + assertEquals(Token.Type.LEXICOGRAPHIC, fourth.type) val fifth = name.sortTokens[4] assertEquals("5", fifth.collationKey.sourceString) - assertEquals(SortToken.Type.NUMERIC, fifth.type) + assertEquals(Token.Type.NUMERIC, fifth.type) } @Test @@ -313,7 +313,7 @@ class NameTest { assertEquals("S", name.thumb) val only = name.sortTokens.single() assertEquals("Smile", only.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type) + assertEquals(Token.Type.LEXICOGRAPHIC, only.type) } @Test