diff --git a/.github/workflows/github_release.yml b/.github/workflows/github_release.yml index afa74d8b..b116f3b2 100644 --- a/.github/workflows/github_release.yml +++ b/.github/workflows/github_release.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup JDK 17 - uses: actions/setup-java@v2 + uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: '17' @@ -28,6 +28,13 @@ jobs: - name: Make gradlew executable run: chmod +x ./gradlew + - name: Setup build tool version variable + shell: bash + run: | + BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1) + echo "BUILD_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV + echo Last build tool version is: $BUILD_TOOL_VERSION + - name: Build APK id: build run: bash ./gradlew assembleTempoRelease @@ -41,6 +48,8 @@ jobs: alias: ${{ secrets.KEY_ALIAS_GITHUB }} keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }} keyPassword: ${{ secrets.KEY_PASSWORD_GITHUB }} + env: + BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }} - name: Make artifact uses: actions/upload-artifact@v2 @@ -48,34 +57,6 @@ jobs: name: app-release-signed path: ${{steps.sign_apk.outputs.signedReleaseFile}} -# - name: Build AAB -# run: bash ./gradlew bundleRelease - -# - name: Sign AAB -# id: sign_aab -# uses: r0adkll/sign-android-release@v1 -# with: -# releaseDirectory: app/build/outputs/bundle/release -# signingKeyBase64: ${{ secrets.KEYSTORE_BASE64 }} -# alias: ${{ secrets.KEY_ALIAS_GITHUB }} -# keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }} -# keyPassword: ${{ secrets.KEY_PASSWORD_GITHUB }} - -# - name: Make artifact -# uses: actions/upload-artifact@v2 -# with: -# name: app-release-signed -# path: ${{steps.sign_aab.outputs.signedReleaseFile}} - -# - name: Build Changelog -# id: changelog -# uses: ardalanamini/auto-changelog@v3 -# with: -# mention-authors: false -# mention-new-contributors: false -# include-compare: false -# semver: false - - name: Create Release id: create_release uses: actions/create-release@v1 @@ -83,7 +64,6 @@ jobs: tag_name: ${{ github.ref }} release_name: Release v${{ github.ref }} body: '> Changelog coming soon' -# body: ${{ steps.changelog.outputs.changelog }} env: GITHUB_TOKEN: ${{ github.token }} @@ -96,13 +76,3 @@ jobs: asset_path: ${{steps.sign_apk.outputs.signedReleaseFile}} asset_name: app-tempo-release.apk asset_content_type: application/zip - -# - name: Upload AAB -# uses: actions/upload-release-asset@v1 -# env: -# GITHUB_TOKEN: ${{ github.token }} -# with: -# upload_url: ${{ steps.create_release.outputs.upload_url }} -# asset_path: ${{steps.sign_aab.outputs.signedReleaseFile}} -# asset_name: app-release.aab -# asset_content_type: application/zip diff --git a/.idea/gradle.xml b/.idea/gradle.xml index a33cee77..0897082f 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,16 +4,15 @@ diff --git a/.idea/misc.xml b/.idea/misc.xml index 3467fa48..d3c08545 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -191,7 +191,7 @@ - + diff --git a/README.md b/README.md index 1380fb2b..349c9187 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,12 @@ Access your music library on all your android devices

+

+ + + +

