Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ val AppBarHeight = 64.dp
val ListItemHeight = 64.dp
val SuggestionItemHeight = 56.dp
val SearchFilterHeight = 48.dp
val ListThumbnailSize = 48.dp
val ListThumbnailSize = 40.dp
val GridThumbnailHeight = 128.dp
val SmallGridThumbnailHeight = 92.dp

Expand Down
38 changes: 26 additions & 12 deletions app/src/main/java/com/zionhuang/music/db/DatabaseDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import com.zionhuang.music.db.entities.Song
import com.zionhuang.music.db.entities.SongAlbumMap
import com.zionhuang.music.db.entities.SongArtistMap
import com.zionhuang.music.db.entities.SongEntity
import android.util.Log
import com.zionhuang.music.extensions.reversed
import com.zionhuang.music.extensions.toSQLiteQuery
import com.zionhuang.music.models.MediaMetadata
Expand Down Expand Up @@ -600,18 +601,31 @@ interface DatabaseDao {
)
fun relatedSongs(songId: String): List<Song>

@Query(
"""
UPDATE playlist_song_map SET position =
CASE
WHEN position < :fromPosition THEN position + 1
WHEN position > :fromPosition THEN position - 1
ELSE :toPosition
END
WHERE playlistId = :playlistId AND position BETWEEN MIN(:fromPosition, :toPosition) AND MAX(:fromPosition, :toPosition)
"""
)
fun move(playlistId: String, fromPosition: Int, toPosition: Int)
@Query("SELECT * FROM playlist_song_map WHERE playlistId = :playlistId ORDER BY position ASC")
fun getPlaylistSongMapsOrderedByPosition(playlistId: String): MutableList<PlaylistSongMap>

@Transaction
fun moveItemInPlaylist(playlistId: String, fromPosition: Int, toPosition: Int) {
if (fromPosition == toPosition) return

val items = getPlaylistSongMapsOrderedByPosition(playlistId)

if (fromPosition < 0 || fromPosition >= items.size) {
Log.e("DatabaseDao", "Invalid fromPosition for move: $fromPosition, current items size: ${items.size}, for playlistId: $playlistId")
return
}

val movedItem = items.removeAt(fromPosition)

val actualToPosition = if (toPosition < 0) 0 else if (toPosition > items.size) items.size else toPosition
items.add(actualToPosition, movedItem)

items.forEachIndexed { index, map ->
if (map.position != index) {
update(map.copy(position = index))
}
}
}

@Query("DELETE FROM playlist_song_map WHERE playlistId = :playlistId")
fun clearPlaylist(playlistId: String)
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/com/zionhuang/music/db/entities/SongEntity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import java.time.LocalDateTime
import android.util.Log

