detail: add support for disc subtittles

Add support for disc subtitle information

This allows disc groups to become named, which is useful for certain
multi-part albums.

Resolves #331.
This commit is contained in:
Alexander Capehart 2023-01-21 17:22:15 -07:00
parent 82a64b5e17
commit 26f0fb7aba
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
12 changed files with 168 additions and 70 deletions

View file

@ -2,10 +2,14 @@
## dev
#### What's New
- Added support for disc subtitles
#### What's Improved
- Auxio will now accept zeroed track/disc numbers in the presence of non-zero total
track/disc fields.
## 3.0.2
#### What's New

View file

@ -29,13 +29,6 @@ import org.oxycblt.auxio.music.storage.MimeType
*/
data class SortHeader(@StringRes val titleRes: Int) : Item
/**
* A header variation that delimits between disc groups.
* @param disc The disc number to be displayed on the header.
* @author Alexander Capehart (OxygenCobalt)
*/
data class DiscHeader(val disc: Int) : Item
/**
* The properties of a [Song]'s file.
* @param bitrateKbps The bit rate, in kilobytes-per-second. Null if it could not be parsed.

View file

@ -37,6 +37,7 @@ import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.library.Library
import org.oxycblt.auxio.music.library.Sort
import org.oxycblt.auxio.music.storage.MimeType
import org.oxycblt.auxio.music.tags.Disc
import org.oxycblt.auxio.music.tags.ReleaseType
import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.util.*
@ -323,11 +324,11 @@ class DetailViewModel(application: Application) :
// songs up by disc and then delimit the groups by a disc header.
val songs = albumSongSort.songs(album.songs)
// Songs without disc tags become part of Disc 1.
val byDisc = songs.groupBy { it.disc ?: 1 }
val byDisc = songs.groupBy { it.disc ?: Disc(1, null) }
if (byDisc.size > 1) {
logD("Album has more than one disc, interspersing headers")
for (entry in byDisc.entries) {
data.add(DiscHeader(entry.key))
data.add(entry.key)
data.addAll(entry.value)
}
} else {

View file

@ -19,6 +19,7 @@ package org.oxycblt.auxio.detail.recycler
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.IntegerTable
@ -26,13 +27,13 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemAlbumSongBinding
import org.oxycblt.auxio.databinding.ItemDetailBinding
import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding
import org.oxycblt.auxio.detail.DiscHeader
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.Album
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.tags.Disc
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getPlural
@ -60,7 +61,7 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
when (getItem(position)) {
// Support the Album header, sub-headers for each disc, and special album songs.
is Album -> AlbumDetailViewHolder.VIEW_TYPE
is DiscHeader -> DiscHeaderViewHolder.VIEW_TYPE
is Disc -> DiscViewHolder.VIEW_TYPE
is Song -> AlbumSongViewHolder.VIEW_TYPE
else -> super.getItemViewType(position)
}
@ -68,7 +69,7 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
when (viewType) {
AlbumDetailViewHolder.VIEW_TYPE -> AlbumDetailViewHolder.from(parent)
DiscHeaderViewHolder.VIEW_TYPE -> DiscHeaderViewHolder.from(parent)
DiscViewHolder.VIEW_TYPE -> DiscViewHolder.from(parent)
AlbumSongViewHolder.VIEW_TYPE -> AlbumSongViewHolder.from(parent)
else -> super.onCreateViewHolder(parent, viewType)
}
@ -77,7 +78,7 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
super.onBindViewHolder(holder, position)
when (val item = getItem(position)) {
is Album -> (holder as AlbumDetailViewHolder).bind(item, listener)
is DiscHeader -> (holder as DiscHeaderViewHolder).bind(item)
is Disc -> (holder as DiscViewHolder).bind(item)
is Song -> (holder as AlbumSongViewHolder).bind(item, listener)
}
}
@ -88,7 +89,7 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
}
// The album and disc headers should be full-width in all configurations.
val item = getItem(position)
return item is Album || item is DiscHeader
return item is Album || item is Disc
}
private companion object {
@ -99,8 +100,8 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
return when {
oldItem is Album && newItem is Album ->
AlbumDetailViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
oldItem is DiscHeader && newItem is DiscHeader ->
DiscHeaderViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
oldItem is Disc && newItem is Disc ->
DiscViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
oldItem is Song && newItem is Song ->
AlbumSongViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
@ -182,18 +183,22 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
}
/**
* A [RecyclerView.ViewHolder] that displays a [DiscHeader] to delimit different disc groups. Use
* [from] to create an instance.
* A [RecyclerView.ViewHolder] that displays a [Disc] to delimit different disc groups. Use [from]
* to create an instance.
* @author Alexander Capehart (OxygenCobalt)
*/
private class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) :
private class DiscViewHolder(private val binding: ItemDiscHeaderBinding) :
RecyclerView.ViewHolder(binding.root) {
/**
* Bind new data to this instance.
* @param discHeader The new [DiscHeader] to bind.
* @param disc The new [disc] to bind.
*/
fun bind(discHeader: DiscHeader) {
binding.discNo.text = binding.context.getString(R.string.fmt_disc_no, discHeader.disc)
fun bind(disc: Disc) {
binding.discNumber.text = binding.context.getString(R.string.fmt_disc_no, disc.number)
binding.discName.apply {
text = disc.name
isGone = disc.name == null
}
}
companion object {
@ -206,13 +211,13 @@ private class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) :
* @return A new instance.
*/
fun from(parent: View) =
DiscHeaderViewHolder(ItemDiscHeaderBinding.inflate(parent.context.inflater))
DiscViewHolder(ItemDiscHeaderBinding.inflate(parent.context.inflater))
/** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK =
object : SimpleDiffCallback<DiscHeader>() {
override fun areContentsTheSame(oldItem: DiscHeader, newItem: DiscHeader) =
oldItem.disc == newItem.disc
object : SimpleDiffCallback<Disc>() {
override fun areContentsTheSame(oldItem: Disc, newItem: Disc) =
oldItem.number == newItem.number && oldItem.name == newItem.name
}
}
}

View file

@ -36,6 +36,7 @@ import org.oxycblt.auxio.music.parsing.parseId3GenreNames
import org.oxycblt.auxio.music.parsing.parseMultiValue
import org.oxycblt.auxio.music.storage.*
import org.oxycblt.auxio.music.tags.Date
import org.oxycblt.auxio.music.tags.Disc
import org.oxycblt.auxio.music.tags.ReleaseType
import org.oxycblt.auxio.util.nonZeroOrNull
import org.oxycblt.auxio.util.unlikelyToBeNull
@ -340,8 +341,8 @@ class Song constructor(raw: Raw, musicSettings: MusicSettings) : Music() {
/** The track number. Will be null if no valid track number was present in the metadata. */
val track = raw.track
/** The disc number. Will be null if no valid disc number was present in the metadata. */
val disc = raw.disc
/** The [Disc] number. Will be null if no valid disc number was present in the metadata. */
val disc = raw.disc?.let { Disc(it, raw.subtitle) }
/** The release [Date]. Will be null if no valid date was present in the metadata. */
val date = raw.date
@ -573,8 +574,10 @@ class Song constructor(raw: Raw, musicSettings: MusicSettings) : Music() {
var sortName: String? = null,
/** @see Song.track */
var track: Int? = null,
/** @see Song.disc */
/** @see Disc.number */
var disc: Int? = null,
/** @See Disc.name */
var subtitle: String? = null,
/** @see Song.date */
var date: Date? = null,
/** @see Album.Raw.mediaStoreId */

View file

@ -186,6 +186,7 @@ private class CacheDatabase(context: Context) :
append("${Columns.SORT_NAME} STRING,")
append("${Columns.TRACK} INT,")
append("${Columns.DISC} INT,")
append("${Columns.SUBTITLE} STRING,")
append("${Columns.DATE} STRING,")
append("${Columns.ALBUM_MUSIC_BRAINZ_ID} STRING,")
append("${Columns.ALBUM_NAME} STRING NOT NULL,")
@ -243,6 +244,7 @@ private class CacheDatabase(context: Context) :
val trackIndex = cursor.getColumnIndexOrThrow(Columns.TRACK)
val discIndex = cursor.getColumnIndexOrThrow(Columns.DISC)
val subtitleIndex = cursor.getColumnIndex(Columns.SUBTITLE)
val dateIndex = cursor.getColumnIndexOrThrow(Columns.DATE)
val albumMusicBrainzIdIndex =
@ -281,6 +283,7 @@ private class CacheDatabase(context: Context) :
raw.track = cursor.getIntOrNull(trackIndex)
raw.disc = cursor.getIntOrNull(discIndex)
raw.subtitle = cursor.getStringOrNull(subtitleIndex)
raw.date = cursor.getStringOrNull(dateIndex)?.let(Date::from)
raw.albumMusicBrainzId = cursor.getStringOrNull(albumMusicBrainzIdIndex)
@ -346,6 +349,7 @@ private class CacheDatabase(context: Context) :
put(Columns.TRACK, rawSong.track)
put(Columns.DISC, rawSong.disc)
put(Columns.SUBTITLE, rawSong.subtitle)
put(Columns.DATE, rawSong.date?.toString())
put(Columns.ALBUM_MUSIC_BRAINZ_ID, rawSong.albumMusicBrainzId)
@ -414,6 +418,8 @@ private class CacheDatabase(context: Context) :
const val TRACK = "track"
/** @see Song.Raw.disc */
const val DISC = "disc"
/** @see Song.Raw.subtitle */
const val SUBTITLE = "subtitle"
/** @see Song.Raw.date */
const val DATE = "date"
/** @see Song.Raw.albumMusicBrainzId */
@ -442,7 +448,7 @@ private class CacheDatabase(context: Context) :
companion object {
private const val DB_NAME = "auxio_music_cache.db"
private const val DB_VERSION = 2
private const val DB_VERSION = 3
private const val TABLE_RAW_SONGS = "raw_songs"
@Volatile private var INSTANCE: CacheDatabase? = null

View file

@ -178,15 +178,16 @@ class Task(context: Context, private val raw: Song.Raw) {
*/
private fun populateWithId3v2(textFrames: Map<String, List<String>>) {
// Song
textFrames["TXXX:musicbrainz release track id"]?.let { raw.musicBrainzId = it[0] }
textFrames["TIT2"]?.let { raw.name = it[0] }
textFrames["TSOT"]?.let { raw.sortName = it[0] }
textFrames["TXXX:musicbrainz release track id"]?.let { raw.musicBrainzId = it.first() }
textFrames["TIT2"]?.let { raw.name = it.first() }
textFrames["TSOT"]?.let { raw.sortName = it.first() }
// Track. Only parse out the track number and ignore the total tracks value.
// Track.
textFrames["TRCK"]?.run { first().parseId3v2PositionField() }?.let { raw.track = it }
// Disc. Only parse out the disc number and ignore the total discs value.
// Disc and it's subtitle name.
textFrames["TPOS"]?.run { first().parseId3v2PositionField() }?.let { raw.disc = it }
textFrames["TSST"]?.let { raw.subtitle = it.first() }
// Dates are somewhat complicated, as not only did their semantics change from a flat year
// value in ID3v2.3 to a full ISO-8601 date in ID3v2.4, but there are also a variety of
@ -204,9 +205,9 @@ class Task(context: Context, private val raw: Song.Raw) {
?.let { raw.date = it }
// Album
textFrames["TXXX:musicbrainz album id"]?.let { raw.albumMusicBrainzId = it[0] }
textFrames["TALB"]?.let { raw.albumName = it[0] }
textFrames["TSOA"]?.let { raw.albumSortName = it[0] }
textFrames["TXXX:musicbrainz album id"]?.let { raw.albumMusicBrainzId = it.first() }
textFrames["TALB"]?.let { raw.albumName = it.first() }
textFrames["TSOA"]?.let { raw.albumSortName = it.first() }
(textFrames["TXXX:musicbrainz album type"] ?: textFrames["GRP1"])?.let {
raw.releaseTypes = it
}
@ -244,19 +245,19 @@ class Task(context: Context, private val raw: Song.Raw) {
?: textFrames["TYER"]?.run { first().toIntOrNull() } ?: return null
val tdat = textFrames["TDAT"]
return if (tdat != null && tdat[0].length == 4 && tdat[0].isDigitsOnly()) {
return if (tdat != null && tdat.first().length == 4 && tdat.first().isDigitsOnly()) {
// TDAT frames consist of a 4-digit string where the first two digits are
// the month and the last two digits are the day.
val mm = tdat[0].substring(0..1).toInt()
val dd = tdat[0].substring(2..3).toInt()
val mm = tdat.first().substring(0..1).toInt()
val dd = tdat.first().substring(2..3).toInt()
val time = textFrames["TIME"]
if (time != null && time[0].length == 4 && time[0].isDigitsOnly()) {
if (time != null && time.first().length == 4 && time.first().isDigitsOnly()) {
// TIME frames consist of a 4-digit string where the first two digits are
// the hour and the last two digits are the minutes. No second value is
// possible.
val hh = time[0].substring(0..1).toInt()
val mi = time[0].substring(2..3).toInt()
val hh = time.first().substring(0..1).toInt()
val mi = time.first().substring(2..3).toInt()
// Able to return a full date.
Date.from(year, mm, dd, hh, mi)
} else {
@ -275,9 +276,9 @@ class Task(context: Context, private val raw: Song.Raw) {
*/
private fun populateWithVorbis(comments: Map<String, List<String>>) {
// Song
comments["musicbrainz_releasetrackid"]?.let { raw.musicBrainzId = it[0] }
comments["title"]?.let { raw.name = it[0] }
comments["titlesort"]?.let { raw.sortName = it[0] }
comments["musicbrainz_releasetrackid"]?.let { raw.musicBrainzId = it.first() }
comments["title"]?.let { raw.name = it.first() }
comments["titlesort"]?.let { raw.sortName = it.first() }
// Track.
parseVorbisPositionField(
@ -285,11 +286,12 @@ class Task(context: Context, private val raw: Song.Raw) {
(comments["totaltracks"] ?: comments["tracktotal"] ?: comments["trackc"])?.first())
?.let { raw.track = it }
// Disc.
// Disc and it's subtitle name.
parseVorbisPositionField(
comments["discnumber"]?.first(),
(comments["totaldiscs"] ?: comments["disctotal"] ?: comments["discc"])?.first())
?.let { raw.disc = it }
comments["discsubtitle"]?.let { raw.subtitle = it.first() }
// Vorbis dates are less complicated, but there are still several types
// Our hierarchy for dates is as such:
@ -303,9 +305,9 @@ class Task(context: Context, private val raw: Song.Raw) {
?.let { raw.date = it }
// Album
comments["musicbrainz_albumid"]?.let { raw.albumMusicBrainzId = it[0] }
comments["album"]?.let { raw.albumName = it[0] }
comments["albumsort"]?.let { raw.albumSortName = it[0] }
comments["musicbrainz_albumid"]?.let { raw.albumMusicBrainzId = it.first() }
comments["album"]?.let { raw.albumName = it.first() }
comments["albumsort"]?.let { raw.albumSortName = it.first() }
comments["releasetype"]?.let { raw.releaseTypes = it }
// Artist

View file

@ -24,6 +24,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.library.Sort.Mode
import org.oxycblt.auxio.music.tags.Date
import org.oxycblt.auxio.music.tags.Disc
/**
* A sorting method.
@ -215,7 +216,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
override fun getSongComparator(isAscending: Boolean): Comparator<Song> =
MultiComparator(
compareByDynamic(isAscending, BasicComparator.ALBUM) { it.album },
compareBy(NullableComparator.INT) { it.disc },
compareBy(NullableComparator.DISC) { it.disc },
compareBy(NullableComparator.INT) { it.track },
compareBy(BasicComparator.SONG))
}
@ -236,7 +237,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
compareByDynamic(isAscending, ListComparator.ARTISTS) { it.artists },
compareByDescending(NullableComparator.DATE_RANGE) { it.album.dates },
compareByDescending(BasicComparator.ALBUM) { it.album },
compareBy(NullableComparator.INT) { it.disc },
compareBy(NullableComparator.DISC) { it.disc },
compareBy(NullableComparator.INT) { it.track },
compareBy(BasicComparator.SONG))
@ -263,7 +264,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
MultiComparator(
compareByDynamic(isAscending, NullableComparator.DATE_RANGE) { it.album.dates },
compareByDescending(BasicComparator.ALBUM) { it.album },
compareBy(NullableComparator.INT) { it.disc },
compareBy(NullableComparator.DISC) { it.disc },
compareBy(NullableComparator.INT) { it.track },
compareBy(BasicComparator.SONG))
@ -342,7 +343,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
override fun getSongComparator(isAscending: Boolean): Comparator<Song> =
MultiComparator(
compareByDynamic(isAscending, NullableComparator.INT) { it.disc },
compareByDynamic(isAscending, NullableComparator.DISC) { it.disc },
compareBy(NullableComparator.INT) { it.track },
compareBy(BasicComparator.SONG))
}
@ -360,7 +361,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
override fun getSongComparator(isAscending: Boolean): Comparator<Song> =
MultiComparator(
compareBy(NullableComparator.INT) { it.disc },
compareBy(NullableComparator.DISC) { it.disc },
compareByDynamic(isAscending, NullableComparator.INT) { it.track },
compareBy(BasicComparator.SONG))
}
@ -545,6 +546,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
val INT = NullableComparator<Int>()
/** A re-usable instance configured for [Long]s. */
val LONG = NullableComparator<Long>()
/** A re-usable instance configured for [Disc]s */
val DISC = NullableComparator<Disc>()
/** A re-usable instance configured for [Date.Range]s. */
val DATE_RANGE = NullableComparator<Date.Range>()
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2023 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.tags
import org.oxycblt.auxio.list.Item
/**
* A disc identifier for a song.
* @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<Disc> {
override fun hashCode() = number.hashCode()
override fun equals(other: Any?) = other is Disc && number == other.number
override fun compareTo(other: Disc) = number.compareTo(other.number)
}

View file

@ -300,7 +300,7 @@ class MediaSessionComponent(private val context: Context, private val listener:
builder.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, it.toLong())
}
song.disc?.let {
builder.putLong(MediaMetadataCompat.METADATA_KEY_DISC_NUMBER, it.toLong())
builder.putLong(MediaMetadataCompat.METADATA_KEY_DISC_NUMBER, it.number.toLong())
}
song.date?.let { builder.putString(MediaMetadataCompat.METADATA_KEY_DATE, it.toString()) }

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@ -11,7 +11,7 @@
android:paddingBottom="@dimen/spacing_mid_medium">
<org.oxycblt.auxio.image.StyledImageView
android:id="@+id/disc_item"
android:id="@+id/disc_icon"
style="@style/Widget.Auxio.Image.Small"
android:scaleType="matrix"
app:layout_constraintBottom_toBottomOf="parent"
@ -21,19 +21,30 @@
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/disc_no"
android:paddingStart="@dimen/spacing_medium"
android:paddingEnd="@dimen/spacing_medium"
android:paddingTop="@dimen/spacing_mid_medium"
android:paddingBottom="@dimen/spacing_mid_medium"
android:textAppearance="@style/TextAppearance.Auxio.TitleMedium"
android:id="@+id/disc_number"
style="@style/Widget.Auxio.TextView.Item.Primary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="@dimen/spacing_mid_medium"
android:textColor="@color/sel_selectable_text_primary"
app:layout_constraintBottom_toTopOf="@+id/disc_name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/disc_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Disc 1" />
<TextView
android:id="@+id/disc_name"
style="@style/Widget.Auxio.TextView.Item.Secondary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_mid_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/disc_item"
app:layout_constraintTop_toTopOf="parent"
tools:text="Disc 16" />
tools:visibility="gone"
app:layout_constraintStart_toEndOf="@+id/disc_icon"
app:layout_constraintTop_toBottomOf="@+id/disc_number"
tools:text="Part 1" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,39 @@
/*
* Copyright (c) 2023 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.tags
import org.junit.Assert.assertTrue
import org.junit.Test
class DiscTest {
@Test
fun disc_equals_correct() {
val a = Disc(1, "Part I")
val b = Disc(1, "Part I")
assertTrue(a == b)
assertTrue(a.hashCode() == b.hashCode())
}
@Test
fun disc_equals_inconsistentNames() {
val a = Disc(1, "Part I")
val b = Disc(1, null)
assertTrue(a == b)
assertTrue(a.hashCode() == b.hashCode())
}
}