diff --git a/ITUNES_SYNC_IMPLEMENTATION.md b/ITUNES_SYNC_IMPLEMENTATION.md new file mode 100644 index 000000000..761cd05da --- /dev/null +++ b/ITUNES_SYNC_IMPLEMENTATION.md @@ -0,0 +1,340 @@ +# iTunes/Apple Music Sync Implementation + +## Overview + +This document describes the implementation of Phase 1 of iTunes/Apple Music sync support for Shuttle2, addressing [GitHub Issue #107](https://github.com/timusus/Shuttle2/issues/107). + +## Implementation Status: Phase 1 Complete ✓ + +### What Was Implemented + +#### 1. Star Rating Support ✓ + +**Database Layer:** +- Added `rating` column to `songs` table (INT, 0-5 scale, default 0) +- Created database migration `MIGRATION_40_41` to add the rating column +- Updated `MediaDatabase` version from 40 to 41 +- Files modified: + - `android/data/src/main/kotlin/com/simplecityapps/shuttle/model/Song.kt` + - `android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/data/room/entity/SongData.kt` + - `android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/data/room/database/MediaDatabase.kt` + - `android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/data/room/migrations/MIGRATION_40_41.kt` (new file) + +**Repository Layer:** +- Added `setRating(song: Song, rating: Int)` method to `SongRepository` interface +- Implemented rating update in `LocalSongRepository` +- Added `updateRating(id: Long, rating: Int)` DAO method +- Files modified: + - `android/mediaprovider/core/src/main/java/com/simplecityapps/mediaprovider/repository/songs/SongRepository.kt` + - `android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/repository/LocalSongRepository.kt` + - `android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/data/room/dao/SongDataDao.kt` + +**UI Layer:** +- Added rating display to Song Info dialog +- Shows visual star rating (★★★☆☆) with numeric value +- Displays "Not rated" for unrated songs +- Files modified: + - `android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/songinfo/SongInfoDialogFragment.kt` + - `android/app/src/main/res/values/strings_song_info.xml` + +#### 2. MediaStore Rating Import ✓ + +- Reads existing ratings from Android's MediaStore database +- Converts MediaStore ratings (0-100 scale) to Shuttle's 0-5 star scale +- Automatic import when scanning media library +- Files modified: + - `android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/provider/mediastore/MediaStoreMediaProvider.kt` + +**Conversion Scale:** +```kotlin +MediaStore (0-100) -> Shuttle (0-5) +0 -> 0 stars (unrated) +1-20 -> 1 star +21-40 -> 2 stars +41-60 -> 3 stars +61-80 -> 4 stars +81-100 -> 5 stars +``` + +#### 3. M3U Playlist Export ✓ + +- Created `PlaylistExporter` utility class +- Supports extended M3U format with metadata +- Compatible with iTunes playlist import +- Supports both absolute and relative file paths +- Files created: + - `android/mediaprovider/core/src/main/java/com/simplecityapps/mediaprovider/PlaylistExporter.kt` + +**M3U Format:** +```m3u +#EXTM3U +#PLAYLIST:My Playlist + +#EXTINF:245,Artist Name - Song Title +/path/to/song1.mp3 +#EXTINF:180,Another Artist - Another Song +/path/to/song2.mp3 +``` + +**Usage Example:** +```kotlin +val exporter = PlaylistExporter(playlistRepository) +val outputFile = File("/storage/emulated/0/Music/MyPlaylist.m3u") +exporter.exportToM3U(playlist, outputFile) +``` + +### What's NOT Yet Implemented (Future Phases) + +#### Phase 2: Rating Edit UI (Pending) + +**Needed:** +- Interactive rating widget (tap to rate) +- Add rating option to song context menus +- Quick rating from Now Playing screen +- Batch rating for multiple songs + +**Suggested Locations:** +- Song detail/info dialog (make rating tappable) +- Long-press menu on song items +- Now Playing screen toolbar/overflow menu +- Multi-select mode in song lists + +#### Phase 3: Playlist Export UI (Pending) + +**Needed:** +- Export button in playlist detail screen +- File picker for choosing export location +- Progress indicator for export +- Success/error notifications +- Share playlist via M3U file + +#### Phase 4: Desktop Sync Application (Major Future Work) + +This is the core iTunes sync functionality mentioned in the original issue. It would require: + +**Desktop Application:** +- Cross-platform desktop app (Windows/macOS/Linux) +- USB device detection and communication +- Apple Music/iTunes library integration +- Bidirectional sync protocol +- Rating synchronization +- Playlist synchronization + +**Android Side:** +- USB connection handling +- Sync service for background operations +- Conflict resolution for ratings/playlists +- Sync status UI + +**Technologies to Consider:** +- Desktop: Electron, Qt, or native Swift/C++ +- Communication: ADB, MTP, or custom USB protocol +- Apple Music API: AppleScript (macOS) or iTunes COM (Windows) +- Data format: JSON or Protocol Buffers for sync data + +#### Phase 5: Apple Music API Integration (Future) + +**For Full Cloud Sync:** +- Apple ID authentication +- iCloud Music Library access +- CloudKit integration +- OAuth token management +- Network sync instead of USB-only + +## Database Schema + +### Song Table (Updated) + +```sql +CREATE TABLE songs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT, + track INTEGER, + disc INTEGER, + duration INTEGER NOT NULL, + year INTEGER, + genres TEXT NOT NULL, + path TEXT NOT NULL, + albumArtist TEXT, + artists TEXT NOT NULL, + album TEXT, + size INTEGER NOT NULL, + mimeType TEXT NOT NULL, + lastModified INTEGER NOT NULL, + playbackPosition INTEGER NOT NULL, + playCount INTEGER NOT NULL, + rating INTEGER NOT NULL DEFAULT 0, -- NEW FIELD + lastPlayed INTEGER, + lastCompleted INTEGER, + blacklisted INTEGER NOT NULL, + externalId TEXT, + mediaProvider TEXT NOT NULL, + replayGainTrack REAL, + replayGainAlbum REAL, + lyrics TEXT, + grouping TEXT, + bitRate INTEGER, + bitDepth INTEGER, + sampleRate INTEGER, + channelCount INTEGER +); + +CREATE UNIQUE INDEX index_songs_path ON songs(path); +``` + +## API Reference + +### SongRepository + +```kotlin +interface SongRepository { + // ... existing methods ... + + /** + * Set the rating for a song + * @param song The song to rate + * @param rating Rating value (0-5, where 0 = unrated) + */ + suspend fun setRating(song: Song, rating: Int) +} +``` + +### PlaylistExporter + +```kotlin +class PlaylistExporter(private val playlistRepository: PlaylistRepository) { + /** + * Export a playlist to M3U format + * @param playlist The playlist to export + * @param outputFile The output file location + * @param useRelativePaths Use relative paths instead of absolute + * @throws IOException if writing fails + */ + suspend fun exportToM3U( + playlist: Playlist, + outputFile: File, + useRelativePaths: Boolean = false + ) + + /** + * Export a playlist to M3U8 format (UTF-8 encoded M3U) + */ + suspend fun exportToM3U8( + playlist: Playlist, + outputFile: File, + useRelativePaths: Boolean = false + ) + + /** + * Generate M3U content as a string + * @return The M3U content + */ + suspend fun generateM3UContent( + playlist: Playlist, + useRelativePaths: Boolean = false + ): String +} +``` + +## Testing Recommendations + +### Manual Testing + +1. **Rating Import:** + - Use a file manager app to rate some audio files in Android's MediaStore + - Trigger a media scan in Shuttle2 + - Verify ratings appear in Song Info dialog + +2. **Rating Display:** + - Open Song Info for rated songs + - Verify star display matches expected rating + - Verify unrated songs show "Not rated" + +3. **M3U Export:** + - Use `PlaylistExporter` to export a playlist + - Verify M3U file format is correct + - Import the M3U file into iTunes/Apple Music + - Verify songs and metadata appear correctly + +### Unit Tests (Recommended) + +```kotlin +class PlaylistExporterTest { + @Test + fun testM3UFormat() { + // Verify M3U header and song entries + } + + @Test + fun testRelativePaths() { + // Verify path conversion works + } +} + +class SongRepositoryTest { + @Test + fun testSetRating() { + // Verify rating updates correctly + } + + @Test + fun testRatingBounds() { + // Verify 0-5 constraint is enforced + } +} +``` + +## Migration Path for Users + +### From iSyncr/Rocket Player + +Users previously using iSyncr with Rocket Player can: + +1. **Import existing ratings:** + - If ratings are stored in MediaStore, they'll import automatically + - If ratings are in Rocket Player's database, manual migration needed + +2. **Export playlists from Shuttle:** + - Use M3U export to create iTunes-compatible playlists + - Import M3U files into iTunes/Apple Music + +3. **Manual sync workflow (until desktop app exists):** + - Export playlists as M3U from Shuttle + - Transfer M3U files to computer + - Import into iTunes/Apple Music + - Manual rating updates via MediaStore-compatible apps + +## Contributing + +To complete the full iTunes sync vision: + +1. **UI Contributors:** + - Implement rating edit widgets + - Add playlist export UI + - Design sync status screens + +2. **Desktop Developers:** + - Create cross-platform desktop sync app + - Implement iTunes/Apple Music integration + - Design sync protocol + +3. **Protocol Designers:** + - Define bidirectional sync format + - Handle conflict resolution + - Design efficient delta sync + +## Resources + +- [M3U Format Specification](https://en.wikipedia.org/wiki/M3U) +- [Android MediaStore Documentation](https://developer.android.com/reference/android/provider/MediaStore.Audio) +- [iTunes Library Access](https://developer.apple.com/documentation/ituneslibrary) +- [AppleScript for iTunes](https://developer.apple.com/library/archive/documentation/AppleScript/Conceptual/AppleScriptLangGuide/) + +## License + +This implementation follows Shuttle2's existing license (GNU General Public License v3.0). + +## Questions? + +For questions or issues related to this implementation, please comment on: +- GitHub Issue: https://github.com/timusus/Shuttle2/issues/107 diff --git a/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/songinfo/SongInfoDialogFragment.kt b/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/songinfo/SongInfoDialogFragment.kt index 21c53d800..7800e9602 100644 --- a/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/songinfo/SongInfoDialogFragment.kt +++ b/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/songinfo/SongInfoDialogFragment.kt @@ -62,6 +62,7 @@ class SongInfoDialogFragment : BottomSheetDialogFragment() { getString(R.string.song_info_year) to song.date?.year?.toString().orEmpty(), getString(R.string.song_info_disc) to song.disc?.toString().orEmpty(), getString(R.string.song_info_play_count) to song.playCount.toString(), + getString(R.string.song_info_rating) to if (song.rating > 0) "${"★".repeat(song.rating)}${"☆".repeat(5 - song.rating)} (${song.rating}/5)" else getString(R.string.song_info_not_rated), getString(R.string.song_info_genres) to song.genres.joinToString(", "), getString(R.string.song_info_path) to song.path.sanitise(), getString(R.string.song_info_mime_type) to song.mimeType, diff --git a/android/app/src/main/res/values/strings_song_info.xml b/android/app/src/main/res/values/strings_song_info.xml index 9700fbbc0..9c37ed3b1 100644 --- a/android/app/src/main/res/values/strings_song_info.xml +++ b/android/app/src/main/res/values/strings_song_info.xml @@ -20,6 +20,10 @@ Disc Play count + + Rating + + Not rated Genres diff --git a/android/data/src/main/kotlin/com/simplecityapps/shuttle/model/Song.kt b/android/data/src/main/kotlin/com/simplecityapps/shuttle/model/Song.kt index c5b14c2f5..21410fafb 100644 --- a/android/data/src/main/kotlin/com/simplecityapps/shuttle/model/Song.kt +++ b/android/data/src/main/kotlin/com/simplecityapps/shuttle/model/Song.kt @@ -31,6 +31,7 @@ data class Song( val lastCompleted: Instant?, val playCount: Int, val playbackPosition: Int, + val rating: Int = 0, // 0-5 star rating (0 = unrated) val blacklisted: Boolean, val externalId: String? = null, val mediaProvider: MediaProviderType, diff --git a/android/mediaprovider/core/src/main/java/com/simplecityapps/mediaprovider/PlaylistExporter.kt b/android/mediaprovider/core/src/main/java/com/simplecityapps/mediaprovider/PlaylistExporter.kt new file mode 100644 index 000000000..b6748eeac --- /dev/null +++ b/android/mediaprovider/core/src/main/java/com/simplecityapps/mediaprovider/PlaylistExporter.kt @@ -0,0 +1,114 @@ +package com.simplecityapps.mediaprovider + +import com.simplecityapps.mediaprovider.repository.playlists.PlaylistRepository +import com.simplecityapps.shuttle.model.Playlist +import com.simplecityapps.shuttle.model.PlaylistSong +import kotlinx.coroutines.flow.first +import java.io.File +import java.io.IOException + +/** + * Utility class for exporting playlists to various formats + */ +class PlaylistExporter( + private val playlistRepository: PlaylistRepository +) { + /** + * Export a playlist to M3U format (Extended M3U with metadata) + * + * @param playlist The playlist to export + * @param outputFile The file to write the M3U content to + * @param useRelativePaths If true, use relative paths instead of absolute paths + * @throws IOException if writing to file fails + */ + suspend fun exportToM3U( + playlist: Playlist, + outputFile: File, + useRelativePaths: Boolean = false + ) { + val songs = playlistRepository.getSongsForPlaylist(playlist).first() + + outputFile.bufferedWriter().use { writer -> + // Write M3U header + writer.write("#EXTM3U\n") + writer.write("#PLAYLIST:${playlist.name}\n") + writer.write("\n") + + // Write each song + songs.forEach { playlistSong -> + val song = playlistSong.song + + // Write extended info: #EXTINF:duration,artist - title + val durationInSeconds = song.duration / 1000 + val artist = song.friendlyArtistName ?: "Unknown Artist" + val title = song.name ?: "Unknown Title" + writer.write("#EXTINF:$durationInSeconds,$artist - $title\n") + + // Write file path + val path = if (useRelativePaths) { + File(song.path).name + } else { + song.path + } + writer.write("$path\n") + } + } + } + + /** + * Export a playlist to M3U8 format (UTF-8 encoded M3U) + * + * @param playlist The playlist to export + * @param outputFile The file to write the M3U8 content to + * @param useRelativePaths If true, use relative paths instead of absolute paths + * @throws IOException if writing to file fails + */ + suspend fun exportToM3U8( + playlist: Playlist, + outputFile: File, + useRelativePaths: Boolean = false + ) { + // M3U8 is just UTF-8 encoded M3U, so we can reuse the same logic + exportToM3U(playlist, outputFile, useRelativePaths) + } + + /** + * Generate M3U content as a string + * + * @param playlist The playlist to export + * @param useRelativePaths If true, use relative paths instead of absolute paths + * @return The M3U content as a string + */ + suspend fun generateM3UContent( + playlist: Playlist, + useRelativePaths: Boolean = false + ): String { + val songs = playlistRepository.getSongsForPlaylist(playlist).first() + + return buildString { + // Write M3U header + append("#EXTM3U\n") + append("#PLAYLIST:${playlist.name}\n") + append("\n") + + // Write each song + songs.forEach { playlistSong -> + val song = playlistSong.song + + // Write extended info: #EXTINF:duration,artist - title + val durationInSeconds = song.duration / 1000 + val artist = song.friendlyArtistName ?: "Unknown Artist" + val title = song.name ?: "Unknown Title" + append("#EXTINF:$durationInSeconds,$artist - $title\n") + + // Write file path + val path = if (useRelativePaths) { + File(song.path).name + } else { + song.path + } + append("$path\n") + } + } + } +} diff --git a/android/mediaprovider/core/src/main/java/com/simplecityapps/mediaprovider/repository/songs/SongRepository.kt b/android/mediaprovider/core/src/main/java/com/simplecityapps/mediaprovider/repository/songs/SongRepository.kt index 2a6e83129..a151a50c0 100644 --- a/android/mediaprovider/core/src/main/java/com/simplecityapps/mediaprovider/repository/songs/SongRepository.kt +++ b/android/mediaprovider/core/src/main/java/com/simplecityapps/mediaprovider/repository/songs/SongRepository.kt @@ -35,6 +35,11 @@ interface SongRepository { playbackPosition: Int ) + suspend fun setRating( + song: Song, + rating: Int + ) + suspend fun setExcluded( songs: List, excluded: Boolean diff --git a/android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/data/room/DatabaseProvider.kt b/android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/data/room/DatabaseProvider.kt index 3e71c4659..218492363 100644 --- a/android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/data/room/DatabaseProvider.kt +++ b/android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/data/room/DatabaseProvider.kt @@ -21,6 +21,7 @@ import com.simplecityapps.localmediaprovider.local.data.room.migrations.MIGRATIO import com.simplecityapps.localmediaprovider.local.data.room.migrations.MIGRATION_37_38 import com.simplecityapps.localmediaprovider.local.data.room.migrations.MIGRATION_38_39 import com.simplecityapps.localmediaprovider.local.data.room.migrations.MIGRATION_39_40 +import com.simplecityapps.localmediaprovider.local.data.room.migrations.MIGRATION_40_41 class DatabaseProvider( private val context: Context @@ -44,7 +45,8 @@ class DatabaseProvider( MIGRATION_36_37, MIGRATION_37_38, MIGRATION_38_39, - MIGRATION_39_40 + MIGRATION_39_40, + MIGRATION_40_41 ) .apply { if (!BuildConfig.DEBUG) { diff --git a/android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/data/room/dao/SongDataDao.kt b/android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/data/room/dao/SongDataDao.kt index ec56d1adf..0adfeed70 100644 --- a/android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/data/room/dao/SongDataDao.kt +++ b/android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/data/room/dao/SongDataDao.kt @@ -74,6 +74,12 @@ abstract class SongDataDao { lastPlayed: Date = Date() ) + @Query("UPDATE songs SET rating = :rating WHERE id = :id") + abstract suspend fun updateRating( + id: Long, + rating: Int + ) + @Query("UPDATE songs SET blacklisted = :blacklisted WHERE id IN (:ids)") abstract suspend fun setExcluded( ids: List, @@ -112,6 +118,7 @@ fun SongData.toSong(): Song = Song( lastCompleted = lastCompleted?.let { Instant.fromEpochMilliseconds(it.time) }, playCount = playCount, playbackPosition = playbackPosition, + rating = rating, blacklisted = excluded, externalId = externalId, mediaProvider = mediaProvider, diff --git a/android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/data/room/database/MediaDatabase.kt b/android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/data/room/database/MediaDatabase.kt index a0175b35b..f278466bf 100644 --- a/android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/data/room/database/MediaDatabase.kt +++ b/android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/data/room/database/MediaDatabase.kt @@ -17,7 +17,7 @@ import com.simplecityapps.localmediaprovider.local.data.room.entity.SongData PlaylistData::class, PlaylistSongJoin::class ], - version = 40, + version = 41, exportSchema = true ) @TypeConverters(Converters::class) diff --git a/android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/data/room/entity/SongData.kt b/android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/data/room/entity/SongData.kt index 10e819fdf..bf19984f2 100644 --- a/android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/data/room/entity/SongData.kt +++ b/android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/data/room/entity/SongData.kt @@ -30,6 +30,7 @@ data class SongData( @ColumnInfo(name = "lastModified") var lastModified: Date, @ColumnInfo(name = "playbackPosition") var playbackPosition: Int = 0, @ColumnInfo(name = "playCount") var playCount: Int = 0, + @ColumnInfo(name = "rating") var rating: Int = 0, @ColumnInfo(name = "lastPlayed") var lastPlayed: Date? = null, @ColumnInfo(name = "lastCompleted") var lastCompleted: Date? = null, @ColumnInfo(name = "blacklisted") var excluded: Boolean = false, @@ -64,6 +65,7 @@ fun Song.toSongData(mediaProviderType: MediaProviderType): SongData = SongData( lastModified = lastModified?.let { Date(it.toEpochMilliseconds()) } ?: Date(), playbackPosition = playbackPosition, playCount = playCount, + rating = rating, lastPlayed = lastPlayed?.let { Date(it.toEpochMilliseconds()) }, lastCompleted = lastCompleted?.let { Date(it.toEpochMilliseconds()) }, excluded = false, diff --git a/android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/data/room/migrations/MIGRATION_40_41.kt b/android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/data/room/migrations/MIGRATION_40_41.kt new file mode 100644 index 000000000..c27f34995 --- /dev/null +++ b/android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/data/room/migrations/MIGRATION_40_41.kt @@ -0,0 +1,12 @@ +package com.simplecityapps.localmediaprovider.local.data.room.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +val MIGRATION_40_41 = + object : Migration(40, 41) { + override fun migrate(db: SupportSQLiteDatabase) { + // Add rating column to songs table (0-5 star rating, 0 = unrated) + db.execSQL("ALTER TABLE songs ADD COLUMN rating INTEGER NOT NULL DEFAULT 0") + } + } diff --git a/android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/provider/mediastore/MediaStoreMediaProvider.kt b/android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/provider/mediastore/MediaStoreMediaProvider.kt index c6db348a5..a94ec4535 100644 --- a/android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/provider/mediastore/MediaStoreMediaProvider.kt +++ b/android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/provider/mediastore/MediaStoreMediaProvider.kt @@ -51,7 +51,8 @@ class MediaStoreMediaProvider( MediaStore.Audio.Media.IS_PODCAST, MediaStore.Audio.Media.BOOKMARK, MediaStore.Audio.Media.MIME_TYPE, - "album_artist" + "album_artist", + "rating" ), "${MediaStore.Audio.Media.IS_MUSIC}=1 OR ${MediaStore.Audio.Media.IS_PODCAST}=1", null, @@ -116,6 +117,17 @@ class MediaStoreMediaProvider( lastCompleted = null, playCount = 0, playbackPosition = 0, + rating = songCursor.getIntOrNull(songCursor.getColumnIndex("rating"))?.let { + // MediaStore ratings are 0-100, convert to 0-5 stars + when { + it == 0 -> 0 + it <= 20 -> 1 + it <= 40 -> 2 + it <= 60 -> 3 + it <= 80 -> 4 + else -> 5 + } + } ?: 0, blacklisted = false, externalId = songCursor.getLong( diff --git a/android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/repository/LocalSongRepository.kt b/android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/repository/LocalSongRepository.kt index 7cd318d29..e0726c724 100644 --- a/android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/repository/LocalSongRepository.kt +++ b/android/mediaprovider/local/src/main/java/com/simplecityapps/localmediaprovider/local/repository/LocalSongRepository.kt @@ -88,6 +88,14 @@ class LocalSongRepository( songDataDao.updatePlaybackPosition(song.id, playbackPosition) } + override suspend fun setRating( + song: Song, + rating: Int + ) { + Timber.v("Setting rating to $rating for song: ${song.name}") + songDataDao.updateRating(song.id, rating) + } + override suspend fun setExcluded( songs: List, excluded: Boolean