diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 00000000..e00a326d
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,49 @@
+name: "CodeQL"
+
+on:
+ push:
+ branches:
+ - main
+ - ng-changes
+ - v0.2.0
+ pull_request:
+ branches:
+ - main
+ schedule:
+ - cron: "22 4 * * 1"
+
+jobs:
+ analyze:
+ name: Analyze (${{ matrix.language }})
+ runs-on: ubuntu-latest
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+ strategy:
+ fail-fast: false
+ matrix:
+ language:
+ - java-kotlin
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: "17"
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v4
+ with:
+ languages: ${{ matrix.language }}
+ build-mode: manual
+
+ - name: Build app (degoogled debug)
+ run: ./gradlew :app:assembleDegoogledDebug --no-daemon
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v4
diff --git a/.github/workflows/github_release.yml b/.github/workflows/github_release.yml
deleted file mode 100644
index 0108abb7..00000000
--- a/.github/workflows/github_release.yml
+++ /dev/null
@@ -1,129 +0,0 @@
-name: Github Release Workflow
-
-on:
- push:
- tags:
- - '[0-9]+.[0-9]+.[0-9]+'
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - name: Setup JDK 17
- uses: actions/setup-java@v4
- with:
- distribution: 'zulu'
- java-version: '17'
-
- - uses: actions/checkout@v3
-
- - name: Cache Gradle and wrapper
- uses: actions/cache@v3
- with:
- path: |
- ~/.gradle/caches
- ~/.gradle/wrapper
- key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
-
- - 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 All Release APKs
- id: build
- run: |
- # Only build release variants (removed debug builds)
- bash ./gradlew assembleTempusRelease
- bash ./gradlew assembleDegoogledRelease
-
- - name: Create Artifact Staging Directory
- run: mkdir -p release-artifacts
-
- - name: Sign Tempus Release APKs
- id: sign_tempus_release
- uses: r0adkll/sign-android-release@v1
- with:
- releaseDirectory: app/build/outputs/apk/tempus/release
- signingKeyBase64: ${{ secrets.KEYSTORE_BASE64 }}
- alias: ${{ secrets.KEY_ALIAS_GITHUB }}
- keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }}
- keyPassword: ${{ secrets.KEY_PASSWORD_GITHUB }}
- env:
- BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
-
- - name: Prepare Signed Tempus APKs for Release
- run: |
- TEMPUS_PATH=app/build/outputs/apk/tempus/release
-
- echo "--- Tempus Files BEFORE Move ---"
- ls -la $TEMPUS_PATH
- echo "--------------------------------"
-
- # FIX: Use find/xargs for robust file matching and moving.
-
- # Renaming 64-bit APK and moving to safe staging directory
- find $TEMPUS_PATH -name '*arm64-v8a*signed.apk' -print0 | xargs -0 mv -t ./release-artifacts/
- mv ./release-artifacts/*arm64-v8a*signed.apk ./release-artifacts/app-tempus-arm64-v8a-release.apk
-
- # Renaming 32-bit APK and moving to safe staging directory
- find $TEMPUS_PATH -name '*armeabi-v7a*signed.apk' -print0 | xargs -0 mv -t ./release-artifacts/
- mv ./release-artifacts/*armeabi-v7a*signed.apk ./release-artifacts/app-tempus-armeabi-v7a-release.apk
-
- echo "Prepared Tempus APKs."
-
- - name: Sign Degoogled Release APKs
- id: sign_degoogled_release
- uses: r0adkll/sign-android-release@v1
- with:
- releaseDirectory: app/build/outputs/apk/degoogled/release
- signingKeyBase64: ${{ secrets.KEYSTORE_BASE64 }}
- alias: ${{ secrets.KEY_ALIAS_GITHUB }}
- keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }}
- keyPassword: ${{ secrets.KEY_PASSWORD_GITHUB }}
- env:
- BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
-
- - name: Prepare Signed Degoogled APKs for Release
- run: |
- DEGOOGLED_PATH=app/build/outputs/apk/degoogled/release
-
- echo "--- Degoogled Files BEFORE Move ---"
- ls -la $DEGOOGLED_PATH
- echo "--------------------------------"
-
- # FIX: Use find/xargs for robust file matching and moving.
-
- # Renaming 64-bit APK and moving to safe staging directory
- find $DEGOOGLED_PATH -name '*arm64-v8a*signed.apk' -print0 | xargs -0 mv -t ./release-artifacts/
- mv ./release-artifacts/*arm64-v8a*signed.apk ./release-artifacts/app-degoogled-arm64-v8a-release.apk
-
- # Renaming 32-bit APK and moving to safe staging directory
- find $DEGOOGLED_PATH -name '*armeabi-v7a*signed.apk' -print0 | xargs -0 mv -t ./release-artifacts/
- mv ./release-artifacts/*armeabi-v7a*signed.apk ./release-artifacts/app-degoogled-armeabi-v7a-release.apk
-
- echo "Prepared Degoogled APKs."
- ls -la ./release-artifacts/
-
- - name: Create Release
- id: create_release
- uses: softprops/action-gh-release@v1
- with:
- tag_name: ${{ github.ref_name }}
- name: ${{ github.ref_name }}
- body: '> Changelog coming soon'
- draft: false
- prerelease: false
- files: ./release-artifacts/*.apk
-
- - name: Upload Release APKs as artifacts (For easy pipeline access)
- uses: actions/upload-artifact@v4
- with:
- name: release-apks
- path: ./release-artifacts/*.apk
- retention-days: 30
diff --git a/.gitignore b/.gitignore
index 99beffd1..5a2f93e9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,5 +17,12 @@
.vscode/settings.json
# release / debug files
tempus-release-key.jks
+*.keystore
+*.jks
app/tempus/
app/degoogled/
+/release-apks/
+*.apk
+*.aab
+
+/changes.md
diff --git a/app/build.gradle b/app/build.gradle
index 9c2baa3f..64ac53b8 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -10,8 +10,8 @@ android {
minSdkVersion 24
targetSdk 35
- versionCode 24
- versionName '4.13.0'
+ versionCode 25
+ versionName 'v0.2.1'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
javaCompileOptions {
@@ -68,12 +68,12 @@ android {
}
compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = "1.8"
+ jvmTarget = "17"
}
buildFeatures {
@@ -88,6 +88,7 @@ dependencies {
implementation files('../libs/lib-decoder-ffmpeg-release.aar')
// AndroidX
+ implementation 'androidx.security:security-crypto:1.1.0-alpha06'
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
implementation 'androidx.preference:preference-ktx:1.2.1'
@@ -97,7 +98,7 @@ dependencies {
implementation 'androidx.room:room-runtime:2.6.1'
implementation 'androidx.core:core-splashscreen:1.0.1'
implementation 'androidx.appcompat:appcompat:1.7.0'
- implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.2.0"
+ implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0'
// Android Material
implementation 'com.google.android.material:material:1.10.0'
@@ -120,7 +121,7 @@ dependencies {
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.11.0'
- implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.14'
+ implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
implementation 'com.squareup.retrofit2:converter-gson:2.11.0'
}
java {
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 48b1f068..1211baa3 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -18,11 +18,24 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
-#-renamesourcefileattribute SourceFile
+-renamesourcefileattribute SourceFile
-keepattributes SourceFile, LineNumberTable
-keep public class * extends java.lang.Exception
-keep class retrofit2.** { *; }
-keep class **.reflect.TypeToken { *; }
--keep class * extends **.reflect.TypeToken
\ No newline at end of file
+-keep class * extends **.reflect.TypeToken
+
+-keep class com.google.crypto.tink.** { *; }
+-keep class androidx.security.crypto.** { *; }
+
+-dontwarn com.google.api.client.http.GenericUrl
+-dontwarn com.google.api.client.http.HttpHeaders
+-dontwarn com.google.api.client.http.HttpRequest
+-dontwarn com.google.api.client.http.HttpRequestFactory
+-dontwarn com.google.api.client.http.HttpResponse
+-dontwarn com.google.api.client.http.HttpTransport
+-dontwarn com.google.api.client.http.javanet.NetHttpTransport$Builder
+-dontwarn com.google.api.client.http.javanet.NetHttpTransport
+-dontwarn org.joda.time.Instant
\ No newline at end of file
diff --git a/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/14.json b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/14.json
index 0e76b479..332782b5 100644
--- a/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/14.json
+++ b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/14.json
@@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 14,
- "identityHash": "42299b5bbc21c4c7c83eea7dc5ca5f66",
+ "identityHash": "5df6007016fbf8aac8b69173ee7f7c6e",
"entities": [
{
"tableName": "queue",
@@ -242,7 +242,7 @@
},
{
"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, `local_address` TEXT, `timestamp` INTEGER NOT NULL, `low_security` INTEGER NOT NULL DEFAULT false, `client_cert` TEXT, PRIMARY KEY(`id`))",
+ "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, `local_address` TEXT, `timestamp` INTEGER NOT NULL, `low_security` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "serverId",
@@ -292,12 +292,6 @@
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
- },
- {
- "fieldPath": "clientCert",
- "columnName": "client_cert",
- "affinity": "TEXT",
- "notNull": false
}
],
"primaryKey": {
@@ -1068,7 +1062,7 @@
},
{
"tableName": "playlist",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `duration` INTEGER NOT NULL, `coverArt` TEXT, PRIMARY KEY(`id`))",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `duration` INTEGER NOT NULL, `songCount` INTEGER NOT NULL, `coverArt` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
@@ -1088,6 +1082,12 @@
"affinity": "INTEGER",
"notNull": true
},
+ {
+ "fieldPath": "songCount",
+ "columnName": "songCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
{
"fieldPath": "coverArtId",
"columnName": "coverArt",
@@ -1158,7 +1158,7 @@
"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, '42299b5bbc21c4c7c83eea7dc5ca5f66')"
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5df6007016fbf8aac8b69173ee7f7c6e')"
]
}
}
\ No newline at end of file
diff --git a/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/15.json b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/15.json
new file mode 100644
index 00000000..0720c2aa
--- /dev/null
+++ b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/15.json
@@ -0,0 +1,1164 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 15,
+ "identityHash": "5df6007016fbf8aac8b69173ee7f7c6e",
+ "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, `sampling_rate` INTEGER, `bit_depth` 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": "samplingRate",
+ "columnName": "sampling_rate",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "bitDepth",
+ "columnName": "bit_depth",
+ "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, `local_address` TEXT, `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": "localAddress",
+ "columnName": "local_address",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "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, `timestamp` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`search`))",
+ "fields": [
+ {
+ "fieldPath": "search",
+ "columnName": "search",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "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, `sampling_rate` INTEGER, `bit_depth` 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": "samplingRate",
+ "columnName": "sampling_rate",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "bitDepth",
+ "columnName": "bit_depth",
+ "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, `sampling_rate` INTEGER, `bit_depth` 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": "samplingRate",
+ "columnName": "sampling_rate",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "bitDepth",
+ "columnName": "bit_depth",
+ "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": []
+ },
+ {
+ "tableName": "playlist",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `duration` INTEGER NOT NULL, `songCount` INTEGER NOT NULL, `coverArt` TEXT, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "duration",
+ "columnName": "duration",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "songCount",
+ "columnName": "songCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "coverArtId",
+ "columnName": "coverArt",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "lyrics_cache",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song_id` TEXT NOT NULL, `artist` TEXT, `title` TEXT, `lyrics` TEXT, `structured_lyrics` TEXT, `updated_at` INTEGER NOT NULL, PRIMARY KEY(`song_id`))",
+ "fields": [
+ {
+ "fieldPath": "songId",
+ "columnName": "song_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "artist",
+ "columnName": "artist",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "lyrics",
+ "columnName": "lyrics",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "structuredLyrics",
+ "columnName": "structured_lyrics",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "updatedAt",
+ "columnName": "updated_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "song_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, '5df6007016fbf8aac8b69173ee7f7c6e')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/16.json b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/16.json
new file mode 100644
index 00000000..3f9a1700
--- /dev/null
+++ b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/16.json
@@ -0,0 +1,1214 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 16,
+ "identityHash": "47867264ef33da252bb39a7625c48d2b",
+ "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, `sampling_rate` INTEGER, `bit_depth` 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": "samplingRate",
+ "columnName": "sampling_rate",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "bitDepth",
+ "columnName": "bit_depth",
+ "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, `local_address` TEXT, `timestamp` INTEGER NOT NULL, `low_security` INTEGER NOT NULL DEFAULT false, `client_cert` TEXT, 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": "localAddress",
+ "columnName": "local_address",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isLowSecurity",
+ "columnName": "low_security",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "clientCert",
+ "columnName": "client_cert",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "recent_search",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`search` TEXT NOT NULL, `timestamp` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`search`))",
+ "fields": [
+ {
+ "fieldPath": "search",
+ "columnName": "search",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "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, `sampling_rate` INTEGER, `bit_depth` 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": "samplingRate",
+ "columnName": "sampling_rate",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "bitDepth",
+ "columnName": "bit_depth",
+ "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, `sampling_rate` INTEGER, `bit_depth` 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": "samplingRate",
+ "columnName": "sampling_rate",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "bitDepth",
+ "columnName": "bit_depth",
+ "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": []
+ },
+ {
+ "tableName": "playlist",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `duration` INTEGER NOT NULL, `songCount` INTEGER NOT NULL, `coverArt` TEXT, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "duration",
+ "columnName": "duration",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "songCount",
+ "columnName": "songCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "coverArtId",
+ "columnName": "coverArt",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "lyrics_cache",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song_id` TEXT NOT NULL, `artist` TEXT, `title` TEXT, `lyrics` TEXT, `structured_lyrics` TEXT, `updated_at` INTEGER NOT NULL, PRIMARY KEY(`song_id`))",
+ "fields": [
+ {
+ "fieldPath": "songId",
+ "columnName": "song_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "artist",
+ "columnName": "artist",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "lyrics",
+ "columnName": "lyrics",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "structuredLyrics",
+ "columnName": "structured_lyrics",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "updatedAt",
+ "columnName": "updated_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "song_id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "scrobble",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`dbId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `submission` INTEGER NOT NULL, `server` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "dbId",
+ "columnName": "dbId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "submission",
+ "columnName": "submission",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "server",
+ "columnName": "server",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "dbId"
+ ]
+ },
+ "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, '47867264ef33da252bb39a7625c48d2b')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.elzify.music.database.AppDatabase/16.json b/app/schemas/com.elzify.music.database.AppDatabase/16.json
new file mode 100644
index 00000000..3f9a1700
--- /dev/null
+++ b/app/schemas/com.elzify.music.database.AppDatabase/16.json
@@ -0,0 +1,1214 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 16,
+ "identityHash": "47867264ef33da252bb39a7625c48d2b",
+ "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, `sampling_rate` INTEGER, `bit_depth` 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": "samplingRate",
+ "columnName": "sampling_rate",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "bitDepth",
+ "columnName": "bit_depth",
+ "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, `local_address` TEXT, `timestamp` INTEGER NOT NULL, `low_security` INTEGER NOT NULL DEFAULT false, `client_cert` TEXT, 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": "localAddress",
+ "columnName": "local_address",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isLowSecurity",
+ "columnName": "low_security",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "clientCert",
+ "columnName": "client_cert",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "recent_search",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`search` TEXT NOT NULL, `timestamp` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`search`))",
+ "fields": [
+ {
+ "fieldPath": "search",
+ "columnName": "search",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "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, `sampling_rate` INTEGER, `bit_depth` 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": "samplingRate",
+ "columnName": "sampling_rate",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "bitDepth",
+ "columnName": "bit_depth",
+ "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, `sampling_rate` INTEGER, `bit_depth` 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": "samplingRate",
+ "columnName": "sampling_rate",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "bitDepth",
+ "columnName": "bit_depth",
+ "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": []
+ },
+ {
+ "tableName": "playlist",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `duration` INTEGER NOT NULL, `songCount` INTEGER NOT NULL, `coverArt` TEXT, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "duration",
+ "columnName": "duration",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "songCount",
+ "columnName": "songCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "coverArtId",
+ "columnName": "coverArt",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "lyrics_cache",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song_id` TEXT NOT NULL, `artist` TEXT, `title` TEXT, `lyrics` TEXT, `structured_lyrics` TEXT, `updated_at` INTEGER NOT NULL, PRIMARY KEY(`song_id`))",
+ "fields": [
+ {
+ "fieldPath": "songId",
+ "columnName": "song_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "artist",
+ "columnName": "artist",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "lyrics",
+ "columnName": "lyrics",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "structuredLyrics",
+ "columnName": "structured_lyrics",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "updatedAt",
+ "columnName": "updated_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "song_id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "scrobble",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`dbId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `submission` INTEGER NOT NULL, `server` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "dbId",
+ "columnName": "dbId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "submission",
+ "columnName": "submission",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "server",
+ "columnName": "server",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "dbId"
+ ]
+ },
+ "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, '47867264ef33da252bb39a7625c48d2b')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/debug/res/xml/network_security_config.xml b/app/src/debug/res/xml/network_security_config.xml
new file mode 100644
index 00000000..8a76775f
--- /dev/null
+++ b/app/src/debug/res/xml/network_security_config.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/degoogled/java/com/elzify/music/ui/fragment/ToolbarFragment.java b/app/src/degoogled/java/com/elzify/music/ui/fragment/ToolbarFragment.java
index 5bead062..d5cee0ec 100644
--- a/app/src/degoogled/java/com/elzify/music/ui/fragment/ToolbarFragment.java
+++ b/app/src/degoogled/java/com/elzify/music/ui/fragment/ToolbarFragment.java
@@ -2,9 +2,6 @@
import android.os.Bundle;
import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
@@ -13,7 +10,6 @@
import androidx.fragment.app.Fragment;
import androidx.media3.common.util.UnstableApi;
-import com.elzify.music.R;
import com.elzify.music.databinding.FragmentToolbarBinding;
import com.elzify.music.ui.activity.MainActivity;
@@ -25,19 +21,6 @@ public class ToolbarFragment extends Fragment {
private MainActivity activity;
public ToolbarFragment() {
- // Required empty public constructor
- }
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setHasOptionsMenu(true);
- }
-
- @Override
- public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
- super.onCreateOptionsMenu(menu, inflater);
- inflater.inflate(R.menu.main_page_menu, menu);
}
@Override
@@ -49,17 +32,4 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
return view;
}
-
- @Override
- public boolean onOptionsItemSelected(@NonNull MenuItem item) {
- if (item.getItemId() == R.id.action_search) {
- activity.navController.navigate(R.id.searchFragment);
- return true;
- } else if (item.getItemId() == R.id.action_settings) {
- activity.navController.navigate(R.id.settingsFragment);
- return true;
- }
-
- return false;
- }
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7a6c915b..3e33f49b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -18,18 +18,13 @@
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
- android:theme="@style/AppTheme.SplashScreen"
- android:usesCleartextTraffic="true">
+ android:theme="@style/AppTheme.SplashScreen">
-
-
@@ -37,6 +32,7 @@
diff --git a/app/src/main/java/com/elzify/music/App.java b/app/src/main/java/com/elzify/music/App.java
index 64ebc031..87ec6aa0 100644
--- a/app/src/main/java/com/elzify/music/App.java
+++ b/app/src/main/java/com/elzify/music/App.java
@@ -3,9 +3,12 @@
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
+import android.util.Log;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
+import androidx.security.crypto.EncryptedSharedPreferences;
+import androidx.security.crypto.MasterKey;
import com.elzify.music.github.Github;
import com.elzify.music.helper.ThemeHelper;
@@ -14,41 +17,53 @@
import com.elzify.music.util.ClientCertManager;
import com.elzify.music.util.Preferences;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+
public class App extends Application {
private static App instance;
private static Context context;
private static Subsonic subsonic;
private static Github github;
private static SharedPreferences preferences;
+ private static SharedPreferences encryptedPreferences;
@Override
public void onCreate() {
super.onCreate();
-
- SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
- String themePref = sharedPreferences.getString(Preferences.THEME, ThemeHelper.DEFAULT_MODE);
- ThemeHelper.applyTheme(themePref);
-
- instance = new App();
+ instance = this;
context = getApplicationContext();
+
preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ try {
+ MasterKey masterKey = new MasterKey.Builder(context)
+ .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
+ .build();
+
+ encryptedPreferences = EncryptedSharedPreferences.create(
+ context,
+ "encrypted_preferences",
+ masterKey,
+ EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
+ EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
+ );
+ } catch (GeneralSecurityException | IOException e) {
+ Log.e("App", "Could not initialize EncryptedSharedPreferences", e);
+ encryptedPreferences = preferences;
+ }
+
+ String themePref = preferences.getString(Preferences.THEME, ThemeHelper.DEFAULT_MODE);
+ ThemeHelper.applyTheme(themePref);
+
ClientCertManager.setupSslSocketFactory(context);
}
public static App getInstance() {
- if (instance == null) {
- instance = new App();
- }
-
return instance;
}
public static Context getContext() {
- if (context == null) {
- context = getInstance();
- }
-
return context;
}
@@ -116,6 +131,13 @@ public SharedPreferences getPreferences() {
return preferences;
}
+ public SharedPreferences getEncryptedPreferences() {
+ if (encryptedPreferences == null) {
+ return getPreferences();
+ }
+ return encryptedPreferences;
+ }
+
public static void refreshSubsonicClient() {
subsonic = getSubsonicClient();
}
diff --git a/app/src/main/java/com/elzify/music/broadcast/receiver/ConnectivityStatusBroadcastReceiver.java b/app/src/main/java/com/elzify/music/broadcast/receiver/ConnectivityStatusBroadcastReceiver.java
index 2f04f566..64e5c5f2 100644
--- a/app/src/main/java/com/elzify/music/broadcast/receiver/ConnectivityStatusBroadcastReceiver.java
+++ b/app/src/main/java/com/elzify/music/broadcast/receiver/ConnectivityStatusBroadcastReceiver.java
@@ -9,6 +9,7 @@
import androidx.annotation.OptIn;
import androidx.media3.common.util.UnstableApi;
+import com.elzify.music.service.MediaManager;
import com.elzify.music.ui.activity.MainActivity;
@OptIn(markerClass = UnstableApi.class)
@@ -28,7 +29,8 @@ public void onReceive(Context context, Intent intent) {
activity.bind.offlineModeTextView.setVisibility(View.VISIBLE);
} else {
activity.bind.offlineModeTextView.setVisibility(View.GONE);
+ MediaManager.submitPendingScrobbles();
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/elzify/music/database/AppDatabase.java b/app/src/main/java/com/elzify/music/database/AppDatabase.java
index e6f33a14..012573af 100644
--- a/app/src/main/java/com/elzify/music/database/AppDatabase.java
+++ b/app/src/main/java/com/elzify/music/database/AppDatabase.java
@@ -16,6 +16,7 @@
import com.elzify.music.database.dao.PlaylistDao;
import com.elzify.music.database.dao.QueueDao;
import com.elzify.music.database.dao.RecentSearchDao;
+import com.elzify.music.database.dao.ScrobbleDao;
import com.elzify.music.database.dao.ServerDao;
import com.elzify.music.database.dao.SessionMediaItemDao;
import com.elzify.music.model.Chronology;
@@ -24,19 +25,16 @@
import com.elzify.music.model.LyricsCache;
import com.elzify.music.model.Queue;
import com.elzify.music.model.RecentSearch;
+import com.elzify.music.model.Scrobble;
import com.elzify.music.model.Server;
import com.elzify.music.model.SessionMediaItem;
import com.elzify.music.subsonic.models.Playlist;
@UnstableApi
@Database(
- version = 14,
- entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class, SessionMediaItem.class, Playlist.class, LyricsCache.class},
- autoMigrations = {
- @AutoMigration(from = 10, to = 11),
- @AutoMigration(from = 11, to = 12),
- @AutoMigration(from = 13, to = 14),
- }
+ version = 16,
+ entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class, SessionMediaItem.class, Playlist.class, LyricsCache.class, Scrobble.class},
+ autoMigrations = {@AutoMigration(from = 10, to = 11), @AutoMigration(from = 11, to = 12)}
)
@TypeConverters({DateConverters.class})
public abstract class AppDatabase extends RoomDatabase {
@@ -70,4 +68,6 @@ public static synchronized AppDatabase getInstance() {
public abstract PlaylistDao playlistDao();
public abstract LyricsDao lyricsDao();
+
+ public abstract ScrobbleDao scrobbleDao();
}
diff --git a/app/src/main/java/com/elzify/music/database/dao/ScrobbleDao.java b/app/src/main/java/com/elzify/music/database/dao/ScrobbleDao.java
new file mode 100644
index 00000000..e995d83d
--- /dev/null
+++ b/app/src/main/java/com/elzify/music/database/dao/ScrobbleDao.java
@@ -0,0 +1,25 @@
+package com.elzify.music.database.dao;
+
+import androidx.room.Dao;
+import androidx.room.Delete;
+import androidx.room.Insert;
+import androidx.room.Query;
+
+import com.elzify.music.model.Scrobble;
+
+import java.util.List;
+
+@Dao
+public interface ScrobbleDao {
+ @Query("SELECT * FROM scrobble WHERE server = :server ORDER BY timestamp ASC")
+ List getPendingScrobbles(String server);
+
+ @Insert
+ void insert(Scrobble scrobble);
+
+ @Delete
+ void delete(Scrobble scrobble);
+
+ @Query("DELETE FROM scrobble WHERE dbId = :dbId")
+ void deleteById(Long dbId);
+}
diff --git a/app/src/main/java/com/elzify/music/github/GithubRetrofitClient.kt b/app/src/main/java/com/elzify/music/github/GithubRetrofitClient.kt
index be335e19..d1711a2f 100644
--- a/app/src/main/java/com/elzify/music/github/GithubRetrofitClient.kt
+++ b/app/src/main/java/com/elzify/music/github/GithubRetrofitClient.kt
@@ -1,5 +1,6 @@
package com.elzify.music.github
+import com.elzify.music.BuildConfig
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
@@ -24,7 +25,10 @@ class GithubRetrofitClient(github: Github) {
private fun getHttpLoggingInterceptor(): HttpLoggingInterceptor {
val loggingInterceptor = HttpLoggingInterceptor()
- loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
+ loggingInterceptor.setLevel(
+ if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BASIC
+ else HttpLoggingInterceptor.Level.NONE
+ )
return loggingInterceptor
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/elzify/music/glide/IPv6StringLoader.java b/app/src/main/java/com/elzify/music/glide/IPv6StringLoader.java
index 8bc01543..91a0c022 100644
--- a/app/src/main/java/com/elzify/music/glide/IPv6StringLoader.java
+++ b/app/src/main/java/com/elzify/music/glide/IPv6StringLoader.java
@@ -17,7 +17,7 @@
import java.net.URL;
public class IPv6StringLoader implements ModelLoader {
- private static final int DEFAULT_TIMEOUT_MS = 2500;
+ private static final int DEFAULT_TIMEOUT_MS = 8000;
@Override
public boolean handles(@NonNull String model) {
@@ -107,4 +107,4 @@ public void teardown() {
// No-op
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/elzify/music/interfaces/HomeRearrangementCallback.java b/app/src/main/java/com/elzify/music/interfaces/HomeRearrangementCallback.java
new file mode 100644
index 00000000..7cb6d927
--- /dev/null
+++ b/app/src/main/java/com/elzify/music/interfaces/HomeRearrangementCallback.java
@@ -0,0 +1,5 @@
+package com.elzify.music.interfaces;
+
+public interface HomeRearrangementCallback {
+ void onChanged();
+}
diff --git a/app/src/main/java/com/elzify/music/lastfm/LastFm.kt b/app/src/main/java/com/elzify/music/lastfm/LastFm.kt
new file mode 100644
index 00000000..aeb8d6be
--- /dev/null
+++ b/app/src/main/java/com/elzify/music/lastfm/LastFm.kt
@@ -0,0 +1,9 @@
+package com.elzify.music.lastfm
+
+import com.elzify.music.lastfm.api.TrackClient
+
+object LastFm {
+ const val BASE_URL = "https://ws.audioscrobbler.com/2.0/"
+
+ val trackClient: TrackClient by lazy { TrackClient() }
+}
diff --git a/app/src/main/java/com/elzify/music/lastfm/LastFmRetrofitClient.kt b/app/src/main/java/com/elzify/music/lastfm/LastFmRetrofitClient.kt
new file mode 100644
index 00000000..fa8207b4
--- /dev/null
+++ b/app/src/main/java/com/elzify/music/lastfm/LastFmRetrofitClient.kt
@@ -0,0 +1,30 @@
+package com.elzify.music.lastfm
+
+import com.elzify.music.BuildConfig
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+
+class LastFmRetrofitClient {
+ val retrofit: Retrofit = Retrofit.Builder()
+ .baseUrl(LastFm.BASE_URL)
+ .addConverterFactory(GsonConverterFactory.create())
+ .client(getOkHttpClient())
+ .build()
+
+ private fun getOkHttpClient(): OkHttpClient {
+ return OkHttpClient.Builder()
+ .addInterceptor(getHttpLoggingInterceptor())
+ .build()
+ }
+
+ private fun getHttpLoggingInterceptor(): HttpLoggingInterceptor {
+ val loggingInterceptor = HttpLoggingInterceptor()
+ loggingInterceptor.setLevel(
+ if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BASIC
+ else HttpLoggingInterceptor.Level.NONE
+ )
+ return loggingInterceptor
+ }
+}
diff --git a/app/src/main/java/com/elzify/music/lastfm/api/TrackClient.kt b/app/src/main/java/com/elzify/music/lastfm/api/TrackClient.kt
new file mode 100644
index 00000000..9081ac96
--- /dev/null
+++ b/app/src/main/java/com/elzify/music/lastfm/api/TrackClient.kt
@@ -0,0 +1,14 @@
+package com.elzify.music.lastfm.api
+
+import com.elzify.music.lastfm.LastFmRetrofitClient
+import com.elzify.music.lastfm.models.LastFmTrackResponse
+import retrofit2.Call
+
+class TrackClient {
+ private val trackService: TrackService =
+ LastFmRetrofitClient().retrofit.create(TrackService::class.java)
+
+ fun getTrackInfo(artist: String, track: String, username: String, apiKey: String): Call {
+ return trackService.getTrackInfo(artist = artist, track = track, username = username, apiKey = apiKey)
+ }
+}
diff --git a/app/src/main/java/com/elzify/music/lastfm/api/TrackService.kt b/app/src/main/java/com/elzify/music/lastfm/api/TrackService.kt
new file mode 100644
index 00000000..b2b4efbf
--- /dev/null
+++ b/app/src/main/java/com/elzify/music/lastfm/api/TrackService.kt
@@ -0,0 +1,18 @@
+package com.elzify.music.lastfm.api
+
+import com.elzify.music.lastfm.models.LastFmTrackResponse
+import retrofit2.Call
+import retrofit2.http.GET
+import retrofit2.http.Query
+
+interface TrackService {
+ @GET(".")
+ fun getTrackInfo(
+ @Query("method") method: String = "track.getInfo",
+ @Query("artist") artist: String,
+ @Query("track") track: String,
+ @Query("username") username: String,
+ @Query("api_key") apiKey: String,
+ @Query("format") format: String = "json"
+ ): Call
+}
diff --git a/app/src/main/java/com/elzify/music/lastfm/models/LastFmTrackResponse.kt b/app/src/main/java/com/elzify/music/lastfm/models/LastFmTrackResponse.kt
new file mode 100644
index 00000000..1cc7b63f
--- /dev/null
+++ b/app/src/main/java/com/elzify/music/lastfm/models/LastFmTrackResponse.kt
@@ -0,0 +1,18 @@
+package com.elzify.music.lastfm.models
+
+import androidx.annotation.Keep
+import com.google.gson.annotations.SerializedName
+
+@Keep
+data class LastFmTrackResponse(
+ @SerializedName("track")
+ val track: LastFmTrack? = null
+)
+
+@Keep
+data class LastFmTrack(
+ @SerializedName("name")
+ val name: String? = null,
+ @SerializedName("userplaycount")
+ val userPlayCount: String? = null
+)
diff --git a/app/src/main/java/com/elzify/music/model/Scrobble.kt b/app/src/main/java/com/elzify/music/model/Scrobble.kt
new file mode 100644
index 00000000..0fefdcb4
--- /dev/null
+++ b/app/src/main/java/com/elzify/music/model/Scrobble.kt
@@ -0,0 +1,19 @@
+package com.elzify.music.model
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "scrobble")
+data class Scrobble(
+ @PrimaryKey(autoGenerate = true)
+ val dbId: Long = 0,
+ @ColumnInfo(name = "id")
+ val id: String,
+ @ColumnInfo(name = "timestamp")
+ val timestamp: Long,
+ @ColumnInfo(name = "submission")
+ val submission: Boolean,
+ @ColumnInfo(name = "server")
+ val server: String
+)
diff --git a/app/src/main/java/com/elzify/music/provider/AlbumArtContentProvider.java b/app/src/main/java/com/elzify/music/provider/AlbumArtContentProvider.java
index 21ca4158..852d051e 100644
--- a/app/src/main/java/com/elzify/music/provider/AlbumArtContentProvider.java
+++ b/app/src/main/java/com/elzify/music/provider/AlbumArtContentProvider.java
@@ -8,7 +8,6 @@
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
-import android.util.Base64;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -52,18 +51,19 @@ public static Uri contentUri(String artworkId) {
@Nullable
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
+ if (uriMatcher.match(uri) != 1) {
+ throw new FileNotFoundException("Unknown URI: " + uri);
+ }
+
Context context = getContext();
String albumId = uri.getLastPathSegment();
- Uri artworkUri;
-
- if (albumId != null && albumId.startsWith("ir_")) {
- String encodedUrl = albumId.substring("ir_".length());
- String decodedUrl = new String(Base64.decode(encodedUrl, Base64.URL_SAFE | Base64.NO_WRAP));
- artworkUri = Uri.parse(decodedUrl);
- } else {
- artworkUri = Uri.parse(CustomGlideRequest.createUrl(albumId, Preferences.getImageSize()));
+
+ if (albumId == null || albumId.isEmpty() || albumId.contains("..") || albumId.contains("/")) {
+ throw new FileNotFoundException("Invalid album ID");
}
+ Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(albumId, Preferences.getImageSize()));
+
try {
// use pipe to communicate between background thread and caller of openFile()
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
diff --git a/app/src/main/java/com/elzify/music/repository/ArtistRepository.java b/app/src/main/java/com/elzify/music/repository/ArtistRepository.java
index 89c45cb5..2465fb17 100644
--- a/app/src/main/java/com/elzify/music/repository/ArtistRepository.java
+++ b/app/src/main/java/com/elzify/music/repository/ArtistRepository.java
@@ -5,6 +5,7 @@
import android.util.Log;
import com.elzify.music.App;
+import com.elzify.music.subsonic.api.navidrome.NavidromeClient;
import com.elzify.music.subsonic.base.ApiResponse;
import com.elzify.music.subsonic.models.ArtistID3;
import com.elzify.music.subsonic.models.AlbumID3;
@@ -362,6 +363,36 @@ public void onFailure(@NonNull Call call, @NonNull Throwable t) {
return topSongs;
}
+ public MutableLiveData> getRecentlyPlayedArtists(int count) {
+ MutableLiveData> result = new MutableLiveData<>();
+ java.util.concurrent.Executors.newSingleThreadExecutor().execute(() -> {
+ try {
+ List artists = NavidromeClient.getInstance().getRecentlyPlayedArtists(count);
+ Log.d("ArtistRepository", "getRecentlyPlayedArtists: returning " + artists.size() + " artists");
+ result.postValue(artists);
+ } catch (Exception e) {
+ Log.e("ArtistRepository", "getRecentlyPlayedArtists: exception", e);
+ result.postValue(null);
+ }
+ });
+ return result;
+ }
+
+ public MutableLiveData> getTopPlayedArtists(int count) {
+ MutableLiveData> result = new MutableLiveData<>();
+ java.util.concurrent.Executors.newSingleThreadExecutor().execute(() -> {
+ try {
+ List artists = NavidromeClient.getInstance().getTopPlayedArtists(count);
+ Log.d("ArtistRepository", "getTopPlayedArtists: returning " + artists.size() + " artists");
+ result.postValue(artists);
+ } catch (Exception e) {
+ Log.e("ArtistRepository", "getTopPlayedArtists: exception", e);
+ result.postValue(null);
+ }
+ });
+ return result;
+ }
+
private void addToMutableLiveData(MutableLiveData> liveData, ArtistID3 artist) {
List liveArtists = liveData.getValue();
if (liveArtists != null) liveArtists.add(artist);
diff --git a/app/src/main/java/com/elzify/music/repository/ChronologyRepository.java b/app/src/main/java/com/elzify/music/repository/ChronologyRepository.java
index 210f1efe..fd900f49 100644
--- a/app/src/main/java/com/elzify/music/repository/ChronologyRepository.java
+++ b/app/src/main/java/com/elzify/music/repository/ChronologyRepository.java
@@ -15,6 +15,10 @@ public LiveData> getChronology(String server, long start, long
return chronologyDao.getAllFrom(start, end, server);
}
+ public LiveData> getLastPlayed(String server, int count) {
+ return chronologyDao.getLastPlayed(server, count);
+ }
+
public void insert(Chronology item) {
InsertThreadSafe insert = new InsertThreadSafe(chronologyDao, item);
Thread thread = new Thread(insert);
diff --git a/app/src/main/java/com/elzify/music/repository/LrcGetRepository.java b/app/src/main/java/com/elzify/music/repository/LrcGetRepository.java
new file mode 100644
index 00000000..4b802b12
--- /dev/null
+++ b/app/src/main/java/com/elzify/music/repository/LrcGetRepository.java
@@ -0,0 +1,206 @@
+package com.elzify.music.repository;
+
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.MutableLiveData;
+
+import com.elzify.music.subsonic.models.Child;
+import com.elzify.music.subsonic.models.Line;
+import com.elzify.music.subsonic.models.LyricsList;
+import com.elzify.music.subsonic.models.StructuredLyrics;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import okhttp3.HttpUrl;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+
+public class LrcGetRepository {
+ private static final String API_BASE_URL = "https://lrclib.net/api/get";
+ private static final Pattern TIMESTAMP_PATTERN = Pattern.compile("\\[(\\d{1,2}):(\\d{2})(?:\\.(\\d{1,3}))?\\]");
+
+ private final OkHttpClient client = new OkHttpClient();
+ private final Gson gson = new Gson();
+ private final ExecutorService executor = Executors.newSingleThreadExecutor();
+
+ public static class LrcGetLyricsResult {
+ private final String plainLyrics;
+ private final LyricsList syncedLyrics;
+
+ public LrcGetLyricsResult(String plainLyrics, LyricsList syncedLyrics) {
+ this.plainLyrics = plainLyrics;
+ this.syncedLyrics = syncedLyrics;
+ }
+
+ public String getPlainLyrics() {
+ return plainLyrics;
+ }
+
+ public LyricsList getSyncedLyrics() {
+ return syncedLyrics;
+ }
+ }
+
+ public MutableLiveData getLyrics(@NonNull Child media) {
+ MutableLiveData result = new MutableLiveData<>(null);
+
+ if (TextUtils.isEmpty(media.getArtist()) || TextUtils.isEmpty(media.getTitle())) {
+ return result;
+ }
+
+ executor.execute(() -> {
+ HttpUrl baseUrl = HttpUrl.parse(API_BASE_URL);
+ if (baseUrl == null) {
+ return;
+ }
+
+ HttpUrl.Builder urlBuilder = baseUrl.newBuilder()
+ .addQueryParameter("artist_name", media.getArtist())
+ .addQueryParameter("track_name", media.getTitle());
+
+ if (media.getDuration() != null && media.getDuration() > 0) {
+ urlBuilder.addQueryParameter("duration", String.valueOf(media.getDuration()));
+ }
+
+ Request request = new Request.Builder()
+ .url(urlBuilder.build())
+ .header("Accept", "application/json")
+ .header("User-Agent", "Rollynn")
+ .build();
+
+ try (Response response = client.newCall(request).execute()) {
+ if (!response.isSuccessful() || response.body() == null) {
+ return;
+ }
+
+ String jsonBody = response.body().string();
+ JsonObject jsonObject = gson.fromJson(jsonBody, JsonObject.class);
+ if (jsonObject == null || jsonObject.isJsonNull()) {
+ return;
+ }
+
+ String plainLyrics = getString(jsonObject, "plainLyrics");
+ String syncedLyricsText = getString(jsonObject, "syncedLyrics");
+ String artist = getString(jsonObject, "artistName");
+ String title = getString(jsonObject, "trackName");
+ String lang = getString(jsonObject, "lang");
+
+ LyricsList syncedLyrics = parseSyncedLyrics(syncedLyricsText, artist, title, lang);
+
+ if (!TextUtils.isEmpty(plainLyrics) || syncedLyrics != null) {
+ result.postValue(new LrcGetLyricsResult(plainLyrics, syncedLyrics));
+ }
+ } catch (IOException ignored) {
+ }
+ });
+
+ return result;
+ }
+
+ private LyricsList parseSyncedLyrics(String lrcText, String artist, String title, String lang) {
+ if (TextUtils.isEmpty(lrcText)) {
+ return null;
+ }
+
+ List parsedLines = new ArrayList<>();
+ String[] rawLines = lrcText.split("\\r?\\n");
+
+ for (String rawLine : rawLines) {
+ if (TextUtils.isEmpty(rawLine)) {
+ continue;
+ }
+
+ Matcher matcher = TIMESTAMP_PATTERN.matcher(rawLine);
+ List starts = new ArrayList<>();
+ int timestampEndIndex = -1;
+
+ while (matcher.find()) {
+ int minutes = parsePart(matcher.group(1));
+ int seconds = parsePart(matcher.group(2));
+ int millis = parseMillis(matcher.group(3));
+ starts.add((minutes * 60 * 1000) + (seconds * 1000) + millis);
+ timestampEndIndex = matcher.end();
+ }
+
+ if (starts.isEmpty()) {
+ continue;
+ }
+
+ String value = rawLine.substring(Math.max(timestampEndIndex, 0)).trim();
+ if (value.isEmpty()) {
+ continue;
+ }
+
+ for (Integer start : starts) {
+ Line line = new Line();
+ line.setStart(start);
+ line.setValue(value);
+ parsedLines.add(line);
+ }
+ }
+
+ if (parsedLines.isEmpty()) {
+ return null;
+ }
+
+ parsedLines.sort(Comparator.comparing(Line::getStart, Comparator.nullsLast(Integer::compareTo)));
+
+ StructuredLyrics structuredLyrics = new StructuredLyrics();
+ structuredLyrics.setDisplayArtist(artist);
+ structuredLyrics.setDisplayTitle(title);
+ structuredLyrics.setLang(lang);
+ structuredLyrics.setOffset(0);
+ structuredLyrics.setSynced(true);
+ structuredLyrics.setLine(parsedLines);
+
+ LyricsList lyricsList = new LyricsList();
+ lyricsList.setStructuredLyrics(Collections.singletonList(structuredLyrics));
+ return lyricsList;
+ }
+
+ private int parsePart(String value) {
+ try {
+ return Integer.parseInt(Objects.requireNonNullElse(value, "0"));
+ } catch (NumberFormatException ignored) {
+ return 0;
+ }
+ }
+
+ private int parseMillis(String fraction) {
+ if (TextUtils.isEmpty(fraction)) {
+ return 0;
+ }
+
+ String normalized = fraction.trim();
+ if (normalized.length() == 1) {
+ return parsePart(normalized) * 100;
+ }
+ if (normalized.length() == 2) {
+ return parsePart(normalized) * 10;
+ }
+
+ return parsePart(normalized.substring(0, Math.min(3, normalized.length())));
+ }
+
+ private String getString(JsonObject jsonObject, String key) {
+ if (!jsonObject.has(key) || jsonObject.get(key).isJsonNull()) {
+ return null;
+ }
+
+ String value = jsonObject.get(key).getAsString();
+ return TextUtils.isEmpty(value) ? null : value;
+ }
+}
diff --git a/app/src/main/java/com/elzify/music/repository/PlaylistRepository.java b/app/src/main/java/com/elzify/music/repository/PlaylistRepository.java
index e4b84b07..d9f7201d 100644
--- a/app/src/main/java/com/elzify/music/repository/PlaylistRepository.java
+++ b/app/src/main/java/com/elzify/music/repository/PlaylistRepository.java
@@ -1,11 +1,9 @@
package com.elzify.music.repository;
import androidx.annotation.NonNull;
-import androidx.annotation.OptIn;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
-import androidx.media3.common.util.UnstableApi;
import com.elzify.music.App;
import com.elzify.music.R;
@@ -171,6 +169,10 @@ public void onFailure(@NonNull Call call, @NonNull Throwable t) {
}
}
+ public void addSongToPlaylist(String playlistId, ArrayList songsId, Boolean playlistVisibilityIsPublic) {
+ addSongToPlaylist(playlistId, songsId, playlistVisibilityIsPublic, null);
+ }
+
public void removeSongFromPlaylist(String playlistId, int index, AddToPlaylistCallback callback) {
ArrayList indexes = new ArrayList<>();
indexes.add(index);
@@ -194,10 +196,6 @@ public void onFailure(@NonNull Call call, @NonNull Throwable t) {
});
}
- public void addSongToPlaylist(String playlistId, ArrayList songsId, Boolean playlistVisibilityIsPublic) {
- addSongToPlaylist(playlistId, songsId, playlistVisibilityIsPublic, null);
- }
-
public void createPlaylist(String playlistId, String name, ArrayList songsId) {
App.getSubsonicClientInstance(false)
.getPlaylistClient()
@@ -241,7 +239,6 @@ public void onFailure(@NonNull Call call, @NonNull Throwable t) {
});
}
- @OptIn(markerClass = UnstableApi.class)
private void updateLocalPinnedPlaylistName(String id, String newName) {
new Thread(() -> {
List pinned = playlistDao.getAllSync();
diff --git a/app/src/main/java/com/elzify/music/repository/SongRepository.java b/app/src/main/java/com/elzify/music/repository/SongRepository.java
index bf120a7c..71ad2b11 100644
--- a/app/src/main/java/com/elzify/music/repository/SongRepository.java
+++ b/app/src/main/java/com/elzify/music/repository/SongRepository.java
@@ -6,16 +6,24 @@
import androidx.lifecycle.MutableLiveData;
import com.elzify.music.App;
+import com.elzify.music.database.AppDatabase;
+import com.elzify.music.database.dao.ScrobbleDao;
+import com.elzify.music.model.Scrobble;
import com.elzify.music.subsonic.base.ApiResponse;
import com.elzify.music.subsonic.models.Child;
import com.elzify.music.subsonic.models.SubsonicResponse;
import com.elzify.music.util.Constants.SeedType;
+import com.elzify.music.util.Preferences;
+
+import com.elzify.music.subsonic.api.navidrome.NavidromeClient;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
import retrofit2.Call;
import retrofit2.Callback;
@@ -24,13 +32,15 @@
public class SongRepository {
private static final String TAG = "SongRepository";
+ private final ScrobbleDao scrobbleDao = AppDatabase.getInstance().scrobbleDao();
+ private final AtomicBoolean pendingScrobbleSyncInProgress = new AtomicBoolean(false);
public interface MediaCallbackInternal {
void onSongsAvailable(List songs);
}
public MutableLiveData> getStarredSongs(boolean random, int size) {
- MutableLiveData> starredSongs = new MutableLiveData<>(Collections.emptyList());
+ MutableLiveData> starredSongs = new MutableLiveData<>(null);
App.getSubsonicClientInstance(false)
.getAlbumSongListClient()
@@ -294,6 +304,36 @@ public MutableLiveData> getRandomSample(int number, Integer fromYear
return randomSongsSample;
}
+ public MutableLiveData> getRecentlyPlayedSongs(int count) {
+ MutableLiveData> recentlyPlayedSongs = new MutableLiveData<>();
+ Executors.newSingleThreadExecutor().execute(() -> {
+ try {
+ List songs = NavidromeClient.getInstance().getRecentlyPlayedSongs(count);
+ Log.d(TAG, "getRecentlyPlayedSongs: returning " + songs.size() + " songs");
+ recentlyPlayedSongs.postValue(songs);
+ } catch (Exception e) {
+ Log.e(TAG, "getRecentlyPlayedSongs: exception", e);
+ recentlyPlayedSongs.postValue(null);
+ }
+ });
+ return recentlyPlayedSongs;
+ }
+
+ public MutableLiveData> getTopPlayedSongs(int count) {
+ MutableLiveData> topPlayedSongs = new MutableLiveData<>();
+ Executors.newSingleThreadExecutor().execute(() -> {
+ try {
+ List songs = NavidromeClient.getInstance().getTopPlayedSongs(count);
+ Log.d(TAG, "getTopPlayedSongs: returning " + songs.size() + " songs");
+ topPlayedSongs.postValue(songs);
+ } catch (Exception e) {
+ Log.e(TAG, "getTopPlayedSongs: exception", e);
+ topPlayedSongs.postValue(null);
+ }
+ });
+ return topPlayedSongs;
+ }
+
public MutableLiveData> getRandomSampleWithGenre(int number, Integer fromYear, Integer toYear, String genre) {
MutableLiveData> randomSongsSample = new MutableLiveData<>();
@@ -314,12 +354,71 @@ public MutableLiveData> getRandomSampleWithGenre(int number, Integer
}
public void scrobble(String id, boolean submission) {
- App.getSubsonicClientInstance(false).getMediaAnnotationClient().scrobble(id, submission).enqueue(new Callback() {
- @Override public void onResponse(@NonNull Call call, @NonNull Response response) {}
- @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) {}
+ scrobble(id, submission, null);
+ }
+
+ public void scrobble(String id, boolean submission, Long time) {
+ String server = Preferences.getServerId();
+ long scrobbleTime = time != null ? time : System.currentTimeMillis();
+
+ App.getSubsonicClientInstance(false).getMediaAnnotationClient().scrobble(id, submission, time).enqueue(new Callback() {
+ @Override
+ public void onResponse(@NonNull Call call, @NonNull Response response) {
+ if (response.isSuccessful()) {
+ submitPendingScrobbles();
+ } else {
+ saveScrobbleLocally(id, submission, scrobbleTime, server);
+ }
+ }
+
+ @Override
+ public void onFailure(@NonNull Call call, @NonNull Throwable t) {
+ saveScrobbleLocally(id, submission, scrobbleTime, server);
+ }
});
}
+ private void saveScrobbleLocally(String id, boolean submission, long time, String server) {
+ if (server == null) return;
+ new Thread(() -> {
+ scrobbleDao.insert(new Scrobble(0, id, time, submission, server));
+ }).start();
+ }
+
+ public void submitPendingScrobbles() {
+ String server = Preferences.getServerId();
+ if (server == null) return;
+ if (!pendingScrobbleSyncInProgress.compareAndSet(false, true)) return;
+
+ new Thread(() -> {
+ try {
+ List pending = scrobbleDao.getPendingScrobbles(server);
+ if (pending.isEmpty()) return;
+
+ for (Scrobble scrobble : pending) {
+ try {
+ Response response = App.getSubsonicClientInstance(false)
+ .getMediaAnnotationClient()
+ .scrobble(scrobble.getId(), scrobble.getSubmission(), scrobble.getTimestamp())
+ .execute();
+
+ if (response.isSuccessful()) {
+ scrobbleDao.delete(scrobble);
+ } else {
+ Log.w(TAG, "Pending scrobble sync failed with HTTP " + response.code());
+ break;
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Pending scrobble sync failed", e);
+ break;
+ }
+ }
+ } finally {
+ pendingScrobbleSyncInProgress.set(false);
+ }
+ }).start();
+ }
+
public void setRating(String id, int rating) {
App.getSubsonicClientInstance(false).getMediaAnnotationClient().setRating(id, rating).enqueue(new Callback() {
@Override public void onResponse(@NonNull Call call, @NonNull Response response) {}
@@ -385,4 +484,4 @@ public MutableLiveData getSongLyrics(Child song) {
});
return lyrics;
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/elzify/music/service/BaseMediaService.kt b/app/src/main/java/com/elzify/music/service/BaseMediaService.kt
index 43852262..3f7540cb 100644
--- a/app/src/main/java/com/elzify/music/service/BaseMediaService.kt
+++ b/app/src/main/java/com/elzify/music/service/BaseMediaService.kt
@@ -73,11 +73,33 @@ open class BaseMediaService : MediaLibraryService() {
widgetUpdateScheduled = false
return
}
+
+ checkScrobbleThreshold(player)
updateWidget(player)
widgetUpdateHandler.postDelayed(this, WIDGET_UPDATE_INTERVAL_MS)
}
}
+ private fun checkScrobbleThreshold(player: Player) {
+ if (currentTrackScrobbled) return
+ if (player.mediaMetadata.extras?.getString("type") != Constants.MEDIA_TYPE_MUSIC) return
+
+ val duration = player.duration
+ val position = player.currentPosition
+
+ if (duration > 0 && position > 0) {
+ val threshold = Preferences.getScrobbleThreshold()
+ if (position * 100 / duration >= threshold) {
+ currentTrackScrobbled = true
+ MediaManager.scrobble(player.currentMediaItem, true, System.currentTimeMillis())
+ MediaManager.saveChronology(player.currentMediaItem)
+ player.currentMediaItem?.mediaMetadata?.extras?.getString("id")?.let {
+ MediaManager.postScrobbleEvent(it)
+ }
+ }
+ }
+ }
+
private val radioHeaderCheckExecutor: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
private var radioHeaderCheckScheduled = false
private var radioHeaderCheckFuture: ScheduledFuture<*>? = null
@@ -86,6 +108,7 @@ open class BaseMediaService : MediaLibraryService() {
}
private val binder = LocalBinder()
+ private var currentTrackScrobbled = false
open fun playerInitHook() {
initializeExoPlayer()
@@ -142,10 +165,14 @@ open class BaseMediaService : MediaLibraryService() {
player.addListener(object : Player.Listener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
Log.d(TAG, "onMediaItemTransition" + player.currentMediaItemIndex)
+ currentTrackScrobbled = false
if (mediaItem == null) return
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
MediaManager.setLastPlayedTimestamp(mediaItem)
+ if (mediaItem.mediaMetadata.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) {
+ MediaManager.saveChronology(mediaItem)
+ }
}
// Restart header checks for radio streams when media item changes
@@ -201,8 +228,8 @@ open class BaseMediaService : MediaLibraryService() {
override fun onMetadata(metadata: Metadata) {
// Handle streaming metadata (ICY, ID3) for radio / streaming content
val currentItem = player.currentMediaItem ?: return
- val extras = currentItem.mediaMetadata.extras
- if (extras?.getString("type") != Constants.MEDIA_TYPE_RADIO) return
+ val extras = currentItem.mediaMetadata.extras ?: return
+ if (extras.getString("type") != Constants.MEDIA_TYPE_RADIO) return
var artist: String? = null
var title: String? = null
@@ -253,14 +280,14 @@ open class BaseMediaService : MediaLibraryService() {
if (currentIndex == C.INDEX_UNSET) return
val metadataBuilder = currentItem.mediaMetadata.buildUpon()
- val newExtras = Bundle(extras ?: Bundle())
+ val newExtras = Bundle(extras)
// Store individual values in extras for UI
artist?.let { newExtras.putString("radioArtist", it) }
title?.let { newExtras.putString("radioTitle", it) }
// Get station name (preserve if already set)
- val stationName = extras?.getString("stationName")
+ val stationName = extras.getString("stationName")
?: currentItem.mediaMetadata.title?.toString()
?: ""
if (stationName.isNotBlank()) {
@@ -313,9 +340,11 @@ open class BaseMediaService : MediaLibraryService() {
super.onPlaybackStateChanged(playbackState)
if (!player.hasNextMediaItem() &&
playbackState == Player.STATE_ENDED &&
- player.mediaMetadata.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC
+ player.mediaMetadata.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC &&
+ !currentTrackScrobbled
) {
- MediaManager.scrobble(player.currentMediaItem, true)
+ currentTrackScrobbled = true
+ MediaManager.scrobble(player.currentMediaItem, true, System.currentTimeMillis())
MediaManager.saveChronology(player.currentMediaItem)
}
updateWidget(player)
@@ -330,8 +359,9 @@ open class BaseMediaService : MediaLibraryService() {
super.onPositionDiscontinuity(oldPosition, newPosition, reason)
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
- if (oldPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) {
- MediaManager.scrobble(oldPosition.mediaItem, true)
+ if (oldPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC && !currentTrackScrobbled) {
+ currentTrackScrobbled = true
+ MediaManager.scrobble(oldPosition.mediaItem, true, System.currentTimeMillis())
MediaManager.saveChronology(oldPosition.mediaItem)
}
@@ -384,10 +414,8 @@ open class BaseMediaService : MediaLibraryService() {
override fun onTaskRemoved(rootIntent: Intent?) {
val player = mediaLibrarySession.player
-
- if (!player.playWhenReady || player.mediaItemCount == 0) {
- stopSelf()
- }
+ player.pause()
+ stopSelf()
}
override fun onCreate() {
@@ -556,18 +584,17 @@ open class BaseMediaService : MediaLibraryService() {
private fun checkRadioHttpHeaders() {
val player = mediaLibrarySession.player
val currentItem = player.currentMediaItem ?: return
- val extras = currentItem.mediaMetadata.extras
- val mediaType = extras?.getString("type")
- if (mediaType != Constants.MEDIA_TYPE_RADIO) return
+ val extras = currentItem.mediaMetadata.extras ?: return
+ if (extras.getString("type") != Constants.MEDIA_TYPE_RADIO) return
// Skip if we already have embedded metadata (ICY/ID3) - HTTP headers are only fallback
val hasEmbeddedMetadata = !currentItem.mediaMetadata.artist.isNullOrBlank() ||
!currentItem.mediaMetadata.title.isNullOrBlank() ||
- (extras != null && !extras.getString("radioArtist").isNullOrBlank()) ||
- (extras != null && !extras.getString("radioTitle").isNullOrBlank())
+ !extras.getString("radioArtist").isNullOrBlank() ||
+ !extras.getString("radioTitle").isNullOrBlank()
if (hasEmbeddedMetadata) return
- val streamUrl = extras?.getString("uri") ?: currentItem.requestMetadata.mediaUri?.toString()
+ val streamUrl = extras.getString("uri") ?: currentItem.requestMetadata.mediaUri?.toString()
if (streamUrl.isNullOrBlank()) return
try {
@@ -577,6 +604,7 @@ open class BaseMediaService : MediaLibraryService() {
// Only try HEAD request (lightweight) - skip GET fallback as it's unreliable
connection.requestMethod = "HEAD"
connection.setRequestProperty("Icy-MetaData", "1")
+ connection.setRequestProperty("User-Agent", "Rollynn/1.0")
connection.setRequestProperty("User-Agent", "Elzify/1.0")
connection.connectTimeout = 3000 // Reduced timeout
connection.readTimeout = 3000
@@ -621,25 +649,25 @@ open class BaseMediaService : MediaLibraryService() {
val currentIndex = player.currentMediaItemIndex
if (currentIndex == C.INDEX_UNSET) return@post
- val currentExtras = currentItemNow.mediaMetadata.extras
- if (currentExtras?.getString("type") != Constants.MEDIA_TYPE_RADIO) return@post
+ val currentExtras = currentItemNow.mediaMetadata.extras ?: return@post
+ if (currentExtras.getString("type") != Constants.MEDIA_TYPE_RADIO) return@post
// Double-check we still don't have embedded metadata (might have arrived since check)
val hasEmbeddedMetadata = !currentItemNow.mediaMetadata.artist.isNullOrBlank() ||
!currentItemNow.mediaMetadata.title.isNullOrBlank() ||
- (currentExtras != null && !currentExtras.getString("radioArtist").isNullOrBlank()) ||
- (currentExtras != null && !currentExtras.getString("radioTitle").isNullOrBlank())
+ !currentExtras.getString("radioArtist").isNullOrBlank() ||
+ !currentExtras.getString("radioTitle").isNullOrBlank()
if (hasEmbeddedMetadata) return@post
val metadataBuilder = currentItemNow.mediaMetadata.buildUpon()
- val newExtras = Bundle(currentExtras ?: Bundle())
+ val newExtras = Bundle(currentExtras)
// Store individual values in extras for UI
artist?.let { newExtras.putString("radioArtist", it) }
title?.let { newExtras.putString("radioTitle", it) }
// Get station name (preserve if already set)
- val stationName = currentExtras?.getString("stationName")
+ val stationName = currentExtras.getString("stationName")
?: currentItemNow.mediaMetadata.title?.toString()
?: ""
if (stationName.isNotBlank()) {
@@ -831,19 +859,42 @@ open class BaseMediaService : MediaLibraryService() {
private inner class CustomNetworkCallback : ConnectivityManager.NetworkCallback() {
var wasWifi = false
+ var wasConnected = false
init {
val manager = getSystemService(ConnectivityManager::class.java)
val network = manager.activeNetwork
val capabilities = manager.getNetworkCapabilities(network)
- if (capabilities != null)
+ if (capabilities != null) {
wasWifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
+ }
+ updateConnectivityState(capabilities)
+ }
+
+ private fun isOnline(capabilities: NetworkCapabilities?): Boolean {
+ return capabilities != null
+ && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+ }
+
+ private fun updateConnectivityState(capabilities: NetworkCapabilities?) {
+ val online = isOnline(capabilities)
+ if (online && !wasConnected) {
+ MediaManager.submitPendingScrobbles()
+ }
+ wasConnected = online
+ }
+
+ override fun onAvailable(network: Network) {
+ val manager = getSystemService(ConnectivityManager::class.java)
+ updateConnectivityState(manager.getNetworkCapabilities(network))
}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
+ updateConnectivityState(networkCapabilities)
val isWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
if (isWifi != wasWifi) {
wasWifi = isWifi
@@ -852,6 +903,12 @@ open class BaseMediaService : MediaLibraryService() {
}
}
}
+
+ override fun onLost(network: Network) {
+ val manager = getSystemService(ConnectivityManager::class.java)
+ val activeCapabilities = manager.getNetworkCapabilities(manager.activeNetwork)
+ updateConnectivityState(activeCapabilities)
+ }
}
inner class LocalBinder : Binder() {
@@ -863,4 +920,3 @@ open class BaseMediaService : MediaLibraryService() {
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L
private const val RADIO_HEADER_CHECK_INTERVAL_SECONDS = 30L // Reduced frequency - only fallback when ICY fails
-
diff --git a/app/src/main/java/com/elzify/music/service/MediaManager.java b/app/src/main/java/com/elzify/music/service/MediaManager.java
index 3b66f23b..b9274cb2 100644
--- a/app/src/main/java/com/elzify/music/service/MediaManager.java
+++ b/app/src/main/java/com/elzify/music/service/MediaManager.java
@@ -8,6 +8,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Player;
@@ -32,18 +33,65 @@
import com.google.common.util.concurrent.MoreExecutors;
import java.lang.ref.WeakReference;
+import java.util.Date;
import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
public class MediaManager {
private static final String TAG = "MediaManager";
private static WeakReference attachedBrowserRef = new WeakReference<>(null);
public static AtomicBoolean justStarted = new AtomicBoolean(false);
+ private static int lastPlayNextInsertedIndex = -1;
private static final ExecutorService backgroundExecutor = Executors.newSingleThreadExecutor();
+ private static final MutableLiveData scrobbledSongId = new MutableLiveData<>();
+ private static final AtomicLong scrobbleVersion = new AtomicLong(0);
+ private static final Map playCountIncrements = new ConcurrentHashMap<>();
+
+ public static LiveData getScrobbledSongId() {
+ return scrobbledSongId;
+ }
+
+ public static long getScrobbleVersion() {
+ return scrobbleVersion.get();
+ }
+
+ public static int getPlayCountIncrement(String songId) {
+ if (songId == null) return 0;
+ return playCountIncrements.getOrDefault(songId, 0);
+ }
+
+ public static void postScrobbleEvent(String songId) {
+ scrobbleVersion.incrementAndGet();
+ playCountIncrements.merge(songId, 1, Integer::sum);
+ scrobbledSongId.postValue(songId);
+ }
+
+ private static final MutableLiveData