@Immutable
@Entity(
Expand All @@ -26,10 +27,14 @@ data class SongEntity(
val totalPlayTime: Long = 0, // in milliseconds
val inLibrary: LocalDateTime? = null,
) {
fun toggleLike() = copy(
liked = !liked,
inLibrary = if (!liked) inLibrary ?: LocalDateTime.now() else inLibrary
)
fun toggleLike(): SongEntity {
val newLikedStatus = !liked
Log.d("SongEntity", "SongEntity.toggleLike for id: $id. New liked status: $newLikedStatus. Old liked status: $liked")
return copy(
liked = newLikedStatus,
inLibrary = if (newLikedStatus) inLibrary ?: LocalDateTime.now() else inLibrary
)
}

fun toggleLibrary() = copy(inLibrary = if (inLibrary == null) LocalDateTime.now() else null)
}
47 changes: 29 additions & 18 deletions app/src/main/java/com/zionhuang/music/playback/DownloadUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.zionhuang.music.di.DownloadCache
import com.zionhuang.music.di.PlayerCache
import com.zionhuang.music.utils.enumPreference
import dagger.hilt.android.qualifiers.ApplicationContext
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
Expand Down Expand Up @@ -63,7 +64,7 @@ class DownloadUtil @Inject constructor(
return@Factory dataSpec
}

songUrlCache[mediaId]?.takeIf { it.second < System.currentTimeMillis() }?.let {
songUrlCache[mediaId]?.takeIf { it.second > System.currentTimeMillis() }?.let {
return@Factory dataSpec.withUri(it.first.toUri())
}

Expand All @@ -72,26 +73,36 @@ class DownloadUtil @Inject constructor(
YouTube.player(mediaId)
}.getOrThrow()
if (playerResponse.playabilityStatus.status != "OK") {
throw PlaybackException(playerResponse.playabilityStatus.reason, null, PlaybackException.ERROR_CODE_REMOTE_ERROR)
}
val status = playerResponse.playabilityStatus.status
val reason = playerResponse.playabilityStatus.reason
Log.w("DownloadUtil", "Download failed for mediaId: $mediaId. PlayabilityStatus: $status, Reason: $reason")

val format =
if (playedFormat != null) {
playerResponse.streamingData?.adaptiveFormats?.find { it.itag == playedFormat.itag }
// Potentially map specific statuses to more user-friendly messages or specific exception types in the future.
// For now, use the reason provided by YouTube, or a generic message if reason is blank.
val messageToThrow = if (reason.isNullOrBlank()) {
"Download unavailable: $status" // Or a generic R.string.error_download_unavailable
} else {
playerResponse.streamingData?.adaptiveFormats
?.filter { it.isAudio }
?.maxByOrNull {
it.bitrate * when (audioQuality) {
AudioQuality.AUTO -> if (connectivityManager.isActiveNetworkMetered) -1 else 1
AudioQuality.HIGH -> 1
AudioQuality.LOW -> -1
} + (if (it.mimeType.startsWith("audio/webm")) 10240 else 0) // prefer opus stream
}
}!!.let {
// Specify range to avoid YouTube's throttling
it.copy(url = "${it.url}&range=0-${it.contentLength ?: 10000000}")
reason
}
throw PlaybackException(messageToThrow, null, PlaybackException.ERROR_CODE_REMOTE_ERROR)
}

val format = (if (playedFormat != null) {
playerResponse.streamingData?.adaptiveFormats?.find { it.itag == playedFormat.itag }
} else {
playerResponse.streamingData?.adaptiveFormats
?.filter { it.isAudio && !it.url.isNullOrBlank() && it.mimeType.isNotBlank() } // Added checks
?.maxByOrNull {
it.bitrate * when (audioQuality) {
AudioQuality.AUTO -> if (connectivityManager.isActiveNetworkMetered) -1 else 1
AudioQuality.HIGH -> 1
AudioQuality.LOW -> -1
} + (if (it.mimeType.startsWith("audio/webm")) 10240 else 0) // prefer opus stream
}
})?.let { // Note: changed from !! to ?.let to handle potential null from find or maxByOrNull
// Specify range to avoid YouTube's throttling
it.copy(url = "${it.url}&range=0-${it.contentLength ?: 10000000}")
} ?: throw PlaybackException("No suitable download format found or format missing URL/MimeType.", null, PlaybackException.ERROR_CODE_IO_UNSPECIFIED) // Or a more specific error

database.query {
upsert(
Expand Down
13 changes: 11 additions & 2 deletions app/src/main/java/com/zionhuang/music/playback/MusicService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import androidx.media3.session.DefaultMediaNotificationProvider
import androidx.media3.session.MediaController
import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession
import android.util.Log
import androidx.media3.session.SessionToken
import com.google.common.util.concurrent.MoreExecutors
import com.zionhuang.innertube.YouTube
Expand Down Expand Up @@ -654,7 +655,15 @@ class MusicService : MediaLibraryService(),
}
}
if (playerResponse.playabilityStatus.status != "OK") {
throw PlaybackException(playerResponse.playabilityStatus.reason, null, PlaybackException.ERROR_CODE_REMOTE_ERROR)
val reason = playerResponse.playabilityStatus.reason
// Log the detailed status and reason for better debugging
Log.w("MusicService", "Playback failed for mediaId: $mediaId. Status: ${playerResponse.playabilityStatus.status}, Reason: $reason")
val messageToUser = if (reason.isNullOrBlank()) {
getString(R.string.error_no_stream)
} else {
reason
}
throw PlaybackException(messageToUser, null, PlaybackException.ERROR_CODE_REMOTE_ERROR)
}

val format =
Expand All @@ -665,7 +674,7 @@ class MusicService : MediaLibraryService(),
}
} else {
playerResponse.streamingData?.adaptiveFormats
?.filter { it.isAudio }
?.filter { it.isAudio && !it.url.isNullOrBlank() && it.mimeType.isNotBlank() } // Added checks
?.maxByOrNull {
it.bitrate * when (audioQuality) {
AudioQuality.AUTO -> if (connectivityManager.isActiveNetworkMetered) -1 else 1
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/java/com/zionhuang/music/ui/player/Player.kt
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ fun BottomSheetPlayer(
useDarkTheme && pureBlack
}
val backgroundColor = if (useBlackBackground && state.value > state.collapsedBound) {
lerp(MaterialTheme.colorScheme.surfaceContainer, Color.Black, state.progress)
// Use a very dark grey as the starting point for the lerp when pureBlack is active
// to ensure a visible gradient effect during sheet expansion.
lerp(Color(0xFF1F1F1F), Color.Black, state.progress)
} else {
MaterialTheme.colorScheme.surfaceContainer
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.fastForEachIndexed
import android.util.Log
import androidx.core.net.toUri
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.media3.exoplayer.offline.Download
Expand Down Expand Up @@ -233,6 +234,9 @@ fun AlbumScreen(
Row {
IconButton(
onClick = {
val albumId = albumWithSongs.album.id // Assuming albumWithSongs is not null here
val currentBookmarkStatus = albumWithSongs.album.bookmarkedAt
Log.d("AlbumScreen", "Album like toggled for albumId: $albumId. Current bookmarkedAt: $currentBookmarkStatus. New bookmarkedAt will be: ${if (currentBookmarkStatus != null) null else "not null"}")
database.query {
update(albumWithSongs.album.toggleLike())
}
Expand Down
Loading