+ **Tempo** is an open-source and lightweight music client for Subsonic, designed and built natively for Android. It provides a seamless and intuitive music streaming experience, allowing you to access and play your Subsonic music library directly from your Android device. Tempo does not rely on magic algorithms to decide what you should listen to. Instead, the interface is built around your listening history, randomness, and optionally integrates with services like Last.fm to personalize your music experience. diff --git a/app/build.gradle b/app/build.gradle index 7267fd4a..2e421ade 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,12 +4,15 @@ apply plugin: 'kotlin-parcelize' android { compileSdk = 34 - buildToolsVersion = "34.0.0" + buildToolsVersion = '34.0.0' defaultConfig { minSdkVersion 24 targetSdkVersion 34 + versionCode 25 + versionName '3.8.1' + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' javaCompileOptions { @@ -28,15 +31,11 @@ android { tempo { dimension = "default" applicationId 'com.cappielloantonio.tempo' - versionCode 22 - versionName '3.5.8' } notquitemy { dimension = "default" applicationId "com.cappielloantonio.notquitemy.tempo" - versionCode 1 - versionName "1.0.0" } } @@ -66,14 +65,12 @@ android { } dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) - // AndroidX implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0' implementation 'androidx.preference:preference-ktx:1.2.1' - implementation 'androidx.navigation:navigation-fragment-ktx:2.7.5' - implementation 'androidx.navigation:navigation-ui-ktx:2.7.5' + implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7' + implementation 'androidx.navigation:navigation-ui-ktx:2.7.7' implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'androidx.room:room-runtime:2.6.1' implementation 'androidx.core:core-splashscreen:1.0.1' @@ -87,17 +84,17 @@ dependencies { implementation 'com.github.bumptech.glide:annotations:4.16.0' // Media3 - implementation 'androidx.media3:media3-session:1.2.0' - implementation 'androidx.media3:media3-common:1.2.0' - implementation 'androidx.media3:media3-exoplayer:1.2.0' - implementation 'androidx.media3:media3-ui:1.2.0' - tempoImplementation 'androidx.media3:media3-cast:1.2.0' + implementation 'androidx.media3:media3-session:1.3.1' + implementation 'androidx.media3:media3-common:1.3.1' + implementation 'androidx.media3:media3-exoplayer:1.3.1' + implementation 'androidx.media3:media3-ui:1.3.1' + tempoImplementation 'androidx.media3:media3-cast:1.3.1' annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0' annotationProcessor 'androidx.room:room-compiler:2.6.1' // Retrofit - implementation 'com.squareup.retrofit2:retrofit:2.9.0' - implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11' - implementation 'com.squareup.retrofit2:converter-gson:2.9.0' + implementation 'com.squareup.retrofit2:retrofit:2.11.0' + implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.14' + implementation 'com.squareup.retrofit2:converter-gson:2.11.0' } \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 2ef25c5f..48b1f068 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -22,4 +22,7 @@ -keepattributes SourceFile, LineNumberTable -keep public class * extends java.lang.Exception --keep class retrofit2.** { *; } \ No newline at end of file +-keep class retrofit2.** { *; } + +-keep class **.reflect.TypeToken { *; } +-keep class * extends **.reflect.TypeToken \ No newline at end of file diff --git a/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/4.json b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/4.json new file mode 100644 index 00000000..5356ab94 --- /dev/null +++ b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/4.json @@ -0,0 +1,997 @@ +{ + "formatVersion": 1, + "database": { + "version": 4, + "identityHash": "528d037bee0f0575f8e0670ae1b04e00", + "entities": [ + { + "tableName": "queue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `track_order` INTEGER NOT NULL, `last_play` INTEGER NOT NULL, `playing_changed` INTEGER NOT NULL, `stream_id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`track_order`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "trackOrder", + "columnName": "track_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastPlay", + "columnName": "last_play", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playingChanged", + "columnName": "playing_changed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "track", + "columnName": "track", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "genre", + "columnName": "genre", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "track_order" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "server", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_name` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `address` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `low_security` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLowSecurity", + "columnName": "low_security", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recent_search", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`search` TEXT NOT NULL, PRIMARY KEY(`search`))", + "fields": [ + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "search" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "download", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlist_id` TEXT, `playlist_name` TEXT, `download_state` INTEGER NOT NULL DEFAULT 1, `download_uri` TEXT DEFAULT '', `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "playlistName", + "columnName": "playlist_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "downloadState", + "columnName": "download_state", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "downloadUri", + "columnName": "download_uri", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "track", + "columnName": "track", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "genre", + "columnName": "genre", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "chronology", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `server` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "server", + "columnName": "server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "track", + "columnName": "track", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "genre", + "columnName": "genre", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "favorite", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `songId` TEXT, `albumId` TEXT, `artistId` TEXT, `toStar` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "toStar", + "columnName": "toStar", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "timestamp" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "session_media_item", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "track", + "columnName": "track", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "genre", + "columnName": "genre", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '528d037bee0f0575f8e0670ae1b04e00')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/5.json b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/5.json new file mode 100644 index 00000000..e8e7e86c --- /dev/null +++ b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/5.json @@ -0,0 +1,1004 @@ +{ + "formatVersion": 1, + "database": { + "version": 5, + "identityHash": "0e65e1c3fb44d9dc04c9c6cf35b7ea58", + "entities": [ + { + "tableName": "queue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `track_order` INTEGER NOT NULL, `last_play` INTEGER NOT NULL, `playing_changed` INTEGER NOT NULL, `stream_id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`track_order`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "trackOrder", + "columnName": "track_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastPlay", + "columnName": "last_play", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playingChanged", + "columnName": "playing_changed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "track", + "columnName": "track", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "genre", + "columnName": "genre", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "track_order" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "server", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_name` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `address` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `low_security` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLowSecurity", + "columnName": "low_security", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recent_search", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`search` TEXT NOT NULL, PRIMARY KEY(`search`))", + "fields": [ + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "search" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "download", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlist_id` TEXT, `playlist_name` TEXT, `download_state` INTEGER NOT NULL DEFAULT 1, `download_uri` TEXT DEFAULT '', `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "playlistName", + "columnName": "playlist_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "downloadState", + "columnName": "download_state", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "downloadUri", + "columnName": "download_uri", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "track", + "columnName": "track", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "genre", + "columnName": "genre", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "chronology", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `server` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "server", + "columnName": "server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "track", + "columnName": "track", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "genre", + "columnName": "genre", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "favorite", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `songId` TEXT, `albumId` TEXT, `artistId` TEXT, `toStar` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "toStar", + "columnName": "toStar", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "timestamp" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "session_media_item", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `index` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL DEFAULT 0, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "track", + "columnName": "track", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "genre", + "columnName": "genre", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "index" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0e65e1c3fb44d9dc04c9c6cf35b7ea58')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/6.json b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/6.json new file mode 100644 index 00000000..c6ccf4b7 --- /dev/null +++ b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/6.json @@ -0,0 +1,1016 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "dff788fb3b6ff922a1f566a9752c2029", + "entities": [ + { + "tableName": "queue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `track_order` INTEGER NOT NULL, `last_play` INTEGER NOT NULL, `playing_changed` INTEGER NOT NULL, `stream_id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`track_order`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "trackOrder", + "columnName": "track_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastPlay", + "columnName": "last_play", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playingChanged", + "columnName": "playing_changed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "track", + "columnName": "track", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "genre", + "columnName": "genre", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "track_order" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "server", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_name` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `address` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `low_security` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLowSecurity", + "columnName": "low_security", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recent_search", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`search` TEXT NOT NULL, PRIMARY KEY(`search`))", + "fields": [ + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "search" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "download", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlist_id` TEXT, `playlist_name` TEXT, `download_state` INTEGER NOT NULL DEFAULT 1, `download_uri` TEXT DEFAULT '', `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "playlistName", + "columnName": "playlist_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "downloadState", + "columnName": "download_state", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "downloadUri", + "columnName": "download_uri", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "track", + "columnName": "track", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "genre", + "columnName": "genre", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "chronology", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `server` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "server", + "columnName": "server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "track", + "columnName": "track", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "genre", + "columnName": "genre", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "favorite", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `songId` TEXT, `albumId` TEXT, `artistId` TEXT, `toStar` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "toStar", + "columnName": "toStar", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "timestamp" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "session_media_item", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`index` INTEGER NOT NULL DEFAULT 0, `id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, `stream_id` TEXT, `stream_url` TEXT, PRIMARY KEY(`index`))", + "fields": [ + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "track", + "columnName": "track", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "genre", + "columnName": "genre", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "streamUrl", + "columnName": "stream_url", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "index" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'dff788fb3b6ff922a1f566a9752c2029')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/7.json b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/7.json new file mode 100644 index 00000000..a3286def --- /dev/null +++ b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/7.json @@ -0,0 +1,1021 @@ +{ + "formatVersion": 1, + "database": { + "version": 7, + "identityHash": "cca7b016c047d8fdc86dd6373f2fb173", + "entities": [ + { + "tableName": "queue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `track_order` INTEGER NOT NULL, `last_play` INTEGER NOT NULL, `playing_changed` INTEGER NOT NULL, `stream_id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`track_order`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "trackOrder", + "columnName": "track_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastPlay", + "columnName": "last_play", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playingChanged", + "columnName": "playing_changed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "track", + "columnName": "track", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "genre", + "columnName": "genre", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "track_order" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "server", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_name` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `address` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `low_security` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLowSecurity", + "columnName": "low_security", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recent_search", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`search` TEXT NOT NULL, PRIMARY KEY(`search`))", + "fields": [ + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "search" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "download", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlist_id` TEXT, `playlist_name` TEXT, `download_state` INTEGER NOT NULL DEFAULT 1, `download_uri` TEXT DEFAULT '', `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "playlistName", + "columnName": "playlist_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "downloadState", + "columnName": "download_state", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "downloadUri", + "columnName": "download_uri", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "track", + "columnName": "track", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "genre", + "columnName": "genre", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "chronology", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `server` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "server", + "columnName": "server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "track", + "columnName": "track", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "genre", + "columnName": "genre", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "favorite", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `songId` TEXT, `albumId` TEXT, `artistId` TEXT, `toStar` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "toStar", + "columnName": "toStar", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "timestamp" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "session_media_item", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`index` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, `stream_id` TEXT, `stream_url` TEXT, `timestamp` INTEGER)", + "fields": [ + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "track", + "columnName": "track", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "genre", + "columnName": "genre", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "streamUrl", + "columnName": "stream_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "index" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cca7b016c047d8fdc86dd6373f2fb173')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/8.json b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/8.json new file mode 100644 index 00000000..26ab81b1 --- /dev/null +++ b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/8.json @@ -0,0 +1,1021 @@ +{ + "formatVersion": 1, + "database": { + "version": 8, + "identityHash": "cca7b016c047d8fdc86dd6373f2fb173", + "entities": [ + { + "tableName": "queue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `track_order` INTEGER NOT NULL, `last_play` INTEGER NOT NULL, `playing_changed` INTEGER NOT NULL, `stream_id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`track_order`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "trackOrder", + "columnName": "track_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastPlay", + "columnName": "last_play", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playingChanged", + "columnName": "playing_changed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "track", + "columnName": "track", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "genre", + "columnName": "genre", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "track_order" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "server", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_name` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `address` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `low_security` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLowSecurity", + "columnName": "low_security", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recent_search", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`search` TEXT NOT NULL, PRIMARY KEY(`search`))", + "fields": [ + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "search" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "download", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlist_id` TEXT, `playlist_name` TEXT, `download_state` INTEGER NOT NULL DEFAULT 1, `download_uri` TEXT DEFAULT '', `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "playlistName", + "columnName": "playlist_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "downloadState", + "columnName": "download_state", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "downloadUri", + "columnName": "download_uri", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "track", + "columnName": "track", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "genre", + "columnName": "genre", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "chronology", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `server` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "server", + "columnName": "server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "track", + "columnName": "track", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "genre", + "columnName": "genre", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "favorite", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `songId` TEXT, `albumId` TEXT, `artistId` TEXT, `toStar` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "toStar", + "columnName": "toStar", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "timestamp" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "session_media_item", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`index` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, `stream_id` TEXT, `stream_url` TEXT, `timestamp` INTEGER)", + "fields": [ + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "track", + "columnName": "track", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "genre", + "columnName": "genre", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "streamUrl", + "columnName": "stream_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "index" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cca7b016c047d8fdc86dd6373f2fb173')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b94766b9..986481bd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,11 +16,16 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:localeConfig="@xml/locale_config" + android:networkSecurityConfig="@xml/network_security_config" android:roundIcon="@mipmap/ic_launcher" android:supportsRtl="true" android:theme="@style/AppTheme.SplashScreen" - android:usesCleartextTraffic="true" - android:networkSecurityConfig="@xml/network_security_config"> + android:usesCleartextTraffic="true"> + + + - + + diff --git a/app/src/main/java/com/cappielloantonio/tempo/database/AppDatabase.java b/app/src/main/java/com/cappielloantonio/tempo/database/AppDatabase.java index 7e516853..f656b3c4 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/database/AppDatabase.java +++ b/app/src/main/java/com/cappielloantonio/tempo/database/AppDatabase.java @@ -14,17 +14,19 @@ import com.cappielloantonio.tempo.database.dao.FavoriteDao; import com.cappielloantonio.tempo.database.dao.QueueDao; import com.cappielloantonio.tempo.database.dao.RecentSearchDao; import com.cappielloantonio.tempo.database.dao.ServerDao; +import com.cappielloantonio.tempo.database.dao.SessionMediaItemDao; import com.cappielloantonio.tempo.model.Chronology; import com.cappielloantonio.tempo.model.Download; import com.cappielloantonio.tempo.model.Favorite; import com.cappielloantonio.tempo.model.Queue; import com.cappielloantonio.tempo.model.RecentSearch; import com.cappielloantonio.tempo.model.Server; +import com.cappielloantonio.tempo.model.SessionMediaItem; @Database( - version = 3, - entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class}, - autoMigrations = {@AutoMigration(from = 2, to = 3)} + version = 8, + entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class, SessionMediaItem.class}, + autoMigrations = {@AutoMigration(from = 7, to = 8)} ) @TypeConverters({DateConverters.class}) public abstract class AppDatabase extends RoomDatabase { @@ -52,4 +54,6 @@ public abstract class AppDatabase extends RoomDatabase { public abstract ChronologyDao chronologyDao(); public abstract FavoriteDao favoriteDao(); + + public abstract SessionMediaItemDao sessionMediaItemDao(); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/database/dao/ChronologyDao.java b/app/src/main/java/com/cappielloantonio/tempo/database/dao/ChronologyDao.java index 573c8940..906f7efa 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/database/dao/ChronologyDao.java +++ b/app/src/main/java/com/cappielloantonio/tempo/database/dao/ChronologyDao.java @@ -12,6 +12,9 @@ import java.util.List; @Dao public interface ChronologyDao { + @Query("SELECT * FROM chronology WHERE server == :server GROUP BY id ORDER BY timestamp DESC LIMIT :count") + LiveData> getLastPlayed(String server, int count); + @Query("SELECT * FROM chronology WHERE timestamp >= :startDate AND timestamp < :endDate AND server == :server GROUP BY id ORDER BY COUNT(id) DESC LIMIT 9") LiveData> getAllFrom(long startDate, long endDate, String server); diff --git a/app/src/main/java/com/cappielloantonio/tempo/database/dao/SessionMediaItemDao.java b/app/src/main/java/com/cappielloantonio/tempo/database/dao/SessionMediaItemDao.java new file mode 100644 index 00000000..a3f415ad --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/database/dao/SessionMediaItemDao.java @@ -0,0 +1,29 @@ +package com.cappielloantonio.tempo.database.dao; + +import androidx.room.Dao; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; + +import com.cappielloantonio.tempo.model.Queue; +import com.cappielloantonio.tempo.model.SessionMediaItem; + +import java.util.List; + +@Dao +public interface SessionMediaItemDao { + @Query("SELECT * FROM session_media_item WHERE id = :id") + SessionMediaItem get(String id); + + @Query("SELECT * FROM session_media_item WHERE timestamp = :timestamp") + List get(long timestamp); + + @Insert(onConflict = OnConflictStrategy.IGNORE) + void insert(SessionMediaItem sessionMediaItem); + + @Insert(onConflict = OnConflictStrategy.IGNORE) + void insertAll(List sessionMediaItems); + + @Query("DELETE FROM session_media_item") + void deleteAll(); +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/model/HomeSector.kt b/app/src/main/java/com/cappielloantonio/tempo/model/HomeSector.kt new file mode 100644 index 00000000..79ca4356 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/model/HomeSector.kt @@ -0,0 +1,11 @@ +package com.cappielloantonio.tempo.model + +import androidx.annotation.Keep + +@Keep +data class HomeSector( + val id: String, + val sectorTitle: String, + var isVisible: Boolean, + val order: Int, +) \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt b/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt new file mode 100644 index 00000000..96e7ac6b --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt @@ -0,0 +1,281 @@ +package com.cappielloantonio.tempo.model + +import android.net.Uri +import android.os.Bundle +import androidx.annotation.Keep +import androidx.media3.common.MediaItem +import androidx.media3.common.MediaItem.RequestMetadata +import androidx.media3.common.MediaMetadata +import androidx.media3.common.MimeTypes +import androidx.media3.common.util.UnstableApi +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.cappielloantonio.tempo.glide.CustomGlideRequest +import com.cappielloantonio.tempo.subsonic.models.Child +import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation +import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode +import com.cappielloantonio.tempo.util.Constants +import com.cappielloantonio.tempo.util.MusicUtil +import com.cappielloantonio.tempo.util.Preferences.getImageSize +import java.util.Date + +@UnstableApi +@Keep +@Entity(tableName = "session_media_item") +class SessionMediaItem() { + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = "index") + var index: Int = 0 + + @ColumnInfo(name = "id") + var id: String? = null + + @ColumnInfo(name = "parent_id") + var parentId: String? = null + + @ColumnInfo(name = "is_dir") + var isDir: Boolean = false + + @ColumnInfo + var title: String? = null + + @ColumnInfo + var album: String? = null + + @ColumnInfo + var artist: String? = null + + @ColumnInfo + var track: Int? = null + + @ColumnInfo + var year: Int? = null + + @ColumnInfo + var genre: String? = null + + @ColumnInfo(name = "cover_art_id") + var coverArtId: String? = null + + @ColumnInfo + var size: Long? = null + + @ColumnInfo(name = "content_type") + var contentType: String? = null + + @ColumnInfo + var suffix: String? = null + + @ColumnInfo("transcoding_content_type") + var transcodedContentType: String? = null + + @ColumnInfo(name = "transcoded_suffix") + var transcodedSuffix: String? = null + + @ColumnInfo + var duration: Int? = null + + @ColumnInfo("bitrate") + var bitrate: Int? = null + + @ColumnInfo + var path: String? = null + + @ColumnInfo(name = "is_video") + var isVideo: Boolean = false + + @ColumnInfo(name = "user_rating") + var userRating: Int? = null + + @ColumnInfo(name = "average_rating") + var averageRating: Double? = null + + @ColumnInfo(name = "play_count") + var playCount: Long? = null + + @ColumnInfo(name = "disc_number") + var discNumber: Int? = null + + @ColumnInfo + var created: Date? = null + + @ColumnInfo + var starred: Date? = null + + @ColumnInfo(name = "album_id") + var albumId: String? = null + + @ColumnInfo(name = "artist_id") + var artistId: String? = null + + @ColumnInfo + var type: String? = null + + @ColumnInfo(name = "bookmark_position") + var bookmarkPosition: Long? = null + + @ColumnInfo(name = "original_width") + var originalWidth: Int? = null + + @ColumnInfo(name = "original_height") + var originalHeight: Int? = null + + @ColumnInfo(name = "stream_id") + var streamId: String? = null + + @ColumnInfo(name = "stream_url") + var streamUrl: String? = null + + @ColumnInfo(name = "timestamp") + var timestamp: Long? = null + + constructor(child: Child) : this() { + id = child.id + parentId = child.parentId + isDir = child.isDir + title = child.title + album = child.album + artist = child.artist + track = child.track + year = child.year + genre = child.genre + coverArtId = child.coverArtId + size = child.size + contentType = child.contentType + suffix = child.suffix + transcodedContentType = child.transcodedContentType + transcodedSuffix = child.transcodedSuffix + duration = child.duration + bitrate = child.bitrate + path = child.path + isVideo = child.isVideo + userRating = child.userRating + averageRating = child.averageRating + playCount = child.playCount + discNumber = child.discNumber + created = child.created + starred = child.starred + albumId = child.albumId + artistId = child.artistId + type = Constants.MEDIA_TYPE_MUSIC + bookmarkPosition = child.bookmarkPosition + originalWidth = child.originalWidth + originalHeight = child.originalHeight + } + + constructor(podcastEpisode: PodcastEpisode) : this() { + id = podcastEpisode.id + parentId = podcastEpisode.parentId + isDir = podcastEpisode.isDir + title = podcastEpisode.title + album = podcastEpisode.album + artist = podcastEpisode.artist + year = podcastEpisode.year + genre = podcastEpisode.genre + coverArtId = podcastEpisode.coverArtId + size = podcastEpisode.size + contentType = podcastEpisode.contentType + suffix = podcastEpisode.suffix + duration = podcastEpisode.duration + bitrate = podcastEpisode.bitrate + path = podcastEpisode.path + isVideo = podcastEpisode.isVideo + created = podcastEpisode.created + artistId = podcastEpisode.artistId + streamId = podcastEpisode.streamId + type = Constants.MEDIA_TYPE_PODCAST + } + + constructor(internetRadioStation: InternetRadioStation) : this() { + id = internetRadioStation.id + title = internetRadioStation.name + streamUrl = internetRadioStation.streamUrl + type = Constants.MEDIA_TYPE_RADIO + } + + fun getMediaItem(): MediaItem { + val uri: Uri = getStreamUri() + val artworkUri = Uri.parse(CustomGlideRequest.createUrl(coverArtId, getImageSize())) + + val bundle = Bundle() + bundle.putString("id", id) + bundle.putString("parentId", parentId) + bundle.putBoolean("isDir", isDir) + bundle.putString("title", title) + bundle.putString("album", album) + bundle.putString("artist", artist) + bundle.putInt("track", track ?: 0) + bundle.putInt("year", year ?: 0) + bundle.putString("genre", genre) + bundle.putString("coverArtId", coverArtId) + bundle.putLong("size", size ?: 0) + bundle.putString("contentType", contentType) + bundle.putString("suffix", suffix) + bundle.putString("transcodedContentType", transcodedContentType) + bundle.putString("transcodedSuffix", transcodedSuffix) + bundle.putInt("duration", duration ?: 0) + bundle.putInt("bitrate", bitrate ?: 0) + bundle.putString("path", path) + bundle.putBoolean("isVideo", isVideo) + bundle.putInt("userRating", userRating ?: 0) + bundle.putDouble("averageRating", averageRating ?: .0) + bundle.putLong("playCount", playCount ?: 0) + bundle.putInt("discNumber", discNumber ?: 0) + bundle.putLong("created", created?.time ?: 0) + bundle.putLong("starred", starred?.time ?: 0) + bundle.putString("albumId", albumId) + bundle.putString("artistId", artistId) + bundle.putString("type", Constants.MEDIA_TYPE_MUSIC) + bundle.putLong("bookmarkPosition", bookmarkPosition ?: 0) + bundle.putInt("originalWidth", originalWidth ?: 0) + bundle.putInt("originalHeight", originalHeight ?: 0) + bundle.putString("uri", uri.toString()) + + return MediaItem.Builder() + .setMediaId(id!!) + .setMediaMetadata( + MediaMetadata.Builder() + .setTitle(MusicUtil.getReadableString(title)) + .setTrackNumber(track ?: 0) + .setDiscNumber(discNumber ?: 0) + .setReleaseYear(year ?: 0) + .setAlbumTitle(MusicUtil.getReadableString(album)) + .setArtist(MusicUtil.getReadableString(artist)) + .setArtworkUri(artworkUri) + .setExtras(bundle) + .setIsBrowsable(false) + .setIsPlayable(true) + .build() + ) + .setRequestMetadata( + RequestMetadata.Builder() + .setMediaUri(uri) + .setExtras(bundle) + .build() + ) + .setMimeType(MimeTypes.BASE_TYPE_AUDIO) + .setUri(uri) + .build() + } + + private fun getStreamUri(): Uri { + return when (type) { + Constants.MEDIA_TYPE_MUSIC -> { + MusicUtil.getStreamUri(id) + } + + Constants.MEDIA_TYPE_PODCAST -> { + MusicUtil.getStreamUri(streamId) + } + + Constants.MEDIA_TYPE_RADIO -> { + Uri.parse(streamUrl) + } + + else -> { + MusicUtil.getStreamUri(id) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java index 76a55220..6dc8d3e3 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java @@ -8,6 +8,7 @@ import com.cappielloantonio.tempo.interfaces.DecadesCallback; import com.cappielloantonio.tempo.interfaces.MediaCallback; import com.cappielloantonio.tempo.subsonic.base.ApiResponse; import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.subsonic.models.AlbumInfo; import com.cappielloantonio.tempo.subsonic.models.Child; import java.util.ArrayList; @@ -131,9 +132,10 @@ public class AlbumRepository { .enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getArtist() != null) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getArtist() != null && response.body().getSubsonicResponse().getArtist().getAlbums() != null) { List albums = response.body().getSubsonicResponse().getArtist().getAlbums(); albums.sort(Comparator.comparing(AlbumID3::getYear)); + Collections.reverse(albums); artistsAlbum.setValue(albums); } } @@ -170,6 +172,29 @@ public class AlbumRepository { return album; } + public MutableLiveData getAlbumInfo(String id) { + MutableLiveData albumInfo = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getAlbumInfo2(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbumInfo() != null) { + albumInfo.setValue(response.body().getSubsonicResponse().getAlbumInfo()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return albumInfo; + } + public void getInstantMix(AlbumID3 album, int count, MediaCallback callback) { App.getSubsonicClientInstance(false) .getBrowsingClient() @@ -250,7 +275,7 @@ public class AlbumRepository { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbumList2() != null && response.body().getSubsonicResponse().getAlbumList2().getAlbums() != null) { - if (response.body().getSubsonicResponse().getAlbumList2().getAlbums().size() > 0 && !response.body().getSubsonicResponse().getAlbumList2().getAlbums().isEmpty()) { + if (!response.body().getSubsonicResponse().getAlbumList2().getAlbums().isEmpty() && !response.body().getSubsonicResponse().getAlbumList2().getAlbums().isEmpty()) { callback.onLoadYear(response.body().getSubsonicResponse().getAlbumList2().getAlbums().get(0).getYear()); } else { callback.onLoadYear(-1); diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java new file mode 100644 index 00000000..fe24d81d --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java @@ -0,0 +1,1027 @@ +package com.cappielloantonio.tempo.repository; + + +import android.net.Uri; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Observer; +import androidx.media3.common.MediaItem; +import androidx.media3.common.MediaMetadata; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.LibraryResult; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.database.AppDatabase; +import com.cappielloantonio.tempo.database.dao.ChronologyDao; +import com.cappielloantonio.tempo.database.dao.SessionMediaItemDao; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.model.Chronology; +import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.model.SessionMediaItem; +import com.cappielloantonio.tempo.service.DownloaderManager; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.subsonic.models.Artist; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.subsonic.models.Directory; +import com.cappielloantonio.tempo.subsonic.models.Index; +import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation; +import com.cappielloantonio.tempo.subsonic.models.MusicFolder; +import com.cappielloantonio.tempo.subsonic.models.Playlist; +import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode; +import com.cappielloantonio.tempo.util.DownloadUtil; +import com.cappielloantonio.tempo.util.MappingUtil; +import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.util.Preferences; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class AutomotiveRepository { + private final SessionMediaItemDao sessionMediaItemDao = AppDatabase.getInstance().sessionMediaItemDao(); + private final ChronologyDao chronologyDao = AppDatabase.getInstance().chronologyDao(); + + public ListenableFuture>> getAlbums(String prefix, String type, int size) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getAlbumList2(type, size, 0, null, null) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbumList2() != null && response.body().getSubsonicResponse().getAlbumList2().getAlbums() != null) { + List albums = response.body().getSubsonicResponse().getAlbumList2().getAlbums(); + + List mediaItems = new ArrayList<>(); + + for (AlbumID3 album : albums) { + Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize())); + + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(album.getName()) + .setAlbumTitle(album.getName()) + .setArtist(album.getArtist()) + .setGenre(album.getGenre()) + .setIsBrowsable(true) + .setIsPlayable(false) + .setMediaType(MediaMetadata.MEDIA_TYPE_ALBUM) + .setArtworkUri(artworkUri) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(prefix + album.getId()) + .setMediaMetadata(mediaMetadata) + .setUri("") + .build(); + + mediaItems.add(mediaItem); + } + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } else { + listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getStarredSongs() { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getStarred2() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getStarred2() != null && response.body().getSubsonicResponse().getStarred2().getSongs() != null) { + List songs = response.body().getSubsonicResponse().getStarred2().getSongs(); + + setChildrenMetadata(songs); + + List mediaItems = MappingUtil.mapMediaItems(songs); + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } else { + listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getRandomSongs(int count) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getRandomSongs(100, null, null) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null && response.body().getSubsonicResponse().getRandomSongs().getSongs() != null) { + List songs = response.body().getSubsonicResponse().getRandomSongs().getSongs(); + + setChildrenMetadata(songs); + + List mediaItems = MappingUtil.mapMediaItems(songs); + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } else { + listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getRecentlyPlayedSongs(String server, int count) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + chronologyDao.getLastPlayed(server, count).observeForever(new Observer>() { + @Override + public void onChanged(List chronology) { + if (chronology != null && !chronology.isEmpty()) { + List songs = new ArrayList<>(chronology); + + setChildrenMetadata(songs); + + List mediaItems = MappingUtil.mapMediaItems(songs); + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } else { + listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); + } + + chronologyDao.getLastPlayed(server, count).removeObserver(this); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getStarredAlbums(String prefix) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getStarred2() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getStarred2() != null && response.body().getSubsonicResponse().getStarred2().getAlbums() != null) { + List albums = response.body().getSubsonicResponse().getStarred2().getAlbums(); + + List mediaItems = new ArrayList<>(); + + for (AlbumID3 album : albums) { + Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize())); + + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(album.getName()) + .setArtist(album.getArtist()) + .setGenre(album.getGenre()) + .setIsBrowsable(true) + .setIsPlayable(false) + .setMediaType(MediaMetadata.MEDIA_TYPE_ALBUM) + .setArtworkUri(artworkUri) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(prefix + album.getId()) + .setMediaMetadata(mediaMetadata) + .setUri("") + .build(); + + mediaItems.add(mediaItem); + } + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } else { + listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getStarredArtists(String prefix) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getStarred2() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getStarred2() != null && response.body().getSubsonicResponse().getStarred2().getArtists() != null) { + List artists = response.body().getSubsonicResponse().getStarred2().getArtists(); + + Collections.shuffle(artists); + + List mediaItems = new ArrayList<>(); + + for (ArtistID3 artist : artists) { + Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(artist.getCoverArtId(), Preferences.getImageSize())); + + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(artist.getName()) + .setIsBrowsable(true) + .setIsPlayable(false) + .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST) + .setArtworkUri(artworkUri) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(prefix + artist.getId()) + .setMediaMetadata(mediaMetadata) + .setUri("") + .build(); + + mediaItems.add(mediaItem); + } + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } else { + listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getMusicFolders(String prefix) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getMusicFolders() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getMusicFolders() != null && response.body().getSubsonicResponse().getMusicFolders().getMusicFolders() != null) { + List musicFolders = response.body().getSubsonicResponse().getMusicFolders().getMusicFolders(); + + List mediaItems = new ArrayList<>(); + + for (MusicFolder musicFolder : musicFolders) { + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(musicFolder.getName()) + .setIsBrowsable(true) + .setIsPlayable(false) + .setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_MIXED) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(prefix + musicFolder.getId()) + .setMediaMetadata(mediaMetadata) + .setUri("") + .build(); + + mediaItems.add(mediaItem); + } + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } else { + listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getIndexes(String prefix, String id) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getIndexes(id, null) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getIndexes() != null) { + List mediaItems = new ArrayList<>(); + + if (response.body().getSubsonicResponse().getIndexes().getIndices() != null) { + List indices = response.body().getSubsonicResponse().getIndexes().getIndices(); + + for (Index index : indices) { + if (index.getArtists() != null) { + for (Artist artist : index.getArtists()) { + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(artist.getName()) + .setIsBrowsable(true) + .setIsPlayable(false) + .setMediaType(MediaMetadata.MEDIA_TYPE_ARTIST) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(prefix + artist.getId()) + .setMediaMetadata(mediaMetadata) + .setUri("") + .build(); + + mediaItems.add(mediaItem); + } + } + } + } + + if (response.body().getSubsonicResponse().getIndexes().getChildren() != null) { + List children = response.body().getSubsonicResponse().getIndexes().getChildren(); + + for (Child song : children) { + Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(song.getCoverArtId(), Preferences.getImageSize())); + + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(song.getTitle()) + .setAlbumTitle(song.getAlbum()) + .setArtist(song.getArtist()) + .setIsBrowsable(false) + .setIsPlayable(true) + .setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC) + .setArtworkUri(artworkUri) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(prefix + song.getId()) + .setMediaMetadata(mediaMetadata) + .setUri(MusicUtil.getStreamUri(song.getId())) + .build(); + + mediaItems.add(mediaItem); + } + + setChildrenMetadata(children); + } + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getDirectories(String prefix, String id) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getMusicDirectory(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getDirectory() != null && response.body().getSubsonicResponse().getDirectory().getChildren() != null) { + Directory directory = response.body().getSubsonicResponse().getDirectory(); + + List mediaItems = new ArrayList<>(); + + for (Child child : directory.getChildren()) { + Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(child.getCoverArtId(), Preferences.getImageSize())); + + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(child.getTitle()) + .setIsBrowsable(child.isDir()) + .setIsPlayable(!child.isDir()) + .setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_MIXED) + .setArtworkUri(artworkUri) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(child.isDir() ? prefix + child.getId() : child.getId()) + .setMediaMetadata(mediaMetadata) + .setUri(!child.isDir() ? MusicUtil.getStreamUri(child.getId()) : Uri.parse("")) + .build(); + + mediaItems.add(mediaItem); + } + + setChildrenMetadata(directory.getChildren().stream().filter(child -> !child.isDir()).collect(Collectors.toList())); + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getPlaylists(String prefix) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getPlaylistClient() + .getPlaylists() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getPlaylists() != null && response.body().getSubsonicResponse().getPlaylists().getPlaylists() != null) { + List playlists = response.body().getSubsonicResponse().getPlaylists().getPlaylists(); + + List mediaItems = new ArrayList<>(); + + for (Playlist playlist : playlists) { + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(playlist.getName()) + .setIsBrowsable(true) + .setIsPlayable(false) + .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(prefix + playlist.getId()) + .setMediaMetadata(mediaMetadata) + .setUri("") + .build(); + + mediaItems.add(mediaItem); + } + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } else { + listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getNewestPodcastEpisodes(int count) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getPodcastClient() + .getNewestPodcasts(count) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getNewestPodcasts() != null && response.body().getSubsonicResponse().getNewestPodcasts().getEpisodes() != null) { + List episodes = response.body().getSubsonicResponse().getNewestPodcasts().getEpisodes(); + + List mediaItems = new ArrayList<>(); + + for (PodcastEpisode episode : episodes) { + Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(episode.getCoverArtId(), Preferences.getImageSize())); + + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(episode.getTitle()) + .setIsBrowsable(false) + .setIsPlayable(true) + .setMediaType(MediaMetadata.MEDIA_TYPE_PODCAST_EPISODE) + .setArtworkUri(artworkUri) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(episode.getId()) + .setMediaMetadata(mediaMetadata) + .setUri(MusicUtil.getStreamUri(episode.getStreamId())) + .build(); + + mediaItems.add(mediaItem); + } + + setPodcastEpisodesMetadata(episodes); + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } else { + listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getInternetRadioStations() { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getInternetRadioClient() + .getInternetRadioStations() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getInternetRadioStations() != null && response.body().getSubsonicResponse().getInternetRadioStations().getInternetRadioStations() != null) { + + List radioStations = response.body().getSubsonicResponse().getInternetRadioStations().getInternetRadioStations(); + + List mediaItems = new ArrayList<>(); + + for (InternetRadioStation radioStation : radioStations) { + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(radioStation.getName()) + .setIsBrowsable(false) + .setIsPlayable(true) + .setMediaType(MediaMetadata.MEDIA_TYPE_RADIO_STATION) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(radioStation.getId()) + .setMediaMetadata(mediaMetadata) + .setUri(radioStation.getStreamUrl()) + .build(); + + mediaItems.add(mediaItem); + } + + setInternetRadioStationsMetadata(radioStations); + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } else { + listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getAlbumTracks(String id) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getAlbum(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbum() != null && response.body().getSubsonicResponse().getAlbum().getSongs() != null) { + List tracks = response.body().getSubsonicResponse().getAlbum().getSongs(); + + setChildrenMetadata(tracks); + + List mediaItems = MappingUtil.mapMediaItems(tracks); + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } else { + listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getArtistAlbum(String prefix, String id) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getArtist(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getArtist() != null && response.body().getSubsonicResponse().getArtist().getAlbums() != null) { + List albums = response.body().getSubsonicResponse().getArtist().getAlbums(); + + List mediaItems = new ArrayList<>(); + + for (AlbumID3 album : albums) { + Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize())); + + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(album.getName()) + .setAlbumTitle(album.getName()) + .setArtist(album.getArtist()) + .setGenre(album.getGenre()) + .setIsBrowsable(true) + .setIsPlayable(false) + .setMediaType(MediaMetadata.MEDIA_TYPE_ALBUM) + .setArtworkUri(artworkUri) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(prefix + album.getId()) + .setMediaMetadata(mediaMetadata) + .setUri("") + .build(); + + mediaItems.add(mediaItem); + } + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getPlaylistSongs(String id) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getPlaylistClient() + .getPlaylist(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getPlaylist() != null && response.body().getSubsonicResponse().getPlaylist().getEntries() != null) { + List tracks = response.body().getSubsonicResponse().getPlaylist().getEntries(); + + setChildrenMetadata(tracks); + + List mediaItems = MappingUtil.mapMediaItems(tracks); + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getMadeForYou(String id, int count) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getSimilarSongs2(id, count) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSimilarSongs2() != null && response.body().getSubsonicResponse().getSimilarSongs2().getSongs() != null) { + List tracks = response.body().getSubsonicResponse().getSimilarSongs2().getSongs(); + + setChildrenMetadata(tracks); + + List mediaItems = MappingUtil.mapMediaItems(tracks); + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> search(String query, String albumPrefix, String artistPrefix) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getSearchingClient() + .search3(query, 20, 20, 20) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSearchResult3() != null) { + List mediaItems = new ArrayList<>(); + + if (response.body().getSubsonicResponse().getSearchResult3().getArtists() != null) { + for (ArtistID3 artist : response.body().getSubsonicResponse().getSearchResult3().getArtists()) { + Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(artist.getCoverArtId(), Preferences.getImageSize())); + + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(artist.getName()) + .setIsBrowsable(true) + .setIsPlayable(false) + .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST) + .setArtworkUri(artworkUri) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(artistPrefix + artist.getId()) + .setMediaMetadata(mediaMetadata) + .setUri("") + .build(); + + mediaItems.add(mediaItem); + } + } + + if (response.body().getSubsonicResponse().getSearchResult3().getAlbums() != null) { + for (AlbumID3 album : response.body().getSubsonicResponse().getSearchResult3().getAlbums()) { + Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize())); + + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(album.getName()) + .setAlbumTitle(album.getName()) + .setArtist(album.getArtist()) + .setGenre(album.getGenre()) + .setIsBrowsable(true) + .setIsPlayable(false) + .setMediaType(MediaMetadata.MEDIA_TYPE_ALBUM) + .setArtworkUri(artworkUri) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(albumPrefix + album.getId()) + .setMediaMetadata(mediaMetadata) + .setUri("") + .build(); + + mediaItems.add(mediaItem); + } + } + + if (response.body().getSubsonicResponse().getSearchResult3().getSongs() != null) { + List tracks = response.body().getSubsonicResponse().getSearchResult3().getSongs(); + setChildrenMetadata(tracks); + mediaItems.addAll(MappingUtil.mapMediaItems(tracks)); + } + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + @OptIn(markerClass = UnstableApi.class) + public void setChildrenMetadata(List children) { + long timestamp = System.currentTimeMillis(); + ArrayList sessionMediaItems = new ArrayList<>(); + + for (Child child : children) { + SessionMediaItem sessionMediaItem = new SessionMediaItem(child); + sessionMediaItem.setTimestamp(timestamp); + sessionMediaItems.add(sessionMediaItem); + } + + InsertAllThreadSafe insertAll = new InsertAllThreadSafe(sessionMediaItemDao, sessionMediaItems); + Thread thread = new Thread(insertAll); + thread.start(); + } + + @OptIn(markerClass = UnstableApi.class) + public void setPodcastEpisodesMetadata(List podcastEpisodes) { + long timestamp = System.currentTimeMillis(); + ArrayList sessionMediaItems = new ArrayList<>(); + + for (PodcastEpisode podcastEpisode : podcastEpisodes) { + SessionMediaItem sessionMediaItem = new SessionMediaItem(podcastEpisode); + sessionMediaItem.setTimestamp(timestamp); + sessionMediaItems.add(sessionMediaItem); + } + + InsertAllThreadSafe insertAll = new InsertAllThreadSafe(sessionMediaItemDao, sessionMediaItems); + Thread thread = new Thread(insertAll); + thread.start(); + } + + @OptIn(markerClass = UnstableApi.class) + public void setInternetRadioStationsMetadata(List internetRadioStations) { + long timestamp = System.currentTimeMillis(); + ArrayList sessionMediaItems = new ArrayList<>(); + + for (InternetRadioStation internetRadioStation : internetRadioStations) { + SessionMediaItem sessionMediaItem = new SessionMediaItem(internetRadioStation); + sessionMediaItem.setTimestamp(timestamp); + sessionMediaItems.add(sessionMediaItem); + } + + InsertAllThreadSafe insertAll = new InsertAllThreadSafe(sessionMediaItemDao, sessionMediaItems); + Thread thread = new Thread(insertAll); + thread.start(); + } + + public SessionMediaItem getSessionMediaItem(String id) { + SessionMediaItem sessionMediaItem = null; + + GetMediaItemThreadSafe getMediaItemThreadSafe = new GetMediaItemThreadSafe(sessionMediaItemDao, id); + Thread thread = new Thread(getMediaItemThreadSafe); + thread.start(); + + try { + thread.join(); + sessionMediaItem = getMediaItemThreadSafe.getSessionMediaItem(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + return sessionMediaItem; + } + + public List getMetadatas(long timestamp) { + List mediaItems = Collections.emptyList(); + + GetMediaItemsThreadSafe getMediaItemsThreadSafe = new GetMediaItemsThreadSafe(sessionMediaItemDao, timestamp); + Thread thread = new Thread(getMediaItemsThreadSafe); + thread.start(); + + try { + thread.join(); + mediaItems = getMediaItemsThreadSafe.getMediaItems(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + return mediaItems; + } + + public void deleteMetadata() { + DeleteAllThreadSafe delete = new DeleteAllThreadSafe(sessionMediaItemDao); + Thread thread = new Thread(delete); + thread.start(); + } + + private static class GetMediaItemThreadSafe implements Runnable { + private final SessionMediaItemDao sessionMediaItemDao; + private final String id; + + private SessionMediaItem sessionMediaItem; + + public GetMediaItemThreadSafe(SessionMediaItemDao sessionMediaItemDao, String id) { + this.sessionMediaItemDao = sessionMediaItemDao; + this.id = id; + } + + @Override + public void run() { + sessionMediaItem = sessionMediaItemDao.get(id); + } + + public SessionMediaItem getSessionMediaItem() { + return sessionMediaItem; + } + } + + @OptIn(markerClass = UnstableApi.class) + private static class GetMediaItemsThreadSafe implements Runnable { + private final SessionMediaItemDao sessionMediaItemDao; + private final Long timestamp; + private final List mediaItems = new ArrayList<>(); + + public GetMediaItemsThreadSafe(SessionMediaItemDao sessionMediaItemDao, Long timestamp) { + this.sessionMediaItemDao = sessionMediaItemDao; + this.timestamp = timestamp; + } + + @Override + public void run() { + List sessionMediaItems = sessionMediaItemDao.get(timestamp); + sessionMediaItems.forEach(sessionMediaItem -> mediaItems.add(sessionMediaItem.getMediaItem())); + } + + public List getMediaItems() { + return mediaItems; + } + } + + private static class InsertAllThreadSafe implements Runnable { + private final SessionMediaItemDao sessionMediaItemDao; + private final List sessionMediaItems; + + public InsertAllThreadSafe(SessionMediaItemDao sessionMediaItemDao, List sessionMediaItems) { + this.sessionMediaItemDao = sessionMediaItemDao; + this.sessionMediaItems = sessionMediaItems; + } + + @Override + public void run() { + sessionMediaItemDao.insertAll(sessionMediaItems); + } + } + + private static class DeleteAllThreadSafe implements Runnable { + private final SessionMediaItemDao sessionMediaItemDao; + + public DeleteAllThreadSafe(SessionMediaItemDao sessionMediaItemDao) { + this.sessionMediaItemDao = sessionMediaItemDao; + } + + @Override + public void run() { + sessionMediaItemDao.deleteAll(); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/GenreRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/GenreRepository.java index 58f00042..4e6addb0 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/GenreRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/GenreRepository.java @@ -8,7 +8,9 @@ import com.cappielloantonio.tempo.subsonic.base.ApiResponse; import com.cappielloantonio.tempo.subsonic.models.Genre; import java.util.Collections; +import java.util.Comparator; import java.util.List; +import java.util.stream.Collectors; import retrofit2.Call; import retrofit2.Callback; @@ -39,7 +41,7 @@ public class GenreRepository { if (size != -1) { genres.setValue(genreList.subList(0, Math.min(size, genreList.size()))); } else { - genres.setValue(genreList); + genres.setValue(genreList.stream().sorted(Comparator.comparing(Genre::getGenre)).collect(Collectors.toList())); } } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/OpenRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/OpenRepository.java new file mode 100644 index 00000000..86216034 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/OpenRepository.java @@ -0,0 +1,37 @@ +package com.cappielloantonio.tempo.repository; + +import androidx.annotation.NonNull; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; +import com.cappielloantonio.tempo.subsonic.models.LyricsList; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class OpenRepository { + public MutableLiveData getLyricsBySongId(String id) { + MutableLiveData lyricsList = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getOpenClient() + .getLyricsBySongId(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getLyricsList() != null) { + lyricsList.setValue(response.body().getSubsonicResponse().getLyricsList()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return lyricsList; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/PlaylistRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/PlaylistRepository.java index 1db0453b..003f845a 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/PlaylistRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/PlaylistRepository.java @@ -1,7 +1,5 @@ package com.cappielloantonio.tempo.repository; -import android.util.Log; - import androidx.annotation.NonNull; import androidx.lifecycle.MutableLiveData; diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java index db75f907..d8038e26 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java @@ -104,10 +104,10 @@ public class SongRepository { return randomSongsSample; } - public void scrobble(String id) { + public void scrobble(String id, boolean submission) { App.getSubsonicClientInstance(false) .getMediaAnnotationClient() - .scrobble(id) + .scrobble(id, submission) .enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/SystemRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/SystemRepository.java index 4dc1dea4..f477e6f0 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/SystemRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/SystemRepository.java @@ -6,7 +6,11 @@ import androidx.lifecycle.MutableLiveData; import com.cappielloantonio.tempo.App; import com.cappielloantonio.tempo.interfaces.SystemCallback; import com.cappielloantonio.tempo.subsonic.base.ApiResponse; +import com.cappielloantonio.tempo.subsonic.models.OpenSubsonicExtension; import com.cappielloantonio.tempo.subsonic.models.ResponseStatus; +import com.cappielloantonio.tempo.subsonic.models.SubsonicResponse; + +import java.util.List; import retrofit2.Call; import retrofit2.Callback; @@ -43,8 +47,8 @@ public class SystemRepository { }); } - public MutableLiveData ping() { - MutableLiveData pingResult = new MutableLiveData<>(); + public MutableLiveData ping() { + MutableLiveData pingResult = new MutableLiveData<>(); App.getSubsonicClientInstance(false) .getSystemClient() @@ -53,16 +57,39 @@ public class SystemRepository { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful() && response.body() != null) { - pingResult.postValue(true); + pingResult.postValue(response.body().getSubsonicResponse()); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { - pingResult.postValue(false); + pingResult.postValue(null); } }); return pingResult; } + + public MutableLiveData> getOpenSubsonicExtensions() { + MutableLiveData> extensionsResult = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getSystemClient() + .getOpenSubsonicExtensions() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null) { + extensionsResult.postValue(response.body().getSubsonicResponse().getOpenSubsonicExtensions()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + extensionsResult.postValue(null); + } + }); + + return extensionsResult; + } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/service/DownloaderManager.java b/app/src/main/java/com/cappielloantonio/tempo/service/DownloaderManager.java index 66f13910..3a695fef 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/service/DownloaderManager.java +++ b/app/src/main/java/com/cappielloantonio/tempo/service/DownloaderManager.java @@ -31,9 +31,10 @@ public class DownloaderManager { private final Context context; private final DataSource.Factory dataSourceFactory; - private final HashMap downloads; private final DownloadIndex downloadIndex; + private static HashMap downloads; + public DownloaderManager(Context context, DataSource.Factory dataSourceFactory, DownloadManager downloadManager) { this.context = context.getApplicationContext(); this.dataSourceFactory = dataSourceFactory; @@ -61,19 +62,11 @@ public class DownloaderManager { } public boolean isDownloaded(MediaItem mediaItem) { - @Nullable Download download = downloads.get(mediaItem.mediaId); - return download != null && download.state != Download.STATE_FAILED; + return isDownloaded(mediaItem.mediaId); } public boolean areDownloaded(List mediaItems) { - for (MediaItem mediaItem : mediaItems) { - @Nullable Download download = downloads.get(mediaItem.mediaId); - if (download != null && download.state != Download.STATE_FAILED) { - return true; - } - } - - return false; + return mediaItems.stream().anyMatch(this::isDownloaded); } public void download(MediaItem mediaItem, com.cappielloantonio.tempo.model.Download download) { @@ -92,6 +85,7 @@ public class DownloaderManager { public void remove(MediaItem mediaItem, com.cappielloantonio.tempo.model.Download download) { DownloadService.sendRemoveDownload(context, DownloaderService.class, buildDownloadRequest(mediaItem).id, false); deleteDatabase(download.getId()); + downloads.remove(download.getId()); } public void remove(List mediaItems, List downloads) { @@ -122,23 +116,33 @@ public class DownloaderManager { return download != null ? download.getTitle() : null; } + public static void updateRequestDownload(Download download) { + updateDatabase(download.request.id); + downloads.put(download.request.id, download); + } + + public static void removeRequestDownload(Download download) { + deleteDatabase(download.request.id); + downloads.remove(download.request.id); + } + private static DownloadRepository getDownloadRepository() { return new DownloadRepository(); } - public static void insertDatabase(com.cappielloantonio.tempo.model.Download download) { + private static void insertDatabase(com.cappielloantonio.tempo.model.Download download) { getDownloadRepository().insert(download); } - public static void deleteDatabase(String id) { + private static void deleteDatabase(String id) { getDownloadRepository().delete(id); } - public static void deleteAllDatabase() { + private static void deleteAllDatabase() { getDownloadRepository().deleteAll(); } - public static void updateDatabase(String id) { + private static void updateDatabase(String id) { getDownloadRepository().update(id); } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/service/DownloaderService.java b/app/src/main/java/com/cappielloantonio/tempo/service/DownloaderService.java index e34867a0..b34daf33 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/service/DownloaderService.java +++ b/app/src/main/java/com/cappielloantonio/tempo/service/DownloaderService.java @@ -95,7 +95,7 @@ public class DownloaderService extends androidx.media3.exoplayer.offline.Downloa notification = notificationHelper.buildDownloadCompletedNotification(context, R.drawable.ic_check_circle, null, DownloaderManager.getDownloadNotificationMessage(download.request.id)); notification = Notification.Builder.recoverBuilder(context, notification).setGroup(DownloadUtil.DOWNLOAD_NOTIFICATION_SUCCESSFUL_GROUP).build(); NotificationUtil.setNotification(this.context, successfulDownloadGroupNotificationId, successfulDownloadGroupNotification); - DownloaderManager.updateDatabase(download.request.id); + DownloaderManager.updateRequestDownload(download); } else if (download.state == Download.STATE_FAILED) { notification = notificationHelper.buildDownloadFailedNotification(context, R.drawable.ic_error, null, DownloaderManager.getDownloadNotificationMessage(download.request.id)); notification = Notification.Builder.recoverBuilder(context, notification).setGroup(DownloadUtil.DOWNLOAD_NOTIFICATION_FAILED_GROUP).build(); @@ -109,7 +109,7 @@ public class DownloaderService extends androidx.media3.exoplayer.offline.Downloa @Override public void onDownloadRemoved(@NonNull DownloadManager downloadManager, Download download) { - DownloaderManager.deleteDatabase(download.request.id); + DownloaderManager.removeRequestDownload(download); } } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java index b27b88a8..57962f23 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java +++ b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java @@ -293,9 +293,9 @@ public class MediaManager { getQueueRepository().setPlayingPausedTimestamp(mediaItem.mediaId, ms); } - public static void scrobble(MediaItem mediaItem) { + public static void scrobble(MediaItem mediaItem, boolean submission) { if (mediaItem != null && Preferences.isScrobblingEnabled()) { - getSongRepository().scrobble(mediaItem.mediaMetadata.extras.getString("id")); + getSongRepository().scrobble(mediaItem.mediaMetadata.extras.getString("id"), submission); } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/Subsonic.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/Subsonic.java index c401bca7..de4b36b7 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/Subsonic.java +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/Subsonic.java @@ -7,6 +7,7 @@ import com.cappielloantonio.tempo.subsonic.api.internetradio.InternetRadioClient import com.cappielloantonio.tempo.subsonic.api.mediaannotation.MediaAnnotationClient; import com.cappielloantonio.tempo.subsonic.api.medialibraryscanning.MediaLibraryScanningClient; import com.cappielloantonio.tempo.subsonic.api.mediaretrieval.MediaRetrievalClient; +import com.cappielloantonio.tempo.subsonic.api.open.OpenClient; import com.cappielloantonio.tempo.subsonic.api.playlist.PlaylistClient; import com.cappielloantonio.tempo.subsonic.api.podcast.PodcastClient; import com.cappielloantonio.tempo.subsonic.api.searching.SearchingClient; @@ -35,6 +36,7 @@ public class Subsonic { private BookmarksClient bookmarksClient; private InternetRadioClient internetRadioClient; private SharingClient sharingClient; + private OpenClient openClient; public Subsonic(SubsonicPreferences preferences) { this.preferences = preferences; @@ -128,6 +130,13 @@ public class Subsonic { return sharingClient; } + public OpenClient getOpenClient() { + if (openClient == null) { + openClient = new OpenClient(this); + } + return openClient; + } + public String getUrl() { String url = preferences.getServerUrl() + "/rest/"; return url.replace("//rest", "/rest"); diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/mediaannotation/MediaAnnotationClient.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/mediaannotation/MediaAnnotationClient.java index eb21b42b..c229aa89 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/mediaannotation/MediaAnnotationClient.java +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/mediaannotation/MediaAnnotationClient.java @@ -9,7 +9,7 @@ import com.cappielloantonio.tempo.subsonic.base.ApiResponse; import retrofit2.Call; public class MediaAnnotationClient { - private static final String TAG = "BrowsingClient"; + private static final String TAG = "MediaAnnotationClient"; private final Subsonic subsonic; private final MediaAnnotationService mediaAnnotationService; @@ -34,8 +34,8 @@ public class MediaAnnotationClient { return mediaAnnotationService.setRating(subsonic.getParams(), id, rating); } - public Call scrobble(String id) { + public Call scrobble(String id, boolean submission) { Log.d(TAG, "scrobble()"); - return mediaAnnotationService.scrobble(subsonic.getParams(), id); + return mediaAnnotationService.scrobble(subsonic.getParams(), id, submission); } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/mediaannotation/MediaAnnotationService.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/mediaannotation/MediaAnnotationService.java index af6aadf5..65c20e92 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/mediaannotation/MediaAnnotationService.java +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/mediaannotation/MediaAnnotationService.java @@ -20,5 +20,5 @@ public interface MediaAnnotationService { Call setRating(@QueryMap Map params, @Query("id") String id, @Query("rating") int rating); @GET("scrobble") - Call scrobble(@QueryMap Map params, @Query("id") String id); + Call scrobble(@QueryMap Map params, @Query("id") String id, @Query("submission") Boolean submission); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/medialibraryscanning/MediaLibraryScanningClient.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/medialibraryscanning/MediaLibraryScanningClient.java index 51b89751..f4485151 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/medialibraryscanning/MediaLibraryScanningClient.java +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/medialibraryscanning/MediaLibraryScanningClient.java @@ -9,7 +9,7 @@ import com.cappielloantonio.tempo.subsonic.base.ApiResponse; import retrofit2.Call; public class MediaLibraryScanningClient { - private static final String TAG = "SystemClient"; + private static final String TAG = "MediaLibraryScanningClient"; private final Subsonic subsonic; private final MediaLibraryScanningService mediaLibraryScanningService; diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/mediaretrieval/MediaRetrievalClient.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/mediaretrieval/MediaRetrievalClient.java index d14ff7d5..742a77d7 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/mediaretrieval/MediaRetrievalClient.java +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/mediaretrieval/MediaRetrievalClient.java @@ -9,7 +9,7 @@ import com.cappielloantonio.tempo.subsonic.base.ApiResponse; import retrofit2.Call; public class MediaRetrievalClient { - private static final String TAG = "BrowsingClient"; + private static final String TAG = "MediaRetrievalClient"; private final Subsonic subsonic; private final MediaRetrievalService mediaRetrievalService; diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/open/OpenClient.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/open/OpenClient.java new file mode 100644 index 00000000..b37b03bb --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/open/OpenClient.java @@ -0,0 +1,26 @@ +package com.cappielloantonio.tempo.subsonic.api.open; + +import android.util.Log; + +import com.cappielloantonio.tempo.subsonic.RetrofitClient; +import com.cappielloantonio.tempo.subsonic.Subsonic; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import retrofit2.Call; + +public class OpenClient { + private static final String TAG = "OpenClient"; + + private final Subsonic subsonic; + private final OpenService openService; + + public OpenClient(Subsonic subsonic) { + this.subsonic = subsonic; + this.openService = new RetrofitClient(subsonic).getRetrofit().create(OpenService.class); + } + + public Call getLyricsBySongId(String id) { + Log.d(TAG, "getLyricsBySongId()"); + return openService.getLyricsBySongId(subsonic.getParams(), id); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/open/OpenService.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/open/OpenService.java new file mode 100644 index 00000000..3122a97c --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/open/OpenService.java @@ -0,0 +1,15 @@ +package com.cappielloantonio.tempo.subsonic.api.open; + +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import java.util.Map; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Query; +import retrofit2.http.QueryMap; + +public interface OpenService { + @GET("getLyricsBySongId") + Call getLyricsBySongId(@QueryMap Map params, @Query("id") String id); +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/podcast/PodcastClient.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/podcast/PodcastClient.java index 766c7e62..ca7fab82 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/podcast/PodcastClient.java +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/podcast/PodcastClient.java @@ -9,7 +9,7 @@ import com.cappielloantonio.tempo.subsonic.base.ApiResponse; import retrofit2.Call; public class PodcastClient { - private static final String TAG = "SystemClient"; + private static final String TAG = "PodcastClient"; private final Subsonic subsonic; private final PodcastService podcastService; diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/system/SystemClient.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/system/SystemClient.java index 2d50c6eb..d4a6521a 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/system/SystemClient.java +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/system/SystemClient.java @@ -28,4 +28,9 @@ public class SystemClient { Log.d(TAG, "getLicense()"); return systemService.getLicense(subsonic.getParams()); } + + public Call getOpenSubsonicExtensions() { + Log.d(TAG, "getOpenSubsonicExtensions()"); + return systemService.getOpenSubsonicExtensions(subsonic.getParams()); + } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/system/SystemService.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/system/SystemService.java index 02c4d799..bc0f4c99 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/system/SystemService.java +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/system/SystemService.java @@ -14,4 +14,7 @@ public interface SystemService { @GET("getLicense") Call getLicense(@QueryMap Map params); + + @GET("getOpenSubsonicExtensions") + Call getOpenSubsonicExtensions(@QueryMap Map params); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/base/ApiResponse.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/base/ApiResponse.kt index c6715031..78745202 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/base/ApiResponse.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/base/ApiResponse.kt @@ -7,5 +7,5 @@ import com.google.gson.annotations.SerializedName @Keep class ApiResponse { @SerializedName("subsonic-response") - lateinit var subsonicResponse: SubsonicResponse; + lateinit var subsonicResponse: SubsonicResponse } \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Line.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Line.kt new file mode 100644 index 00000000..1809091e --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Line.kt @@ -0,0 +1,9 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class Line { + var start: Int? = null + lateinit var value: String +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/LyricsList.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/LyricsList.kt new file mode 100644 index 00000000..9c3b65e8 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/LyricsList.kt @@ -0,0 +1,8 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class LyricsList { + var structuredLyrics: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/OpenSubsonicExtension.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/OpenSubsonicExtension.kt new file mode 100644 index 00000000..dd170c83 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/OpenSubsonicExtension.kt @@ -0,0 +1,9 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class OpenSubsonicExtension { + var name: String? = null + var versions: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/StructuredLyrics.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/StructuredLyrics.kt new file mode 100644 index 00000000..d77b2e03 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/StructuredLyrics.kt @@ -0,0 +1,13 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class StructuredLyrics { + var displayArtist: String? = null + var displayTitle: String? = null + var lang: String? = null + var offset: Int = 0 + var synced: Boolean = false + var line: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SubsonicResponse.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SubsonicResponse.kt index 6a43c2da..f886f55c 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SubsonicResponse.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SubsonicResponse.kt @@ -51,4 +51,7 @@ class SubsonicResponse { var version: String? = null var type: String? = null var serverVersion: String? = null + var openSubsonic: Boolean? = null + var openSubsonicExtensions: List? = null + var lyricsList: LyricsList? = null } \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java index e40873e6..40574fce 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java @@ -70,6 +70,7 @@ public class MainActivity extends BaseActivity { init(); checkConnectionType(); + getOpenSubsonicExtensions(); } @Override @@ -94,7 +95,7 @@ public class MainActivity extends BaseActivity { @Override public void onBackPressed() { if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) - collapseBottomSheet(); + collapseBottomSheetDelayed(); else super.onBackPressed(); } @@ -118,7 +119,7 @@ public class MainActivity extends BaseActivity { bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback); fragmentManager.beginTransaction().replace(R.id.player_bottom_sheet, new PlayerBottomSheetFragment(), "PlayerBottomSheet").commit(); - setBottomSheetInPeek(mainViewModel.isQueueLoaded()); + checkBottomSheetAfterStateChanged(); } public void setBottomSheetInPeek(Boolean isVisible) { @@ -137,7 +138,13 @@ public class MainActivity extends BaseActivity { } } - public void collapseBottomSheet() { + private void checkBottomSheetAfterStateChanged() { + final Handler handler = new Handler(); + final Runnable runnable = () -> setBottomSheetInPeek(mainViewModel.isQueueLoaded()); + handler.postDelayed(runnable, 100); + } + + public void collapseBottomSheetDelayed() { final Handler handler = new Handler(); final Runnable runnable = () -> bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); handler.postDelayed(runnable, 100); @@ -328,11 +335,25 @@ public class MainActivity extends BaseActivity { private void pingServer() { if (Preferences.getToken() != null) { - mainViewModel.ping().observe(this, isPingSuccessfull -> { - if (!isPingSuccessfull && Preferences.showServerUnreachableDialog()) { + mainViewModel.ping().observe(this, subsonicResponse -> { + if (subsonicResponse == null && Preferences.showServerUnreachableDialog()) { ServerUnreachableDialog dialog = new ServerUnreachableDialog(); dialog.show(getSupportFragmentManager(), null); } + + if (subsonicResponse != null) { + Preferences.setOpenSubsonic(subsonicResponse.getOpenSubsonic() != null && subsonicResponse.getOpenSubsonic()); + } + }); + } + } + + private void getOpenSubsonicExtensions() { + if (Preferences.getToken() != null) { + mainViewModel.getOpenSubsonicExtensions().observe(this, openSubsonicExtensions -> { + if (openSubsonicExtensions != null) { + Preferences.setOpenSubsonicExtensions(openSubsonicExtensions); + } }); } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/BaseActivity.java b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/BaseActivity.java index 37fb3ebf..9eaa2c36 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/BaseActivity.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/BaseActivity.java @@ -6,6 +6,7 @@ import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.os.PowerManager; +import android.view.WindowManager; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; @@ -37,6 +38,7 @@ public class BaseActivity extends AppCompatActivity { initializeDownloader(); checkBatteryOptimization(); checkPermission(); + checkAlwaysOnDisplay(); } @Override @@ -66,6 +68,12 @@ public class BaseActivity extends AppCompatActivity { } } + private void checkAlwaysOnDisplay() { + if (Preferences.isDisplayAlwaysOn()) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } + private boolean detectBatteryOptimization() { String packageName = getPackageName(); PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumCatalogueAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumCatalogueAdapter.java index 9fedd9d1..8027578f 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumCatalogueAdapter.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumCatalogueAdapter.java @@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.adapter; import android.os.Bundle; import android.view.LayoutInflater; +import android.view.View; import android.view.ViewGroup; import android.widget.Filter; import android.widget.Filterable; @@ -23,6 +24,9 @@ import java.util.List; public class AlbumCatalogueAdapter extends RecyclerView.Adapter implements Filterable { private final ClickCallback click; + private String currentFilter; + private boolean showArtist; + private final Filter filtering = new Filter() { @Override protected FilterResults performFiltering(CharSequence constraint) { @@ -32,6 +36,7 @@ public class AlbumCatalogueAdapter extends RecyclerView.Adapter) results.values; notifyDataSetChanged(); } }; @@ -57,9 +61,12 @@ public class AlbumCatalogueAdapter extends RecyclerView.Adapter albums; private List albumsFull; - public AlbumCatalogueAdapter(ClickCallback click) { + public AlbumCatalogueAdapter(ClickCallback click, boolean showArtist) { this.click = click; this.albums = Collections.emptyList(); + this.albumsFull = Collections.emptyList(); + this.currentFilter = ""; + this.showArtist = showArtist; } @NonNull @@ -75,6 +82,7 @@ public class AlbumCatalogueAdapter extends RecyclerView.Adapter albums) { - this.albums = albums; this.albumsFull = new ArrayList<>(albums); - notifyDataSetChanged(); + filtering.filter(currentFilter); } @Override @@ -158,8 +165,12 @@ public class AlbumCatalogueAdapter extends RecyclerView.Adapter songs; + private List shuffling; private List grouped; public DownloadHorizontalAdapter(ClickCallback click) { @@ -82,6 +83,7 @@ public class DownloadHorizontalAdapter extends RecyclerView.Adapter(songs)); notifyDataSetChanged(); } @@ -90,6 +92,10 @@ public class DownloadHorizontalAdapter extends RecyclerView.Adapter getShuffling() { + return shuffling; + } + @Override public int getItemViewType(int position) { return position; @@ -136,6 +142,27 @@ public class DownloadHorizontalAdapter extends RecyclerView.Adapter shufflingSong(List songs) { + if (filterValue == null) { + return songs; + } + + switch (filterKey) { + case Constants.DOWNLOAD_TYPE_TRACK: + return songs.stream().filter(child -> child.getId().equals(filterValue)).collect(Collectors.toList()); + case Constants.DOWNLOAD_TYPE_ALBUM: + return songs.stream().filter(child -> Objects.equals(child.getAlbumId(), filterValue)).collect(Collectors.toList()); + case Constants.DOWNLOAD_TYPE_GENRE: + return songs.stream().filter(child -> Objects.equals(child.getGenre(), filterValue)).collect(Collectors.toList()); + case Constants.DOWNLOAD_TYPE_YEAR: + return songs.stream().filter(child -> Objects.equals(child.getYear(), Integer.valueOf(filterValue))).collect(Collectors.toList()); + case Constants.DOWNLOAD_TYPE_ARTIST: + return songs.stream().filter(child -> Objects.equals(child.getArtistId(), filterValue)).collect(Collectors.toList()); + default: + return songs; + } + } + private String countSong(String filterKey, String filterValue, List songs) { if (filterValue != null) { switch (filterKey) { @@ -159,7 +186,15 @@ public class DownloadHorizontalAdapter extends RecyclerView.Adapter { + private List sectors; + + public HomeSectorHorizontalAdapter() { + this.sectors = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemHorizontalHomeSectorBinding view = ItemHorizontalHomeSectorBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + HomeSector sector = sectors.get(position); + + holder.item.homeSectorTitleCheckBox.setText(sector.getSectorTitle()); + holder.item.homeSectorTitleCheckBox.setChecked(sector.isVisible()); + } + + @Override + public int getItemCount() { + return sectors.size(); + } + + public List getItems() { + return this.sectors; + } + + public void setItems(List sectors) { + this.sectors = sectors; + notifyDataSetChanged(); + } + + public HomeSector getItem(int id) { + return sectors.get(id); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemHorizontalHomeSectorBinding item; + + ViewHolder(ItemHorizontalHomeSectorBinding item) { + super(item.getRoot()); + + this.item = item; + + this.item.homeSectorTitleCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> onCheck(isChecked)); + } + + private void onCheck(boolean isChecked) { + sectors.get(getBindingAdapterPosition()).setVisible(isChecked); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PlayerSongQueueAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PlayerSongQueueAdapter.java index a0defa1e..d0b9764d 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PlayerSongQueueAdapter.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PlayerSongQueueAdapter.java @@ -2,9 +2,11 @@ package com.cappielloantonio.tempo.ui.adapter; import android.os.Bundle; import android.view.LayoutInflater; +import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; +import androidx.appcompat.content.res.AppCompatResources; import androidx.media3.session.MediaBrowser; import androidx.recyclerview.widget.RecyclerView; @@ -17,6 +19,7 @@ import com.cappielloantonio.tempo.service.MediaManager; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.util.Preferences; import com.google.common.util.concurrent.ListenableFuture; import java.util.ArrayList; @@ -46,7 +49,14 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter= 1 ? R.drawable.ic_star : R.drawable.ic_star_outlined)); + holder.item.twoStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 2 ? R.drawable.ic_star : R.drawable.ic_star_outlined)); + holder.item.threeStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 3 ? R.drawable.ic_star : R.drawable.ic_star_outlined)); + holder.item.fourStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 4 ? R.drawable.ic_star : R.drawable.ic_star_outlined)); + holder.item.fiveStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 5 ? R.drawable.ic_star : R.drawable.ic_star_outlined)); + } + } else { + holder.item.ratingIndicatorImageView.setVisibility(View.GONE); + } } public List getItems() { diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/SongHorizontalAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/SongHorizontalAdapter.java index 367660ce..c2e1d7d6 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/SongHorizontalAdapter.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/SongHorizontalAdapter.java @@ -6,6 +6,7 @@ import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; +import androidx.appcompat.content.res.AppCompatResources; import androidx.media3.common.util.UnstableApi; import androidx.recyclerview.widget.RecyclerView; @@ -17,6 +18,7 @@ import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.DownloadUtil; import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.util.Preferences; import java.util.ArrayList; import java.util.Collections; @@ -49,13 +51,26 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter 0 && songs.get(position - 1) != null && songs.get(position - 1).getDiscNumber() != null && songs.get(position).getDiscNumber() != null && songs.get(position - 1).getDiscNumber() < songs.get(position).getDiscNumber())) { holder.item.differentDiskDivider.setVisibility(View.VISIBLE); } + + if (Preferences.showItemRating()) { + if (song.getStarred() == null && song.getUserRating() == null) { + holder.item.ratingIndicatorImageView.setVisibility(View.GONE); + } + + holder.item.preferredIcon.setVisibility(song.getStarred() != null ? View.VISIBLE : View.GONE); + holder.item.ratingBarLayout.setVisibility(song.getUserRating() != null ? View.VISIBLE : View.GONE); + + if (song.getUserRating() != null) { + holder.item.oneStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 1 ? R.drawable.ic_star : R.drawable.ic_star_outlined)); + holder.item.twoStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 2 ? R.drawable.ic_star : R.drawable.ic_star_outlined)); + holder.item.threeStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 3 ? R.drawable.ic_star : R.drawable.ic_star_outlined)); + holder.item.fourStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 4 ? R.drawable.ic_star : R.drawable.ic_star_outlined)); + holder.item.fiveStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 5 ? R.drawable.ic_star : R.drawable.ic_star_outlined)); + } + } else { + holder.item.ratingIndicatorImageView.setVisibility(View.GONE); + } } @Override diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/HomeRearrangementDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/HomeRearrangementDialog.java new file mode 100644 index 00000000..8ed67e42 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/HomeRearrangementDialog.java @@ -0,0 +1,115 @@ +package com.cappielloantonio.tempo.ui.dialog; + +import android.app.Dialog; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.DialogHomeRearrangementBinding; +import com.cappielloantonio.tempo.ui.adapter.HomeSectorHorizontalAdapter; +import com.cappielloantonio.tempo.viewmodel.HomeRearrangementViewModel; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.util.Collections; +import java.util.Objects; + +public class HomeRearrangementDialog extends DialogFragment { + private DialogHomeRearrangementBinding bind; + private HomeRearrangementViewModel homeRearrangementViewModel; + private HomeSectorHorizontalAdapter homeSectorHorizontalAdapter; + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + bind = DialogHomeRearrangementBinding.inflate(getLayoutInflater()); + + homeRearrangementViewModel = new ViewModelProvider(requireActivity()).get(HomeRearrangementViewModel.class); + + return new MaterialAlertDialogBuilder(requireContext()) + .setView(bind.getRoot()) + .setTitle(R.string.home_rearrangement_dialog_title) + .setPositiveButton(R.string.home_rearrangement_dialog_positive_button, (dialog, id) -> { }) + .setNeutralButton(R.string.home_rearrangement_dialog_neutral_button, (dialog, id) -> { }) + .setNegativeButton(R.string.home_rearrangement_dialog_negative_button, (dialog, id) -> dialog.cancel()) + .create(); + } + + @Override + public void onStart() { + super.onStart(); + + setButtonAction(); + initSectorView(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + homeRearrangementViewModel.closeDialog(); + bind = null; + } + + private void setButtonAction() { + androidx.appcompat.app.AlertDialog alertDialog = (androidx.appcompat.app.AlertDialog) Objects.requireNonNull(getDialog()); + + alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> { + homeRearrangementViewModel.saveHomeSectorList(homeSectorHorizontalAdapter.getItems()); + dismiss(); + }); + + alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> { + homeRearrangementViewModel.resetHomeSectorList(); + dismiss(); + }); + } + + private void initSectorView() { + bind.homeSectorItemRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.homeSectorItemRecyclerView.setHasFixedSize(true); + + homeSectorHorizontalAdapter = new HomeSectorHorizontalAdapter(); + bind.homeSectorItemRecyclerView.setAdapter(homeSectorHorizontalAdapter); + homeSectorHorizontalAdapter.setItems(homeRearrangementViewModel.getHomeSectorList()); + + new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) { + int originalPosition = -1; + int fromPosition = -1; + int toPosition = -1; + + @Override + public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { + if (originalPosition == -1) originalPosition = viewHolder.getBindingAdapterPosition(); + + fromPosition = viewHolder.getBindingAdapterPosition(); + toPosition = target.getBindingAdapterPosition(); + + Collections.swap(homeSectorHorizontalAdapter.getItems(), fromPosition, toPosition); + Objects.requireNonNull(recyclerView.getAdapter()).notifyItemMoved(fromPosition, toPosition); + + return false; + } + + @Override + public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { + super.clearView(recyclerView, viewHolder); + + homeRearrangementViewModel.orderSectorLiveListAfterSwap(homeSectorHorizontalAdapter.getItems()); + + originalPosition = -1; + fromPosition = -1; + toPosition = -1; + } + + @Override + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { + + } + } + ).attachToRecyclerView(bind.homeSectorItemRecyclerView); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PlaylistChooserDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PlaylistChooserDialog.java index 8ce5c48e..f0266c84 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PlaylistChooserDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PlaylistChooserDialog.java @@ -83,7 +83,7 @@ public class PlaylistChooserDialog extends DialogFragment implements ClickCallba playlistChooserViewModel.getPlaylistList(requireActivity()).observe(requireActivity(), playlists -> { if (playlists != null) { - if (playlists.size() > 0) { + if (!playlists.isEmpty()) { if (bind != null) bind.noPlaylistsCreatedTextView.setVisibility(View.GONE); if (bind != null) bind.playlistDialogRecyclerView.setVisibility(View.VISIBLE); playlistDialogHorizontalAdapter.setItems(playlists); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PlaylistEditorDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PlaylistEditorDialog.java index 9a5c759f..edc949c9 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PlaylistEditorDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PlaylistEditorDialog.java @@ -31,7 +31,8 @@ import java.util.Objects; public class PlaylistEditorDialog extends DialogFragment { private DialogPlaylistEditorBinding bind; private PlaylistEditorViewModel playlistEditorViewModel; - private PlaylistCallback playlistCallback; + + private final PlaylistCallback playlistCallback; private String playlistName; private PlaylistDialogSongHorizontalAdapter playlistDialogSongHorizontalAdapter; diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PodcastChannelEditorDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PodcastChannelEditorDialog.java index 09d92b3c..2226ab85 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PodcastChannelEditorDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PodcastChannelEditorDialog.java @@ -20,7 +20,8 @@ import java.util.Objects; public class PodcastChannelEditorDialog extends DialogFragment { private DialogPodcastChannelEditorBinding bind; private PodcastChannelEditorViewModel podcastChannelEditorViewModel; - private PodcastCallback podcastCallback; + + private final PodcastCallback podcastCallback; private String channelUrl; diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/RadioEditorDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/RadioEditorDialog.java index 38cb7a82..b4aba968 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/RadioEditorDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/RadioEditorDialog.java @@ -21,7 +21,8 @@ import java.util.Objects; public class RadioEditorDialog extends DialogFragment { private DialogRadioEditorBinding bind; private RadioEditorViewModel radioEditorViewModel; - private RadioCallback radioCallback; + + private final RadioCallback radioCallback; private String radioName; private String radioStreamURL; diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/TrackInfoDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/TrackInfoDialog.java index aad842c3..ca9b6d20 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/TrackInfoDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/TrackInfoDialog.java @@ -17,7 +17,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; public class TrackInfoDialog extends DialogFragment { private DialogTrackInfoBinding bind; - private MediaMetadata mediaMetadata; + + private final MediaMetadata mediaMetadata; public TrackInfoDialog(MediaMetadata mediaMetadata) { this.mediaMetadata = mediaMetadata; @@ -28,7 +29,7 @@ public class TrackInfoDialog extends DialogFragment { public Dialog onCreateDialog(Bundle savedInstanceState) { bind = DialogTrackInfoBinding.inflate(getLayoutInflater()); - return new MaterialAlertDialogBuilder(getActivity()) + return new MaterialAlertDialogBuilder(requireActivity()) .setView(bind.getRoot()) .setPositiveButton(R.string.track_info_dialog_positive_button, (dialog, id) -> dialog.cancel()) .create(); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumCatalogueFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumCatalogueFragment.java index 3ccf2c99..e33b5142 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumCatalogueFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumCatalogueFragment.java @@ -52,6 +52,12 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback { initData(); } + @Override + public void onDestroy() { + super.onDestroy(); + albumCatalogueViewModel.stopLoading(); + } + @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { activity = (MainActivity) getActivity(); @@ -61,6 +67,7 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback { initAppBar(); initAlbumCatalogueView(); + initProgressLoader(); return view; } @@ -73,7 +80,7 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback { private void initData() { albumCatalogueViewModel = new ViewModelProvider(requireActivity()).get(AlbumCatalogueViewModel.class); - albumCatalogueViewModel.loadAlbums(500); + albumCatalogueViewModel.loadAlbums(); } private void initAppBar() { @@ -105,7 +112,7 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback { bind.albumCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false)); bind.albumCatalogueRecyclerView.setHasFixedSize(true); - albumAdapter = new AlbumCatalogueAdapter(this); + albumAdapter = new AlbumCatalogueAdapter(this, true); albumAdapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY); bind.albumCatalogueRecyclerView.setAdapter(albumAdapter); albumCatalogueViewModel.getAlbumList().observe(getViewLifecycleOwner(), albums -> albumAdapter.setItems(albums)); @@ -118,6 +125,18 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback { bind.albumListSortImageView.setOnClickListener(view -> showPopupMenu(view, R.menu.sort_album_popup_menu)); } + private void initProgressLoader() { + albumCatalogueViewModel.getLoadingStatus().observe(getViewLifecycleOwner(), isLoading -> { + if (isLoading) { + bind.albumListSortImageView.setEnabled(false); + bind.albumListProgressLoader.setVisibility(View.VISIBLE); + } else { + bind.albumListSortImageView.setEnabled(true); + bind.albumListProgressLoader.setVisibility(View.GONE); + } + }); + } + @Override public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { inflater.inflate(R.menu.toolbar_menu, menu); @@ -165,6 +184,9 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback { } else if (menuItem.getItemId() == R.id.menu_album_sort_random) { albumAdapter.sort(Constants.ALBUM_ORDER_BY_RANDOM); return true; + } else if (menuItem.getItemId() == R.id.menu_album_sort_recently_added) { + albumAdapter.sort(Constants.ALBUM_ORDER_BY_RECENTLY_ADDED); + return true; } return false; diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumPageFragment.java index 1bba506b..99b797fe 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumPageFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumPageFragment.java @@ -34,6 +34,7 @@ import com.cappielloantonio.tempo.util.DownloadUtil; import com.cappielloantonio.tempo.util.MappingUtil; import com.cappielloantonio.tempo.util.MusicUtil; import com.cappielloantonio.tempo.viewmodel.AlbumPageViewModel; +import com.google.android.material.chip.Chip; import com.google.common.util.concurrent.ListenableFuture; import java.util.Collections; @@ -73,6 +74,7 @@ public class AlbumPageFragment extends Fragment implements ClickCallback { init(); initAppBar(); initAlbumInfoTextButton(); + initAlbumNotes(); initMusicButton(); initBackCover(); initSongsView(); @@ -131,10 +133,20 @@ public class AlbumPageFragment extends Fragment implements ClickCallback { bind.albumNameLabel.setText(MusicUtil.getReadableString(albumPageViewModel.getAlbum().getName())); bind.albumArtistLabel.setText(MusicUtil.getReadableString(albumPageViewModel.getAlbum().getArtist())); bind.albumReleaseYearLabel.setText(albumPageViewModel.getAlbum().getYear() != 0 ? String.valueOf(albumPageViewModel.getAlbum().getYear()) : ""); + bind.albumSongCountDurationTextview.setText(getString(R.string.album_page_tracks_count_and_duration, albumPageViewModel.getAlbum().getSongCount(), albumPageViewModel.getAlbum().getDuration() != null ? albumPageViewModel.getAlbum().getDuration() / 60 : 0)); + bind.albumGenresTextview.setText(albumPageViewModel.getAlbum().getGenre()); bind.animToolbar.setNavigationOnClickListener(v -> activity.navController.navigateUp()); Objects.requireNonNull(bind.animToolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null)); + + bind.albumOtherInfoButton.setOnClickListener(v -> { + if (bind.albumDetailView.getVisibility() == View.GONE) { + bind.albumDetailView.setVisibility(View.VISIBLE); + } else if (bind.albumDetailView.getVisibility() == View.VISIBLE) { + bind.albumDetailView.setVisibility(View.GONE); + } + }); } private void initAlbumInfoTextButton() { @@ -148,6 +160,18 @@ public class AlbumPageFragment extends Fragment implements ClickCallback { })); } + private void initAlbumNotes() { + albumPageViewModel.getAlbumInfo().observe(getViewLifecycleOwner(), albumInfo -> { + if (albumInfo != null) { + if (bind != null) bind.albumNotesTextview.setVisibility(View.VISIBLE); + if (bind != null) + bind.albumNotesTextview.setText(MusicUtil.getReadableString(albumInfo.getNotes())); + } else { + if (bind != null) bind.albumNotesTextview.setVisibility(View.GONE); + } + }); + } + private void initMusicButton() { albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> { if (bind != null && !songs.isEmpty()) { diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java index 0e4f050c..23c034fd 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java @@ -27,16 +27,23 @@ import com.cappielloantonio.tempo.helper.recyclerview.GridItemDecoration; import com.cappielloantonio.tempo.interfaces.ClickCallback; import com.cappielloantonio.tempo.service.MediaManager; import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.ui.activity.MainActivity; import com.cappielloantonio.tempo.ui.adapter.AlbumArtistPageOrSimilarAdapter; import com.cappielloantonio.tempo.ui.adapter.AlbumCatalogueAdapter; +import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter; import com.cappielloantonio.tempo.ui.adapter.ArtistSimilarAdapter; import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter; import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.util.Preferences; import com.cappielloantonio.tempo.viewmodel.ArtistPageViewModel; import com.google.common.util.concurrent.ListenableFuture; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + @UnstableApi public class ArtistPageFragment extends Fragment implements ClickCallback { private FragmentArtistPageBinding bind; @@ -44,9 +51,8 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { private ArtistPageViewModel artistPageViewModel; private SongHorizontalAdapter songHorizontalAdapter; - private AlbumArtistPageOrSimilarAdapter albumArtistPageOrSimilarAdapter; private AlbumCatalogueAdapter albumCatalogueAdapter; - private ArtistSimilarAdapter artistSimilarAdapter; + private ArtistCatalogueAdapter artistCatalogueAdapter; private ListenableFuture mediaBrowserListenableFuture; @@ -63,8 +69,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { initArtistInfo(); initPlayButtons(); initTopSongsView(); - initHorizontalAlbumsView(); - initVerticalAlbumsView(); + initAlbumsView(); initSimilarArtistsView(); return view; @@ -98,13 +103,6 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { bundle.putParcelable(Constants.ARTIST_OBJECT, artistPageViewModel.getArtist()); activity.navController.navigate(R.id.action_artistPageFragment_to_songListPageFragment, bundle); }); - - bind.artistPageAlbumsSwitchLayoutTextViewClickable.setOnClickListener(view -> { - boolean isHorizontalRecyclerViewVisible = bind.albumsHorizontalRecyclerView.getVisibility() == View.VISIBLE; - - bind.albumsHorizontalRecyclerView.setVisibility(isHorizontalRecyclerViewVisible ? View.GONE : View.VISIBLE); - bind.albumsVerticalRecyclerView.setVisibility(isHorizontalRecyclerViewVisible ? View.VISIBLE : View.GONE); - }); } private void initAppBar() { @@ -120,8 +118,6 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { private void initArtistInfo() { artistPageViewModel.getArtistInfo(artistPageViewModel.getArtist().getId()).observe(getViewLifecycleOwner(), artistInfo -> { if (artistInfo == null) { - if (bind != null) - bind.artistPageBioPlaceholder.placeholder.setVisibility(View.VISIBLE); if (bind != null) bind.artistPageBioSector.setVisibility(View.GONE); } else { String normalizedBio = MusicUtil.forceReadableString(artistInfo.getBiography()); @@ -144,8 +140,6 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { startActivity(intent); }); - if (bind != null) - bind.artistPageBioPlaceholder.placeholder.setVisibility(View.GONE); if (bind != null) bind.artistPageBioSector.setVisibility(View.VISIBLE); } }); @@ -154,7 +148,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { private void initPlayButtons() { bind.artistPageShuffleButton.setOnClickListener(v -> { artistPageViewModel.getArtistShuffleList().observe(getViewLifecycleOwner(), songs -> { - if (songs.size() > 0) { + if (!songs.isEmpty()) { MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); activity.setBottomSheetInPeek(true); } else { @@ -165,7 +159,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { bind.artistPageRadioButton.setOnClickListener(v -> { artistPageViewModel.getArtistInstantMix().observe(getViewLifecycleOwner(), songs -> { - if (songs.size() > 0) { + if (!songs.isEmpty()) { MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); activity.setBottomSheetInPeek(true); } else { @@ -182,58 +176,29 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { bind.mostStreamedSongRecyclerView.setAdapter(songHorizontalAdapter); artistPageViewModel.getArtistTopSongList().observe(getViewLifecycleOwner(), songs -> { if (songs == null) { - if (bind != null) - bind.artistPageTopTracksPlaceholder.placeholder.setVisibility(View.VISIBLE); if (bind != null) bind.artistPageTopSongsSector.setVisibility(View.GONE); } else { - if (bind != null) - bind.artistPageTopTracksPlaceholder.placeholder.setVisibility(View.GONE); if (bind != null) bind.artistPageTopSongsSector.setVisibility(!songs.isEmpty() ? View.VISIBLE : View.GONE); + if (bind != null) + bind.artistPageShuffleButton.setEnabled(!songs.isEmpty()); songHorizontalAdapter.setItems(songs); } }); } - private void initHorizontalAlbumsView() { - bind.albumsHorizontalRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); + private void initAlbumsView() { + bind.albumsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2)); + bind.albumsRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false)); + bind.albumsRecyclerView.setHasFixedSize(true); - albumArtistPageOrSimilarAdapter = new AlbumArtistPageOrSimilarAdapter(this); - bind.albumsHorizontalRecyclerView.setAdapter(albumArtistPageOrSimilarAdapter); - artistPageViewModel.getAlbumList().observe(getViewLifecycleOwner(), albums -> { - if (albums == null) { - if (bind != null) - bind.artistPageAlbumPlaceholder.placeholder.setVisibility(View.VISIBLE); - if (bind != null) bind.artistPageAlbumsSector.setVisibility(View.GONE); - } else { - if (bind != null) - bind.artistPageAlbumPlaceholder.placeholder.setVisibility(View.GONE); - if (bind != null) - bind.artistPageAlbumsSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE); - albumArtistPageOrSimilarAdapter.setItems(albums); - } - }); - - CustomLinearSnapHelper albumSnapHelper = new CustomLinearSnapHelper(); - albumSnapHelper.attachToRecyclerView(bind.albumsHorizontalRecyclerView); - } - - private void initVerticalAlbumsView() { - bind.albumsVerticalRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2)); - bind.albumsVerticalRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false)); - bind.albumsVerticalRecyclerView.setHasFixedSize(true); - - albumCatalogueAdapter = new AlbumCatalogueAdapter(this); - bind.albumsVerticalRecyclerView.setAdapter(albumCatalogueAdapter); + albumCatalogueAdapter = new AlbumCatalogueAdapter(this, false); + bind.albumsRecyclerView.setAdapter(albumCatalogueAdapter); artistPageViewModel.getAlbumList().observe(getViewLifecycleOwner(), albums -> { if (albums == null) { - if (bind != null) - bind.artistPageAlbumPlaceholder.placeholder.setVisibility(View.VISIBLE); if (bind != null) bind.artistPageAlbumsSector.setVisibility(View.GONE); } else { - if (bind != null) - bind.artistPageAlbumPlaceholder.placeholder.setVisibility(View.GONE); if (bind != null) bind.artistPageAlbumsSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE); albumCatalogueAdapter.setItems(albums); @@ -242,22 +207,27 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { } private void initSimilarArtistsView() { - bind.similarArtistsRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); + bind.similarArtistsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2)); + bind.similarArtistsRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false)); bind.similarArtistsRecyclerView.setHasFixedSize(true); - artistSimilarAdapter = new ArtistSimilarAdapter(this); - bind.similarArtistsRecyclerView.setAdapter(artistSimilarAdapter); + artistCatalogueAdapter = new ArtistCatalogueAdapter(this); + bind.similarArtistsRecyclerView.setAdapter(artistCatalogueAdapter); + artistPageViewModel.getArtistInfo(artistPageViewModel.getArtist().getId()).observe(getViewLifecycleOwner(), artist -> { if (artist == null) { - if (bind != null) - bind.artistPageSimilarArtistPlaceholder.placeholder.setVisibility(View.VISIBLE); if (bind != null) bind.similarArtistSector.setVisibility(View.GONE); } else { - if (bind != null) - bind.artistPageSimilarArtistPlaceholder.placeholder.setVisibility(View.GONE); - if (bind != null) + if (bind != null && artist.getSimilarArtists() != null) bind.similarArtistSector.setVisibility(!artist.getSimilarArtists().isEmpty() ? View.VISIBLE : View.GONE); - artistSimilarAdapter.setItems(artist.getSimilarArtists()); + + List artists = new ArrayList<>(); + + if (artist.getSimilarArtists() != null) { + artists.addAll(artist.getSimilarArtists()); + } + + artistCatalogueAdapter.setItems(artists); } }); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java index 83995d8a..b8108fe3 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java @@ -33,6 +33,7 @@ import com.cappielloantonio.tempo.viewmodel.DownloadViewModel; import com.google.android.material.appbar.MaterialToolbar; import com.google.common.util.concurrent.ListenableFuture; +import java.util.Collections; import java.util.List; import java.util.Objects; @@ -110,20 +111,14 @@ public class DownloadFragment extends Fragment implements ClickCallback { if (bind != null) { bind.emptyDownloadLayout.setVisibility(View.VISIBLE); bind.fragmentDownloadNestedScrollView.setVisibility(View.GONE); - - bind.downloadDownloadedPlaceholder.placeholder.setVisibility(View.VISIBLE); bind.downloadDownloadedSector.setVisibility(View.GONE); - bind.downloadedGroupByImageView.setVisibility(View.GONE); } } else { if (bind != null) { bind.emptyDownloadLayout.setVisibility(View.GONE); bind.fragmentDownloadNestedScrollView.setVisibility(View.VISIBLE); - - bind.downloadDownloadedPlaceholder.placeholder.setVisibility(View.GONE); bind.downloadDownloadedSector.setVisibility(View.VISIBLE); - bind.downloadedGroupByImageView.setVisibility(View.VISIBLE); finishDownloadView(songs); @@ -165,6 +160,19 @@ public class DownloadFragment extends Fragment implements ClickCallback { bind.downloadedGoBackImageView.setVisibility(stack.size() > 1 ? View.VISIBLE : View.GONE); setupBackPressing(stack.size()); + setupShuffleButton(); + }); + } + + private void setupShuffleButton() { + bind.shuffleDownloadedTextViewClickable.setOnClickListener(view -> { + List songs = downloadHorizontalAdapter.getShuffling(); + + if (songs != null && !songs.isEmpty()) { + Collections.shuffle(songs); + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + activity.setBottomSheetInPeek(true); + } }); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeTabMusicFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeTabMusicFragment.java index 251699f0..a4cd411f 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeTabMusicFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeTabMusicFragment.java @@ -32,6 +32,7 @@ import com.cappielloantonio.tempo.helper.recyclerview.DotsIndicatorDecoration; import com.cappielloantonio.tempo.helper.recyclerview.GridItemDecoration; import com.cappielloantonio.tempo.interfaces.ClickCallback; import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.model.HomeSector; import com.cappielloantonio.tempo.service.DownloaderManager; import com.cappielloantonio.tempo.service.MediaManager; import com.cappielloantonio.tempo.service.MediaService; @@ -48,6 +49,7 @@ import com.cappielloantonio.tempo.ui.adapter.ShareHorizontalAdapter; import com.cappielloantonio.tempo.ui.adapter.SimilarTrackAdapter; import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter; import com.cappielloantonio.tempo.ui.adapter.YearAdapter; +import com.cappielloantonio.tempo.ui.dialog.HomeRearrangementDialog; import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.DownloadUtil; import com.cappielloantonio.tempo.util.MappingUtil; @@ -119,6 +121,9 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { initRecentAddedAlbumView(); initGridView(); initSharesView(); + initHomeReorganizer(); + + reorder(); } @Override @@ -156,7 +161,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { homeViewModel.getRandomShuffleSample().observe(getViewLifecycleOwner(), songs -> { MusicUtil.ratingFilter(songs); - if (songs.size() > 0) { + if (!songs.isEmpty()) { MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); activity.setBottomSheetInPeek(true); } @@ -303,6 +308,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { } private void initDiscoverSongSlideView() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_DISCOVERY)) return; + bind.discoverSongViewPager.setOrientation(ViewPager2.ORIENTATION_HORIZONTAL); discoverSongAdapter = new DiscoverSongAdapter(this); @@ -312,12 +319,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { MusicUtil.ratingFilter(songs); if (songs == null) { - if (bind != null) - bind.homeDiscoveryPlaceholder.placeholder.setVisibility(View.VISIBLE); if (bind != null) bind.homeDiscoverSector.setVisibility(View.GONE); } else { - if (bind != null) - bind.homeDiscoveryPlaceholder.placeholder.setVisibility(View.GONE); if (bind != null) bind.homeDiscoverSector.setVisibility(!songs.isEmpty() ? View.VISIBLE : View.GONE); @@ -329,6 +332,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { } private void initSimilarSongView() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_MADE_FOR_YOU)) return; + bind.similarTracksRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); bind.similarTracksRecyclerView.setHasFixedSize(true); @@ -338,12 +343,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { MusicUtil.ratingFilter(songs); if (songs == null) { - if (bind != null) - bind.homeSimilarTracksPlaceholder.placeholder.setVisibility(View.VISIBLE); if (bind != null) bind.homeSimilarTracksSector.setVisibility(View.GONE); } else { - if (bind != null) - bind.homeSimilarTracksPlaceholder.placeholder.setVisibility(View.GONE); if (bind != null) bind.homeSimilarTracksSector.setVisibility(!songs.isEmpty() ? View.VISIBLE : View.GONE); @@ -356,6 +357,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { } private void initArtistBestOf() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_BEST_OF)) return; + bind.bestOfArtistRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); bind.bestOfArtistRecyclerView.setHasFixedSize(true); @@ -363,12 +366,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { bind.bestOfArtistRecyclerView.setAdapter(bestOfArtistAdapter); homeViewModel.getBestOfArtists(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), artists -> { if (artists == null) { - if (bind != null) - bind.homeBestOfArtistPlaceholder.placeholder.setVisibility(View.VISIBLE); if (bind != null) bind.homeBestOfArtistSector.setVisibility(View.GONE); } else { - if (bind != null) - bind.homeBestOfArtistPlaceholder.placeholder.setVisibility(View.GONE); if (bind != null) bind.homeBestOfArtistSector.setVisibility(!artists.isEmpty() ? View.VISIBLE : View.GONE); @@ -381,6 +380,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { } private void initArtistRadio() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_RADIO_STATION)) return; + bind.radioArtistRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); bind.radioArtistRecyclerView.setHasFixedSize(true); @@ -388,12 +389,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { bind.radioArtistRecyclerView.setAdapter(radioArtistAdapter); homeViewModel.getStarredArtistsSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), artists -> { if (artists == null) { - if (bind != null) - bind.homeRadioArtistPlaceholder.placeholder.setVisibility(View.VISIBLE); if (bind != null) bind.homeRadioArtistSector.setVisibility(View.GONE); } else { - if (bind != null) - bind.homeRadioArtistPlaceholder.placeholder.setVisibility(View.GONE); if (bind != null) bind.homeRadioArtistSector.setVisibility(!artists.isEmpty() ? View.VISIBLE : View.GONE); if (bind != null) @@ -408,6 +405,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { } private void initGridView() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_TOP_SONGS)) return; + bind.gridTracksRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 3)); bind.gridTracksRecyclerView.addItemDecoration(new GridItemDecoration(3, 8, false)); bind.gridTracksRecyclerView.setHasFixedSize(true); @@ -418,7 +417,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { homeViewModel.getDiscoverSongSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), music -> { if (music != null) { homeViewModel.getGridSongSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), chronologies -> { - if (chronologies == null || chronologies.size() == 0) { + if (chronologies == null || chronologies.isEmpty()) { if (bind != null) bind.homeGridTracksSector.setVisibility(View.GONE); if (bind != null) bind.afterGridDivider.setVisibility(View.GONE); } else { @@ -432,18 +431,16 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { } private void initStarredTracksView() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_STARRED_TRACKS)) return; + bind.starredTracksRecyclerView.setHasFixedSize(true); starredSongAdapter = new SongHorizontalAdapter(this, true, false); bind.starredTracksRecyclerView.setAdapter(starredSongAdapter); homeViewModel.getStarredTracks(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> { if (songs == null) { - if (bind != null) - bind.starredTracksPlaceholder.placeholder.setVisibility(View.VISIBLE); if (bind != null) bind.starredTracksSector.setVisibility(View.GONE); } else { - if (bind != null) - bind.starredTracksPlaceholder.placeholder.setVisibility(View.GONE); if (bind != null) bind.starredTracksSector.setVisibility(!songs.isEmpty() ? View.VISIBLE : View.GONE); if (bind != null) @@ -467,18 +464,16 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { } private void initStarredAlbumsView() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_STARRED_ALBUMS)) return; + bind.starredAlbumsRecyclerView.setHasFixedSize(true); starredAlbumAdapter = new AlbumHorizontalAdapter(this, false); bind.starredAlbumsRecyclerView.setAdapter(starredAlbumAdapter); homeViewModel.getStarredAlbums(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), albums -> { if (albums == null) { - if (bind != null) - bind.starredAlbumsPlaceholder.placeholder.setVisibility(View.VISIBLE); if (bind != null) bind.starredAlbumsSector.setVisibility(View.GONE); } else { - if (bind != null) - bind.starredAlbumsPlaceholder.placeholder.setVisibility(View.GONE); if (bind != null) bind.starredAlbumsSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE); if (bind != null) @@ -502,18 +497,16 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { } private void initStarredArtistsView() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_STARRED_ARTISTS)) return; + bind.starredArtistsRecyclerView.setHasFixedSize(true); starredArtistAdapter = new ArtistHorizontalAdapter(this); bind.starredArtistsRecyclerView.setAdapter(starredArtistAdapter); homeViewModel.getStarredArtists(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), artists -> { if (artists == null) { - if (bind != null) - bind.starredArtistsPlaceholder.placeholder.setVisibility(View.VISIBLE); if (bind != null) bind.starredArtistsSector.setVisibility(View.GONE); } else { - if (bind != null) - bind.starredArtistsPlaceholder.placeholder.setVisibility(View.GONE); if (bind != null) bind.starredArtistsSector.setVisibility(!artists.isEmpty() ? View.VISIBLE : View.GONE); if (bind != null) @@ -539,18 +532,16 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { } private void initNewReleasesView() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_NEW_RELEASES)) return; + bind.newReleasesRecyclerView.setHasFixedSize(true); newReleasesAlbumAdapter = new AlbumHorizontalAdapter(this, false); bind.newReleasesRecyclerView.setAdapter(newReleasesAlbumAdapter); homeViewModel.getRecentlyReleasedAlbums(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), albums -> { if (albums == null) { - if (bind != null) - bind.homeNewReleasesPlaceholder.placeholder.setVisibility(View.VISIBLE); if (bind != null) bind.homeNewReleasesSector.setVisibility(View.GONE); } else { - if (bind != null) - bind.homeNewReleasesPlaceholder.placeholder.setVisibility(View.GONE); if (bind != null) bind.homeNewReleasesSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE); if (bind != null) @@ -574,6 +565,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { } private void initYearSongView() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_FLASHBACK)) return; + bind.yearsRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); bind.yearsRecyclerView.setHasFixedSize(true); @@ -581,12 +574,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { bind.yearsRecyclerView.setAdapter(yearAdapter); homeViewModel.getYearList(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), years -> { if (years == null) { - if (bind != null) - bind.homeFlashbackPlaceholder.placeholder.setVisibility(View.VISIBLE); if (bind != null) bind.homeFlashbackSector.setVisibility(View.GONE); } else { - if (bind != null) - bind.homeFlashbackPlaceholder.placeholder.setVisibility(View.GONE); if (bind != null) bind.homeFlashbackSector.setVisibility(!years.isEmpty() ? View.VISIBLE : View.GONE); @@ -599,6 +588,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { } private void initMostPlayedAlbumView() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_MOST_PLAYED)) return; + bind.mostPlayedAlbumsRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); bind.mostPlayedAlbumsRecyclerView.setHasFixedSize(true); @@ -606,15 +597,10 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { bind.mostPlayedAlbumsRecyclerView.setAdapter(mostPlayedAlbumAdapter); homeViewModel.getMostPlayedAlbums(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), albums -> { if (albums == null) { - if (bind != null) - bind.homeMostPlayedAlbumsPlaceholder.placeholder.setVisibility(View.VISIBLE); if (bind != null) bind.homeMostPlayedAlbumsSector.setVisibility(View.GONE); } else { - if (bind != null) - bind.homeMostPlayedAlbumsPlaceholder.placeholder.setVisibility(View.GONE); if (bind != null) bind.homeMostPlayedAlbumsSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE); - // if (albums.size() < 5) reorder(); mostPlayedAlbumAdapter.setItems(albums); } @@ -625,6 +611,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { } private void initRecentPlayedAlbumView() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_LAST_PLAYED)) return; + bind.recentlyPlayedAlbumsRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); bind.recentlyPlayedAlbumsRecyclerView.setHasFixedSize(true); @@ -632,12 +620,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { bind.recentlyPlayedAlbumsRecyclerView.setAdapter(recentlyPlayedAlbumAdapter); homeViewModel.getRecentlyPlayedAlbumList(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), albums -> { if (albums == null) { - if (bind != null) - bind.homeRecentlyPlayedAlbumsPlaceholder.placeholder.setVisibility(View.VISIBLE); if (bind != null) bind.homeRecentlyPlayedAlbumsSector.setVisibility(View.GONE); } else { - if (bind != null) - bind.homeRecentlyPlayedAlbumsPlaceholder.placeholder.setVisibility(View.GONE); if (bind != null) bind.homeRecentlyPlayedAlbumsSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE); @@ -650,6 +634,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { } private void initRecentAddedAlbumView() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_RECENTLY_ADDED)) return; + bind.recentlyAddedAlbumsRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); bind.recentlyAddedAlbumsRecyclerView.setHasFixedSize(true); @@ -657,12 +643,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { bind.recentlyAddedAlbumsRecyclerView.setAdapter(recentlyAddedAlbumAdapter); homeViewModel.getMostRecentlyAddedAlbums(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), albums -> { if (albums == null) { - if (bind != null) - bind.homeRecentlyAddedAlbumsPlaceholder.placeholder.setVisibility(View.VISIBLE); if (bind != null) bind.homeRecentlyAddedAlbumsSector.setVisibility(View.GONE); } else { - if (bind != null) - bind.homeRecentlyAddedAlbumsPlaceholder.placeholder.setVisibility(View.GONE); if (bind != null) bind.homeRecentlyAddedAlbumsSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE); @@ -675,6 +657,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { } private void initSharesView() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_SHARED)) return; + bind.sharesRecyclerView.setHasFixedSize(true); shareHorizontalAdapter = new ShareHorizontalAdapter(this); @@ -682,11 +666,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { if (Preferences.isSharingEnabled()) { homeViewModel.getShares(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), shares -> { if (shares == null) { - if (bind != null) - bind.sharesPlaceholder.placeholder.setVisibility(View.VISIBLE); if (bind != null) bind.sharesSector.setVisibility(View.GONE); } else { - if (bind != null) bind.sharesPlaceholder.placeholder.setVisibility(View.GONE); if (bind != null) bind.sharesSector.setVisibility(!shares.isEmpty() ? View.VISIBLE : View.GONE); if (bind != null) @@ -710,6 +691,17 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { ); } + private void initHomeReorganizer() { + final Handler handler = new Handler(); + final Runnable runnable = () -> { if (bind != null) bind.homeSectorRearrangementButton.setVisibility(View.VISIBLE); }; + handler.postDelayed(runnable, 5000); + + bind.homeSectorRearrangementButton.setOnClickListener(v -> { + HomeRearrangementDialog dialog = new HomeRearrangementDialog(); + dialog.show(requireActivity().getSupportFragmentManager(), null); + }); + } + private void refreshSharesView() { final Handler handler = new Handler(); final Runnable runnable = () -> { @@ -735,6 +727,63 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { }); } + public void reorder() { + if (bind != null && homeViewModel.getHomeSectorList() != null) { + bind.homeLinearLayoutContainer.removeAllViews(); + + for (HomeSector sector : homeViewModel.getHomeSectorList()) { + if (!sector.isVisible()) continue; + + switch (sector.getId()) { + case Constants.HOME_SECTOR_DISCOVERY: + bind.homeLinearLayoutContainer.addView(bind.homeDiscoverSector); + break; + case Constants.HOME_SECTOR_MADE_FOR_YOU: + bind.homeLinearLayoutContainer.addView(bind.homeSimilarTracksSector); + break; + case Constants.HOME_SECTOR_BEST_OF: + bind.homeLinearLayoutContainer.addView(bind.homeBestOfArtistSector); + break; + case Constants.HOME_SECTOR_RADIO_STATION: + bind.homeLinearLayoutContainer.addView(bind.homeRadioArtistSector); + break; + case Constants.HOME_SECTOR_TOP_SONGS: + bind.homeLinearLayoutContainer.addView(bind.homeGridTracksSector); + break; + case Constants.HOME_SECTOR_STARRED_TRACKS: + bind.homeLinearLayoutContainer.addView(bind.starredTracksSector); + break; + case Constants.HOME_SECTOR_STARRED_ALBUMS: + bind.homeLinearLayoutContainer.addView(bind.starredAlbumsSector); + break; + case Constants.HOME_SECTOR_STARRED_ARTISTS: + bind.homeLinearLayoutContainer.addView(bind.starredArtistsSector); + break; + case Constants.HOME_SECTOR_NEW_RELEASES: + bind.homeLinearLayoutContainer.addView(bind.homeNewReleasesSector); + break; + case Constants.HOME_SECTOR_FLASHBACK: + bind.homeLinearLayoutContainer.addView(bind.homeFlashbackSector); + break; + case Constants.HOME_SECTOR_MOST_PLAYED: + bind.homeLinearLayoutContainer.addView(bind.homeMostPlayedAlbumsSector); + break; + case Constants.HOME_SECTOR_LAST_PLAYED: + bind.homeLinearLayoutContainer.addView(bind.homeRecentlyPlayedAlbumsSector); + break; + case Constants.HOME_SECTOR_RECENTLY_ADDED: + bind.homeLinearLayoutContainer.addView(bind.homeRecentlyAddedAlbumsSector); + break; + case Constants.HOME_SECTOR_SHARED: + bind.homeLinearLayoutContainer.addView(bind.sharesSector); + break; + } + } + + bind.homeLinearLayoutContainer.addView(bind.homeSectorRearrangementButton); + } + } + private void initializeMediaBrowser() { mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); } @@ -753,7 +802,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { homeViewModel.getMediaInstantMix(getViewLifecycleOwner(), bundle.getParcelable(Constants.TRACK_OBJECT)).observe(getViewLifecycleOwner(), songs -> { MusicUtil.ratingFilter(songs); - if (songs != null && songs.size() > 0) { + if (songs != null && !songs.isEmpty()) { MediaManager.enqueue(mediaBrowserListenableFuture, songs, true); } }); @@ -794,7 +843,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { homeViewModel.getArtistInstantMix(getViewLifecycleOwner(), bundle.getParcelable(Constants.ARTIST_OBJECT)).observe(getViewLifecycleOwner(), songs -> { MusicUtil.ratingFilter(songs); - if (songs.size() > 0) { + if (!songs.isEmpty()) { MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); activity.setBottomSheetInPeek(true); } @@ -805,7 +854,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { homeViewModel.getArtistBestOf(getViewLifecycleOwner(), bundle.getParcelable(Constants.ARTIST_OBJECT)).observe(getViewLifecycleOwner(), songs -> { MusicUtil.ratingFilter(songs); - if (songs.size() > 0) { + if (!songs.isEmpty()) { MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); activity.setBottomSheetInPeek(true); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LibraryFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LibraryFragment.java index b395ab77..711b1c6e 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LibraryFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LibraryFragment.java @@ -142,12 +142,8 @@ public class LibraryFragment extends Fragment implements ClickCallback { bind.musicFolderRecyclerView.setAdapter(musicFolderAdapter); libraryViewModel.getMusicFolders(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), musicFolders -> { if (musicFolders == null) { - if (bind != null) - bind.libraryMusicFolderPlaceholder.placeholder.setVisibility(View.VISIBLE); if (bind != null) bind.libraryMusicFolderSector.setVisibility(View.GONE); } else { - if (bind != null) - bind.libraryMusicFolderPlaceholder.placeholder.setVisibility(View.GONE); if (bind != null) bind.libraryMusicFolderSector.setVisibility(!musicFolders.isEmpty() ? View.VISIBLE : View.GONE); @@ -164,11 +160,8 @@ public class LibraryFragment extends Fragment implements ClickCallback { bind.albumRecyclerView.setAdapter(albumAdapter); libraryViewModel.getAlbumSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), albums -> { if (albums == null) { - if (bind != null) - bind.libraryAlbumPlaceholder.placeholder.setVisibility(View.VISIBLE); if (bind != null) bind.libraryAlbumSector.setVisibility(View.GONE); } else { - if (bind != null) bind.libraryAlbumPlaceholder.placeholder.setVisibility(View.GONE); if (bind != null) bind.libraryAlbumSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE); @@ -188,12 +181,8 @@ public class LibraryFragment extends Fragment implements ClickCallback { bind.artistRecyclerView.setAdapter(artistAdapter); libraryViewModel.getArtistSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), artists -> { if (artists == null) { - if (bind != null) - bind.libraryArtistPlaceholder.placeholder.setVisibility(View.VISIBLE); if (bind != null) bind.libraryArtistSector.setVisibility(View.GONE); } else { - if (bind != null) - bind.libraryArtistPlaceholder.placeholder.setVisibility(View.GONE); if (bind != null) bind.libraryArtistSector.setVisibility(!artists.isEmpty() ? View.VISIBLE : View.GONE); @@ -214,11 +203,8 @@ public class LibraryFragment extends Fragment implements ClickCallback { libraryViewModel.getGenreSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), genres -> { if (genres == null) { - if (bind != null) - bind.libraryGenrePlaceholder.placeholder.setVisibility(View.VISIBLE); if (bind != null) bind.libraryGenresSector.setVisibility(View.GONE); } else { - if (bind != null) bind.libraryGenrePlaceholder.placeholder.setVisibility(View.GONE); if (bind != null) bind.libraryGenresSector.setVisibility(!genres.isEmpty() ? View.VISIBLE : View.GONE); @@ -238,12 +224,8 @@ public class LibraryFragment extends Fragment implements ClickCallback { bind.playlistRecyclerView.setAdapter(playlistHorizontalAdapter); libraryViewModel.getPlaylistSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), playlists -> { if (playlists == null) { - if (bind != null) - bind.libraryPlaylistPlaceholder.placeholder.setVisibility(View.VISIBLE); if (bind != null) bind.libraryPlaylistSector.setVisibility(View.GONE); } else { - if (bind != null) - bind.libraryPlaylistPlaceholder.placeholder.setVisibility(View.GONE); if (bind != null) bind.libraryPlaylistSector.setVisibility(!playlists.isEmpty() ? View.VISIBLE : View.GONE); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LoginFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LoginFragment.java index f88e19b5..63f6fe3b 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LoginFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LoginFragment.java @@ -92,7 +92,7 @@ public class LoginFragment extends Fragment implements ClickCallback { serverAdapter = new ServerAdapter(this); bind.serverListRecyclerView.setAdapter(serverAdapter); loginViewModel.getServerList().observe(getViewLifecycleOwner(), servers -> { - if (servers.size() > 0) { + if (!servers.isEmpty()) { if (bind != null) bind.noServerAddedTextView.setVisibility(View.GONE); if (bind != null) bind.serverListRecyclerView.setVisibility(View.VISIBLE); serverAdapter.setItems(servers); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java index 761c6f3c..e618ec12 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java @@ -249,6 +249,11 @@ public class PlayerBottomSheetFragment extends Fragment { bind.playerBodyLayout.playerBodyBottomSheetViewPager.setCurrentItem(1, true); } + public void setPlayerControllerVerticalPagerDraggableState(Boolean isDraggable) { + ViewPager2 playerControllerVerticalPager = (ViewPager2) bind.playerBodyLayout.playerBodyBottomSheetViewPager; + playerControllerVerticalPager.setUserInputEnabled(isDraggable); + } + private void defineProgressBarHandler(MediaBrowser mediaBrowser) { progressBarHandler = new Handler(); progressBarRunnable = () -> { diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java index 08e36a4a..2b48c1ad 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java @@ -257,10 +257,20 @@ public class PlayerControllerFragment extends Fragment { public void onPageSelected(int position) { super.onPageSelected(position); + PlayerBottomSheetFragment playerBottomSheetFragment = (PlayerBottomSheetFragment) requireActivity().getSupportFragmentManager().findFragmentByTag("PlayerBottomSheet"); + if (position == 0) { activity.setBottomSheetDraggableState(true); + + if (playerBottomSheetFragment != null) { + playerBottomSheetFragment.setPlayerControllerVerticalPagerDraggableState(true); + } } else if (position == 1) { activity.setBottomSheetDraggableState(false); + + if (playerBottomSheetFragment != null) { + playerBottomSheetFragment.setPlayerControllerVerticalPagerDraggableState(false); + } } } }); @@ -296,7 +306,7 @@ public class PlayerControllerFragment extends Fragment { Bundle bundle = new Bundle(); bundle.putParcelable(Constants.ARTIST_OBJECT, artist); NavHostFragment.findNavController(this).navigate(R.id.artistPageFragment, bundle); - activity.collapseBottomSheet(); + activity.collapseBottomSheetDelayed(); }); } }); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerCoverFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerCoverFragment.java index d29b0f53..f44ed41e 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerCoverFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerCoverFragment.java @@ -80,7 +80,10 @@ public class PlayerCoverFragment extends Fragment { handler.removeCallbacksAndMessages(null); - final Runnable runnable = () -> bind.nowPlayingTapButton.setVisibility(View.GONE); + final Runnable runnable = () -> { + if (bind != null) bind.nowPlayingTapButton.setVisibility(View.GONE); + }; + handler.postDelayed(runnable, 10000); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java index a492014e..58e7a7c5 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java @@ -1,31 +1,60 @@ package com.cappielloantonio.tempo.ui.fragment; +import android.annotation.SuppressLint; +import android.content.ComponentName; import android.os.Bundle; +import android.os.Handler; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.ForegroundColorSpan; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.OptIn; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; +import com.cappielloantonio.tempo.R; import com.cappielloantonio.tempo.databinding.InnerFragmentPlayerLyricsBinding; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.subsonic.models.Line; +import com.cappielloantonio.tempo.subsonic.models.LyricsList; import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.util.OpenSubsonicExtensionsUtil; import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import java.util.List; + + +@OptIn(markerClass = UnstableApi.class) public class PlayerLyricsFragment extends Fragment { private static final String TAG = "PlayerLyricsFragment"; private InnerFragmentPlayerLyricsBinding bind; private PlayerBottomSheetViewModel playerBottomSheetViewModel; + private ListenableFuture mediaBrowserListenableFuture; + private MediaBrowser mediaBrowser; + private Handler syncLyricsHandler; + private Runnable syncLyricsRunnable; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { bind = InnerFragmentPlayerLyricsBinding.inflate(inflater, container, false); View view = bind.getRoot(); + playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class); + initOverlay(); + return view; } @@ -33,7 +62,32 @@ public class PlayerLyricsFragment extends Fragment { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - initLyrics(); + initPanelContent(); + } + + @Override + public void onStart() { + super.onStart(); + initializeBrowser(); + + } + + @Override + public void onResume() { + super.onResume(); + bindMediaController(); + } + + @Override + public void onPause() { + super.onPause(); + releaseHandler(); + } + + @Override + public void onStop() { + releaseBrowser(); + super.onStop(); } @Override @@ -42,27 +96,195 @@ public class PlayerLyricsFragment extends Fragment { bind = null; } - private void initLyrics() { - playerBottomSheetViewModel.getLiveLyrics().observe(getViewLifecycleOwner(), lyrics -> { - playerBottomSheetViewModel.getLiveDescription().observe(getViewLifecycleOwner(), description -> { - if (bind != null) { - if (lyrics != null && !lyrics.trim().equals("")) { - bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(lyrics)); - bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE); - bind.emptyDescriptionImageView.setVisibility(View.GONE); - bind.titleEmptyDescriptionLabel.setVisibility(View.GONE); - } else if (description != null && !description.trim().equals("")) { - bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(description)); - bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE); - bind.emptyDescriptionImageView.setVisibility(View.GONE); - bind.titleEmptyDescriptionLabel.setVisibility(View.GONE); - } else { - bind.nowPlayingSongLyricsTextView.setVisibility(View.GONE); - bind.emptyDescriptionImageView.setVisibility(View.VISIBLE); - bind.titleEmptyDescriptionLabel.setVisibility(View.VISIBLE); - } - } - }); + private void initOverlay() { + bind.syncLyricsTapButton.setOnClickListener(view -> { + playerBottomSheetViewModel.changeSyncLyricsState(); }); } + + private void initializeBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseHandler() { + if (syncLyricsHandler != null) { + syncLyricsHandler.removeCallbacks(syncLyricsRunnable); + syncLyricsHandler = null; + } + } + + private void releaseBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } + + private void bindMediaController() { + mediaBrowserListenableFuture.addListener(() -> { + try { + mediaBrowser = mediaBrowserListenableFuture.get(); + defineProgressHandler(); + } catch (Exception e) { + e.printStackTrace(); + } + }, MoreExecutors.directExecutor()); + } + + private void initPanelContent() { + if (OpenSubsonicExtensionsUtil.isSongLyricsExtensionAvailable()) { + playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> { + setPanelContent(null, lyricsList); + }); + } else { + playerBottomSheetViewModel.getLiveLyrics().observe(getViewLifecycleOwner(), lyrics -> { + setPanelContent(lyrics, null); + }); + } + } + + private void setPanelContent(String lyrics, LyricsList lyricsList) { + playerBottomSheetViewModel.getLiveDescription().observe(getViewLifecycleOwner(), description -> { + if (bind != null) { + bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, 0); + + if (lyrics != null && !lyrics.trim().equals("")) { + bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(lyrics)); + bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE); + bind.emptyDescriptionImageView.setVisibility(View.GONE); + bind.titleEmptyDescriptionLabel.setVisibility(View.GONE); + bind.syncLyricsTapButton.setVisibility(View.GONE); + } else if (lyricsList != null && lyricsList.getStructuredLyrics() != null) { + setSyncLirics(lyricsList); + bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE); + bind.emptyDescriptionImageView.setVisibility(View.GONE); + bind.titleEmptyDescriptionLabel.setVisibility(View.GONE); + bind.syncLyricsTapButton.setVisibility(View.VISIBLE); + } else if (description != null && !description.trim().equals("")) { + bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(description)); + bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE); + bind.emptyDescriptionImageView.setVisibility(View.GONE); + bind.titleEmptyDescriptionLabel.setVisibility(View.GONE); + bind.syncLyricsTapButton.setVisibility(View.GONE); + } else { + bind.nowPlayingSongLyricsTextView.setVisibility(View.GONE); + bind.emptyDescriptionImageView.setVisibility(View.VISIBLE); + bind.titleEmptyDescriptionLabel.setVisibility(View.VISIBLE); + bind.syncLyricsTapButton.setVisibility(View.GONE); + } + } + }); + } + + @SuppressLint("DefaultLocale") + private void setSyncLirics(LyricsList lyricsList) { + if (lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) { + StringBuilder lyricsBuilder = new StringBuilder(); + List lines = lyricsList.getStructuredLyrics().get(0).getLine(); + + if (lines != null) { + for (Line line : lines) { + lyricsBuilder.append(line.getValue().trim()).append("\n"); + } + } + + bind.nowPlayingSongLyricsTextView.setText(lyricsBuilder.toString()); + } + } + + private void defineProgressHandler() { + playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> { + if (lyricsList != null) { + + if (lyricsList.getStructuredLyrics() != null && lyricsList.getStructuredLyrics().get(0) != null && !lyricsList.getStructuredLyrics().get(0).getSynced()) { + releaseHandler(); + return; + } + + syncLyricsHandler = new Handler(); + syncLyricsRunnable = () -> { + if (syncLyricsHandler != null) { + if (bind != null) { + displaySyncedLyrics(); + } + + syncLyricsHandler.postDelayed(syncLyricsRunnable, 250); + } + }; + + syncLyricsHandler.postDelayed(syncLyricsRunnable, 250); + } else { + releaseHandler(); + } + }); + } + + private void displaySyncedLyrics() { + LyricsList lyricsList = playerBottomSheetViewModel.getLiveLyricsList().getValue(); + int timestamp = (int) (mediaBrowser.getCurrentPosition()); + + if (lyricsList != null && lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) { + StringBuilder lyricsBuilder = new StringBuilder(); + List lines = lyricsList.getStructuredLyrics().get(0).getLine(); + + if (lines == null || lines.isEmpty()) return; + + for (Line line : lines) { + lyricsBuilder.append(line.getValue().trim()).append("\n"); + } + + Line toHighlight = lines.stream().filter(line -> line != null && line.getStart() != null && line.getStart() < timestamp).reduce((first, second) -> second).orElse(null); + + if (toHighlight != null) { + String lyrics = lyricsBuilder.toString(); + Spannable spannableString = new SpannableString(lyrics); + + int startingPosition = getStartPosition(lines, toHighlight); + int endingPosition = startingPosition + toHighlight.getValue().length(); + + spannableString.setSpan(new ForegroundColorSpan(requireContext().getResources().getColor(R.color.shadowsLyricsTextColor, null)), 0, lyrics.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + spannableString.setSpan(new ForegroundColorSpan(requireContext().getResources().getColor(R.color.lyricsTextColor, null)), startingPosition, endingPosition, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + + bind.nowPlayingSongLyricsTextView.setText(spannableString); + + if (playerBottomSheetViewModel.getSyncLyricsState()) { + bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, getScroll(lines, toHighlight)); + } + } + } + } + + private int getStartPosition(List lines, Line toHighlight) { + int start = 0; + + for (Line line : lines) { + if (line != toHighlight) { + start = start + line.getValue().length() + 1; + } else { + break; + } + } + + return start; + } + + private int getLineCount(List lines, Line toHighlight) { + int start = 0; + + for (Line line : lines) { + if (line != toHighlight) { + bind.tempLyricsLineTextView.setText(line.getValue()); + start = start + bind.tempLyricsLineTextView.getLineCount(); + } else { + break; + } + } + + return start; + } + + private int getScroll(List lines, Line toHighlight) { + int lineHeight = bind.nowPlayingSongLyricsTextView.getLineHeight(); + int lineCount = getLineCount(lines, toHighlight); + int scrollViewHeight = bind.nowPlayingSongLyricsSrollView.getHeight(); + + return lineHeight * lineCount < scrollViewHeight / 2 ? 0 : lineHeight * lineCount - scrollViewHeight / 2 + lineHeight; + } } \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlaylistPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlaylistPageFragment.java index 44b1c59c..1ef5c931 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlaylistPageFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlaylistPageFragment.java @@ -167,7 +167,7 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback { // Pic top-left CustomGlideRequest.Builder - .from(requireContext(), songs.size() > 0 ? songs.get(0).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song) + .from(requireContext(), !songs.isEmpty() ? songs.get(0).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song) .build() .transform(new GranularRoundedCorners(CustomGlideRequest.CORNER_RADIUS, 0, 0, 0)) .into(bind.playlistCoverImageViewTopLeft); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java index c28335cd..99abcbdc 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java @@ -6,6 +6,7 @@ import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.WindowManager; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; @@ -91,6 +92,7 @@ public class SettingsFragment extends PreferenceFragmentCompat { actionSyncStarredTracks(); actionChangeDownloadStorage(); actionDeleteDownloadStorage(); + actionKeepScreenOn(); } @Override @@ -248,4 +250,17 @@ public class SettingsFragment extends PreferenceFragmentCompat { } }); } + + private void actionKeepScreenOn() { + findPreference("always_on_display").setOnPreferenceChangeListener((preference, newValue) -> { + if (newValue instanceof Boolean) { + if ((Boolean) newValue) { + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } + return true; + }); + } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java index a8059622..5ce0b87f 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java @@ -117,7 +117,7 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements public void onLoadMedia(List media) { MusicUtil.ratingFilter((ArrayList) media); - if (media.size() > 0) { + if (!media.isEmpty()) { MediaManager.startQueue(mediaBrowserListenableFuture, (ArrayList) media, 0); ((MainActivity) requireActivity()).setBottomSheetInPeek(true); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java index fde89f3b..b55d8494 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java @@ -91,7 +91,7 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement artistRepository.getInstantMix(artist, 20).observe(getViewLifecycleOwner(), songs -> { MusicUtil.ratingFilter(songs); - if (songs.size() > 0) { + if (!songs.isEmpty()) { MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); ((MainActivity) requireActivity()).setBottomSheetInPeek(true); } @@ -106,7 +106,7 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement artistRepository.getRandomSong(artist, 50).observe(getViewLifecycleOwner(), songs -> { MusicUtil.ratingFilter(songs); - if (songs.size() > 0) { + if (!songs.isEmpty()) { MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); ((MainActivity) requireActivity()).setBottomSheetInPeek(true); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java index d7d444dc..970c4e9c 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java @@ -121,7 +121,7 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements return; } - if (songs.size() > 0) { + if (!songs.isEmpty()) { MediaManager.enqueue(mediaBrowserListenableFuture, songs, true); dismissBottomSheet(); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt b/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt index d4818482..a97ff060 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt @@ -30,6 +30,7 @@ object Constants { const val ALBUM_ORDER_BY_ARTIST = "ALBUM_ORDER_BY_ARTIST" const val ALBUM_ORDER_BY_YEAR = "ALBUM_ORDER_BY_YEAR" const val ALBUM_ORDER_BY_RANDOM = "ALBUM_ORDER_BY_RANDOM" + const val ALBUM_ORDER_BY_RECENTLY_ADDED = "ALBUM_ORDER_BY_RECENTLY_ADDED" const val ARTIST_DOWNLOADED = "ARTIST_DOWNLOADED" const val ARTIST_STARRED = "ARTIST_STARRED" @@ -90,4 +91,19 @@ object Constants { const val PLAYABLE_MEDIA_LIMIT = 100 const val PRE_PLAYABLE_MEDIA = 15 + + const val HOME_SECTOR_DISCOVERY = "HOME_SECTOR_DISCOVERY" + const val HOME_SECTOR_MADE_FOR_YOU = "HOME_SECTOR_MADE_FOR_YOU" + const val HOME_SECTOR_BEST_OF = "HOME_SECTOR_BEST_OF" + const val HOME_SECTOR_RADIO_STATION = "HOME_SECTOR_RADIO_STATION" + const val HOME_SECTOR_TOP_SONGS = "HOME_SECTOR_TOP_SONGS" + const val HOME_SECTOR_STARRED_TRACKS = "HOME_SECTOR_STARRED_TRACKS" + const val HOME_SECTOR_STARRED_ALBUMS = "HOME_SECTOR_STARRED_ALBUMS" + const val HOME_SECTOR_STARRED_ARTISTS = "HOME_SECTOR_STARRED_ARTISTS" + const val HOME_SECTOR_NEW_RELEASES = "HOME_SECTOR_NEW_RELEASES" + const val HOME_SECTOR_FLASHBACK = "HOME_SECTOR_FLASHBACK" + const val HOME_SECTOR_MOST_PLAYED = "HOME_SECTOR_MOST_PLAYED" + const val HOME_SECTOR_LAST_PLAYED = "HOME_SECTOR_LAST_PLAYED" + const val HOME_SECTOR_RECENTLY_ADDED = "HOME_SECTOR_RECENTLY_ADDED" + const val HOME_SECTOR_SHARED = "HOME_SECTOR_SHARED" } \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java index f94b84a9..3b43d30d 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java +++ b/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java @@ -82,6 +82,8 @@ public class MappingUtil { .setArtist(MusicUtil.getReadableString(media.getArtist())) .setArtworkUri(artworkUri) .setExtras(bundle) + .setIsBrowsable(false) + .setIsPlayable(true) .build() ) .setRequestMetadata( @@ -116,6 +118,8 @@ public class MappingUtil { .setReleaseYear(media.getYear() != null ? media.getYear() : 0) .setAlbumTitle(MusicUtil.getReadableString(media.getAlbum())) .setArtist(MusicUtil.getReadableString(media.getArtist())) + .setIsBrowsable(false) + .setIsPlayable(true) .build() ) .setRequestMetadata( @@ -145,6 +149,8 @@ public class MappingUtil { .setTitle(internetRadioStation.getName()) .setArtist(internetRadioStation.getStreamUrl()) .setExtras(bundle) + .setIsBrowsable(false) + .setIsPlayable(true) .build() ) .setRequestMetadata( @@ -193,6 +199,8 @@ public class MappingUtil { .setArtist(MusicUtil.getReadableString(podcastEpisode.getArtist())) .setArtworkUri(artworkUri) .setExtras(bundle) + .setIsBrowsable(false) + .setIsPlayable(true) .build() ) .setRequestMetadata( @@ -201,12 +209,6 @@ public class MappingUtil { .setExtras(bundle) .build() ) - /* .setClippingConfiguration( - new MediaItem.ClippingConfiguration.Builder() - .setStartPositionMs(0) - .setEndPositionMs(podcastEpisode.getDuration() * 1000) - .build() - ) */ .setMimeType(MimeTypes.BASE_TYPE_AUDIO) .setUri(uri) .build(); diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/MusicUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/MusicUtil.java index b0a7931c..6b0b23fe 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/MusicUtil.java +++ b/app/src/main/java/com/cappielloantonio/tempo/util/MusicUtil.java @@ -128,16 +128,18 @@ public class MusicUtil { } - public static String getReadableDurationString(long duration, boolean millis) { + public static String getReadableDurationString(Long duration, boolean millis) { + long lenght = duration != null ? duration : 0; + long minutes; long seconds; if (millis) { - minutes = (duration / 1000) / 60; - seconds = (duration / 1000) % 60; + minutes = (lenght / 1000) / 60; + seconds = (lenght / 1000) % 60; } else { - minutes = duration / 60; - seconds = duration % 60; + minutes = lenght / 60; + seconds = lenght % 60; } if (minutes < 60) { @@ -149,6 +151,22 @@ public class MusicUtil { } } + public static String getReadableDurationString(Integer duration, boolean millis) { + long lenght = duration != null ? duration : 0; + return getReadableDurationString(lenght, millis); + } + + public static String getReadableAudioQualityString(Child child) { + if (!Preferences.showAudioQuality()) return ""; + + return "•" + + " " + + child.getBitrate() + + "kbps" + + " " + + child.getSuffix(); + } + public static String getReadablePodcastDurationString(long duration) { long minutes = duration / 60; @@ -204,7 +222,7 @@ public class MusicUtil { public static List getReadableStrings(List strings) { List readableStrings = new ArrayList<>(); - if (strings.size() > 0) { + if (!strings.isEmpty()) { for (String string : strings) { if (string != null) { readableStrings.add(Html.fromHtml(string, Html.FROM_HTML_MODE_COMPACT).toString()); @@ -320,4 +338,4 @@ public class MusicUtil { toFilter.addAll(filtered); } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/OpenSubsonicExtensionsUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/OpenSubsonicExtensionsUtil.java new file mode 100644 index 00000000..78c59fb9 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/util/OpenSubsonicExtensionsUtil.java @@ -0,0 +1,41 @@ +package com.cappielloantonio.tempo.util; + +import com.cappielloantonio.tempo.subsonic.models.OpenSubsonicExtension; +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; + +import java.util.List; + +public class OpenSubsonicExtensionsUtil { + private static List getOpenSubsonicExtensions() { + List extensions = null; + + if (Preferences.isOpenSubsonic() && Preferences.getOpenSubsonicExtensions() != null) { + extensions = new Gson().fromJson( + Preferences.getOpenSubsonicExtensions(), + new TypeToken>() { + }.getType() + ); + } + + return extensions; + } + + private static OpenSubsonicExtension getOpenSubsonicExtension(String extensionName) { + if (getOpenSubsonicExtensions() == null) return null; + + return getOpenSubsonicExtensions().stream().filter(openSubsonicExtension -> openSubsonicExtension.getName().equals(extensionName)).findAny().orElse(null); + } + + public static boolean isTranscodeOffsetExtensionAvailable() { + return getOpenSubsonicExtension("transcodeOffset") != null; + } + + public static boolean isFormPostExtensionAvailable() { + return getOpenSubsonicExtension("formPost") != null; + } + + public static boolean isSongLyricsExtensionAvailable() { + return getOpenSubsonicExtension("songLyrics") != null; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt index 701eda68..7ac0e80f 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt @@ -1,6 +1,12 @@ package com.cappielloantonio.tempo.util import com.cappielloantonio.tempo.App +import com.cappielloantonio.tempo.model.HomeSector +import com.cappielloantonio.tempo.subsonic.models.OpenSubsonicExtension +import com.google.gson.Gson + + + object Preferences { const val THEME = "theme" @@ -12,6 +18,8 @@ object Preferences { private const val LOW_SECURITY = "low_security" private const val BATTERY_OPTIMIZATION = "battery_optimization" private const val SERVER_ID = "server_id" + private const val OPEN_SUBSONIC = "open_subsonic" + private const val OPEN_SUBSONIC_EXTENSIONS = "open_subsonic_extensions" private const val PLAYBACK_SPEED = "playback_speed" private const val SKIP_SILENCE = "skip_silence" private const val IMAGE_CACHE_SIZE = "image_cache_size" @@ -45,6 +53,10 @@ object Preferences { private const val BUFFERING_STRATEGY = "buffering_strategy" private const val SKIP_MIN_STAR_RATING = "skip_min_star_rating" private const val MIN_STAR_RATING = "min_star_rating" + private const val ALWAYS_ON_DISPLAY = "always_on_display" + private const val AUDIO_QUALITY_PER_ITEM = "audio_quality_per_item" + private const val HOME_SECTOR_LIST = "home_sector_list" + private const val RATING_PER_ITEM = "rating_per_item" @JvmStatic @@ -117,6 +129,26 @@ object Preferences { App.getInstance().preferences.edit().putString(SERVER_ID, serverId).apply() } + @JvmStatic + fun isOpenSubsonic(): Boolean { + return App.getInstance().preferences.getBoolean(OPEN_SUBSONIC, false) + } + + @JvmStatic + fun setOpenSubsonic(isOpenSubsonic: Boolean) { + App.getInstance().preferences.edit().putBoolean(OPEN_SUBSONIC, isOpenSubsonic).apply() + } + + @JvmStatic + fun getOpenSubsonicExtensions(): String? { + return App.getInstance().preferences.getString(OPEN_SUBSONIC_EXTENSIONS, null) + } + + @JvmStatic + fun setOpenSubsonicExtensions(extension: List) { + App.getInstance().preferences.edit().putString(OPEN_SUBSONIC_EXTENSIONS, Gson().toJson(extension)).apply() + } + @JvmStatic fun askForOptimization(): Boolean { return App.getInstance().preferences.getBoolean(BATTERY_OPTIMIZATION, true) @@ -345,4 +377,29 @@ object Preferences { fun getMinStarRatingAccepted(): Int { return App.getInstance().preferences.getInt(MIN_STAR_RATING, 0) } + + @JvmStatic + fun isDisplayAlwaysOn(): Boolean { + return App.getInstance().preferences.getBoolean(ALWAYS_ON_DISPLAY, false) + } + + @JvmStatic + fun showAudioQuality(): Boolean { + return App.getInstance().preferences.getBoolean(AUDIO_QUALITY_PER_ITEM, false) + } + + @JvmStatic + fun getHomeSectorList(): String? { + return App.getInstance().preferences.getString(HOME_SECTOR_LIST, null) + } + + @JvmStatic + fun setHomeSectorList(extension: List?) { + App.getInstance().preferences.edit().putString(HOME_SECTOR_LIST, Gson().toJson(extension)).apply() + } + + @JvmStatic + fun showItemRating(): Boolean { + return App.getInstance().preferences.getBoolean(RATING_PER_ITEM, false) + } } \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/ReplayGainUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/ReplayGainUtil.java index 053cdd34..510673cb 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/ReplayGainUtil.java +++ b/app/src/main/java/com/cappielloantonio/tempo/util/ReplayGainUtil.java @@ -102,7 +102,7 @@ public class ReplayGainUtil { private static Float parseReplayGainTag(Metadata.Entry entry) { try { - return Float.parseFloat(entry.toString().replaceAll("[^\\d.]", "")); + return Float.parseFloat(entry.toString().replaceAll("[^\\d.-]", "")); } catch (NumberFormatException exception) { return 0f; } diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumCatalogueViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumCatalogueViewModel.java index 7182e5d4..a18e645c 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumCatalogueViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumCatalogueViewModel.java @@ -20,8 +20,10 @@ import retrofit2.Callback; public class AlbumCatalogueViewModel extends AndroidViewModel { private final MutableLiveData> albumList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData loading = new MutableLiveData<>(true); private int page = 0; + private Status status = Status.STOPPED; public AlbumCatalogueViewModel(@NonNull Application application) { super(application); @@ -31,7 +33,22 @@ public class AlbumCatalogueViewModel extends AndroidViewModel { return albumList; } - public void loadAlbums(int size) { + public LiveData getLoadingStatus() { + return loading; + } + + public void loadAlbums() { + page = 0; + status = Status.RUNNING; + albumList.setValue(new ArrayList<>()); + loadAlbums(500); + } + + public void stopLoading() { + status = Status.STOPPED; + } + + private void loadAlbums(int size) { retrieveAlbums(new MediaCallback() { @Override public void onError(Exception exception) { @@ -39,15 +56,22 @@ public class AlbumCatalogueViewModel extends AndroidViewModel { @Override public void onLoadMedia(List media) { - List liveAlbum = albumList.getValue(); + if (status == Status.STOPPED) { + loading.setValue(false); + return; + } - if (liveAlbum == null) liveAlbum = new ArrayList<>(); + List liveAlbum = albumList.getValue(); liveAlbum.addAll((List) media); albumList.setValue(liveAlbum); if (media.size() == size) { loadAlbums(size); + loading.setValue(true); + } else { + status = Status.STOPPED; + loading.setValue(false); } } }, size, size * page++); @@ -73,4 +97,9 @@ public class AlbumCatalogueViewModel extends AndroidViewModel { } }); } -} + + private enum Status { + RUNNING, + STOPPED + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumPageViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumPageViewModel.java index 5aa40e51..e5c30894 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumPageViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumPageViewModel.java @@ -9,6 +9,7 @@ import androidx.lifecycle.LiveData; import com.cappielloantonio.tempo.repository.AlbumRepository; import com.cappielloantonio.tempo.repository.ArtistRepository; import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.subsonic.models.AlbumInfo; import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.subsonic.models.Child; @@ -42,4 +43,8 @@ public class AlbumPageViewModel extends AndroidViewModel { public LiveData getArtist() { return artistRepository.getArtistInfo(album.getArtistId()); } + + public LiveData getAlbumInfo() { + return albumRepository.getAlbumInfo(album.getId()); + } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeRearrangementViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeRearrangementViewModel.java new file mode 100644 index 00000000..fd7f193c --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeRearrangementViewModel.java @@ -0,0 +1,77 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.model.HomeSector; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.Preferences; +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; + +import java.util.ArrayList; +import java.util.List; + +public class HomeRearrangementViewModel extends AndroidViewModel { + private List sectors = new ArrayList<>(); + + public HomeRearrangementViewModel(@NonNull Application application) { + super(application); + } + + public List getHomeSectorList() { + if (sectors != null && !sectors.isEmpty()) return sectors; + + if (Preferences.getHomeSectorList() != null && !Preferences.getHomeSectorList().equals("null")) { + sectors = new Gson().fromJson( + Preferences.getHomeSectorList(), + new TypeToken>() { + }.getType() + ); + } else { + sectors = fillStandardHomeSectorList(); + } + + return sectors; + } + + public void orderSectorLiveListAfterSwap(List sectors) { + this.sectors = sectors; + } + + public void saveHomeSectorList(List sectors) { + Preferences.setHomeSectorList(sectors); + } + + public void resetHomeSectorList() { + Preferences.setHomeSectorList(null); + } + + public void closeDialog() { + sectors = null; + } + + private List fillStandardHomeSectorList() { + List sectors = new ArrayList<>(); + + sectors.add(new HomeSector(Constants.HOME_SECTOR_DISCOVERY, getApplication().getString(R.string.home_title_discovery), true, 1)); + sectors.add(new HomeSector(Constants.HOME_SECTOR_MADE_FOR_YOU, getApplication().getString(R.string.home_title_made_for_you), true, 2)); + sectors.add(new HomeSector(Constants.HOME_SECTOR_BEST_OF, getApplication().getString(R.string.home_title_best_of), true, 3)); + sectors.add(new HomeSector(Constants.HOME_SECTOR_RADIO_STATION, getApplication().getString(R.string.home_title_radio_station), true, 4)); + sectors.add(new HomeSector(Constants.HOME_SECTOR_TOP_SONGS, getApplication().getString(R.string.home_title_top_songs), true, 5)); + sectors.add(new HomeSector(Constants.HOME_SECTOR_STARRED_TRACKS, getApplication().getString(R.string.home_title_starred_tracks), true, 6)); + sectors.add(new HomeSector(Constants.HOME_SECTOR_STARRED_ALBUMS, getApplication().getString(R.string.home_title_starred_albums), true, 7)); + sectors.add(new HomeSector(Constants.HOME_SECTOR_STARRED_ARTISTS, getApplication().getString(R.string.home_title_starred_artists), true, 8)); + sectors.add(new HomeSector(Constants.HOME_SECTOR_NEW_RELEASES, getApplication().getString(R.string.home_title_new_releases), true, 9)); + sectors.add(new HomeSector(Constants.HOME_SECTOR_FLASHBACK, getApplication().getString(R.string.home_title_flashback), true, 10)); + sectors.add(new HomeSector(Constants.HOME_SECTOR_MOST_PLAYED, getApplication().getString(R.string.home_title_most_played), true, 11)); + sectors.add(new HomeSector(Constants.HOME_SECTOR_LAST_PLAYED, getApplication().getString(R.string.home_title_last_played), true, 12)); + sectors.add(new HomeSector(Constants.HOME_SECTOR_RECENTLY_ADDED, getApplication().getString(R.string.home_title_recently_added), true, 13)); + sectors.add(new HomeSector(Constants.HOME_SECTOR_SHARED, getApplication().getString(R.string.home_title_shares), true, 14)); + + return sectors; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java index 25c48055..a9408df8 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java @@ -11,6 +11,7 @@ import androidx.lifecycle.MutableLiveData; import com.cappielloantonio.tempo.interfaces.StarCallback; import com.cappielloantonio.tempo.model.Chronology; import com.cappielloantonio.tempo.model.Favorite; +import com.cappielloantonio.tempo.model.HomeSector; import com.cappielloantonio.tempo.repository.AlbumRepository; import com.cappielloantonio.tempo.repository.ArtistRepository; import com.cappielloantonio.tempo.repository.ChronologyRepository; @@ -22,6 +23,8 @@ import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.Share; import com.cappielloantonio.tempo.util.Preferences; +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; import java.util.ArrayList; import java.util.Calendar; @@ -59,10 +62,13 @@ public class HomeViewModel extends AndroidViewModel { private final MutableLiveData> artistBestOf = new MutableLiveData<>(null); private final MutableLiveData> shares = new MutableLiveData<>(null); + private List sectors; public HomeViewModel(@NonNull Application application) { super(application); + setHomeSectorList(); + songRepository = new SongRepository(); albumRepository = new AlbumRepository(); artistRepository = new ArtistRepository(); @@ -266,6 +272,26 @@ public class HomeViewModel extends AndroidViewModel { sharingRepository.getShares().observe(owner, this.shares::postValue); } + private void setHomeSectorList() { + if (Preferences.getHomeSectorList() != null && !Preferences.getHomeSectorList().equals("null")) { + sectors = new Gson().fromJson( + Preferences.getHomeSectorList(), + new TypeToken>() { + }.getType() + ); + } + } + + public List getHomeSectorList() { + return sectors; + } + + public boolean checkHomeSectorVisibility(String sectorId) { + return sectors != null && sectors.stream().filter(sector -> sector.getId().equals(sectorId)) + .findAny() + .orElse(null) == null; + } + public void setOfflineFavorite() { ArrayList favorites = getFavorites(); ArrayList favoritesToSave = getFavoritesToSave(favorites); diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/IndexViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/IndexViewModel.java index 9cfa67f5..8801519b 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/IndexViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/IndexViewModel.java @@ -15,8 +15,6 @@ public class IndexViewModel extends AndroidViewModel { private MusicFolder musicFolder; - private MutableLiveData indexes = new MutableLiveData<>(null); - public IndexViewModel(@NonNull Application application) { super(application); diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/MainViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/MainViewModel.java index 97528ee9..70a62b2e 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/MainViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/MainViewModel.java @@ -8,6 +8,10 @@ import androidx.lifecycle.LiveData; import com.cappielloantonio.tempo.repository.QueueRepository; import com.cappielloantonio.tempo.repository.SystemRepository; +import com.cappielloantonio.tempo.subsonic.models.OpenSubsonicExtension; +import com.cappielloantonio.tempo.subsonic.models.SubsonicResponse; + +import java.util.List; public class MainViewModel extends AndroidViewModel { private static final String TAG = "SearchViewModel"; @@ -25,7 +29,11 @@ public class MainViewModel extends AndroidViewModel { return queueRepository.count() != 0; } - public LiveData ping() { + public LiveData ping() { return systemRepository.ping(); } + + public LiveData> getOpenSubsonicExtensions() { + return systemRepository.getOpenSubsonicExtensions(); + } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java index f91c4995..eb7c7f8f 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java @@ -16,15 +16,18 @@ import com.cappielloantonio.tempo.model.Download; import com.cappielloantonio.tempo.model.Queue; import com.cappielloantonio.tempo.repository.ArtistRepository; import com.cappielloantonio.tempo.repository.FavoriteRepository; +import com.cappielloantonio.tempo.repository.OpenRepository; import com.cappielloantonio.tempo.repository.QueueRepository; import com.cappielloantonio.tempo.repository.SongRepository; import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.subsonic.models.LyricsList; import com.cappielloantonio.tempo.subsonic.models.PlayQueue; import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.DownloadUtil; import com.cappielloantonio.tempo.util.MappingUtil; import com.cappielloantonio.tempo.util.NetworkUtil; +import com.cappielloantonio.tempo.util.OpenSubsonicExtensionsUtil; import com.cappielloantonio.tempo.util.Preferences; import java.util.Collections; @@ -40,13 +43,14 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel { private final ArtistRepository artistRepository; private final QueueRepository queueRepository; private final FavoriteRepository favoriteRepository; - + private final OpenRepository openRepository; private final MutableLiveData lyricsLiveData = new MutableLiveData<>(null); + private final MutableLiveData lyricsListLiveData = new MutableLiveData<>(null); private final MutableLiveData descriptionLiveData = new MutableLiveData<>(null); - private final MutableLiveData liveMedia = new MutableLiveData<>(null); private final MutableLiveData liveArtist = new MutableLiveData<>(null); private final MutableLiveData> instantMix = new MutableLiveData<>(null); + private boolean lyricsSyncState = true; public PlayerBottomSheetViewModel(@NonNull Application application) { @@ -56,6 +60,7 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel { artistRepository = new ArtistRepository(); queueRepository = new QueueRepository(); favoriteRepository = new FavoriteRepository(); + openRepository = new OpenRepository(); } public LiveData> getQueueSong() { @@ -125,8 +130,18 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel { return lyricsLiveData; } + public LiveData getLiveLyricsList() { + return lyricsListLiveData; + } + public void refreshMediaInfo(LifecycleOwner owner, Child media) { - songRepository.getSongLyrics(media).observe(owner, lyricsLiveData::postValue); + if (OpenSubsonicExtensionsUtil.isSongLyricsExtensionAvailable()) { + openRepository.getLyricsBySongId(media.getId()).observe(owner, lyricsListLiveData::postValue); + lyricsLiveData.postValue(null); + } else { + songRepository.getSongLyrics(media).observe(owner, lyricsLiveData::postValue); + lyricsListLiveData.postValue(null); + } } public LiveData getLiveMedia() { @@ -196,4 +211,12 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel { return false; } + + public void changeSyncLyricsState() { + lyricsSyncState = !lyricsSyncState; + } + + public boolean getSyncLyricsState() { + return lyricsSyncState; + } } diff --git a/app/src/main/res/drawable/ic_arrow_down.xml b/app/src/main/res/drawable/ic_arrow_down.xml new file mode 100644 index 00000000..59ec8149 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_down.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_drag_handle.xml b/app/src/main/res/drawable/ic_drag_handle.xml index 3c6cdf66..d283f15d 100644 --- a/app/src/main/res/drawable/ic_drag_handle.xml +++ b/app/src/main/res/drawable/ic_drag_handle.xml @@ -1,9 +1,9 @@ + android:width="20dp" + android:height="20dp" + android:viewportWidth="960" + android:viewportHeight="960"> - + android:pathData="M228.29,600Q213,600 202.5,589.71Q192,579.42 192,564.21Q192,549 202.34,538.5Q212.69,528 227.98,528L731.71,528Q747,528 757.5,538.29Q768,548.58 768,563.79Q768,579 757.66,589.5Q747.31,600 732.02,600L228.29,600ZM228.29,432Q213,432 202.5,421.71Q192,411.42 192,396.21Q192,381 202.34,370.5Q212.69,360 227.98,360L731.71,360Q747,360 757.5,370.29Q768,380.58 768,395.79Q768,411 757.66,421.5Q747.31,432 732.02,432L228.29,432Z" /> + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_favorite.xml b/app/src/main/res/drawable/ic_favorite.xml index 7d395976..753a4220 100644 --- a/app/src/main/res/drawable/ic_favorite.xml +++ b/app/src/main/res/drawable/ic_favorite.xml @@ -4,6 +4,6 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_favorites_outlined.xml b/app/src/main/res/drawable/ic_favorites_outlined.xml index 76525d3e..03706ffe 100644 --- a/app/src/main/res/drawable/ic_favorites_outlined.xml +++ b/app/src/main/res/drawable/ic_favorites_outlined.xml @@ -4,6 +4,6 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_lyrics_sync_lock.xml b/app/src/main/res/drawable/ic_lyrics_sync_lock.xml new file mode 100644 index 00000000..5576242d --- /dev/null +++ b/app/src/main/res/drawable/ic_lyrics_sync_lock.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_star.xml b/app/src/main/res/drawable/ic_star.xml new file mode 100644 index 00000000..2d74f8a6 --- /dev/null +++ b/app/src/main/res/drawable/ic_star.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_star_outlined.xml b/app/src/main/res/drawable/ic_star_outlined.xml new file mode 100644 index 00000000..7637008b --- /dev/null +++ b/app/src/main/res/drawable/ic_star_outlined.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/inner_fragment_player_controller_layout.xml b/app/src/main/res/layout-land/inner_fragment_player_controller_layout.xml new file mode 100644 index 00000000..61112689 --- /dev/null +++ b/app/src/main/res/layout-land/inner_fragment_player_controller_layout.xml @@ -0,0 +1,359 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +