initial db rewrite

This commit is contained in:
Boof2015
2026-07-24 02:18:37 -04:00
parent 26fba836f4
commit a10ecf4a0f
90 changed files with 12669 additions and 5173 deletions
+3
View File
@@ -19,5 +19,8 @@ android {
dependencies {
implementation "androidx.media:media:1.6.0"
implementation "androidx.room:room-runtime:2.8.4"
implementation "androidx.room:room-ktx:2.8.4"
implementation "com.facebook.react:react-android"
implementation project(':astra-library-scanner')
}
@@ -9,13 +9,8 @@ import android.os.ParcelFileDescriptor
import android.util.Log
import java.io.File
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.security.MessageDigest
private const val TAG = "AstraCarArt"
private const val DOWNLOAD_TIMEOUT_MS = 8_000
/**
* Serves artwork to Android Auto as `content://` URIs (the only scheme Auto accepts).
@@ -59,58 +54,12 @@ class AstraCarArtworkProvider : ContentProvider() {
}
private fun remoteFile(ctx: Context, sourceId: Long?, artworkSourceId: String?): File? {
if (sourceId == null || artworkSourceId.isNullOrBlank()) return null
val cacheDir = File(ctx.cacheDir, "astra-car-art").apply { mkdirs() }
val cacheFile = File(cacheDir, sha1("$sourceId:$artworkSourceId") + ".img")
if (cacheFile.exists() && cacheFile.length() > 0) return cacheFile
// art_auth is a full cover-art URL template with an id placeholder, built by JS from
// the (well-tested) Subsonic/Jellyfin URL builders so the provider stays auth-agnostic.
val template = artAuthTemplate(ctx, sourceId) ?: return null
val url = template.replace(AstraCarArtwork.ART_ID_PLACEHOLDER, Uri.encode(artworkSourceId))
return runCatching {
download(url, cacheFile)
cacheFile.takeIf { it.length() > 0 }
}.onFailure { Log.w(TAG, "remote art download failed for $sourceId/$artworkSourceId", it) }
.getOrNull()
// Credential-bearing remote artwork URLs live only in SecureStore. Remote
// covers are fetched by the app and become available here once cached as
// normal local artwork; the provider never reads or persists credentials.
return null
}
private fun artAuthTemplate(ctx: Context, sourceId: Long): String? =
AstraCarDb.openReadable(ctx)?.use { db ->
db.rawQuery(
"SELECT art_auth FROM remote_sources WHERE id = ? LIMIT 1",
arrayOf(sourceId.toString()),
).use { cursor ->
if (cursor.moveToFirst() && !cursor.isNull(0)) cursor.getString(0) else null
}
}?.takeIf { it.isNotBlank() }
private fun download(url: String, dest: File) {
val connection = (URL(url).openConnection() as HttpURLConnection).apply {
connectTimeout = DOWNLOAD_TIMEOUT_MS
readTimeout = DOWNLOAD_TIMEOUT_MS
instanceFollowRedirects = true
}
try {
if (connection.responseCode !in 200..299) {
throw FileNotFoundException("HTTP ${connection.responseCode}")
}
val tmp = File(dest.absolutePath + ".tmp")
connection.inputStream.use { input -> FileOutputStream(tmp).use(input::copyTo) }
if (!tmp.renameTo(dest)) {
tmp.copyTo(dest, overwrite = true)
tmp.delete()
}
} finally {
connection.disconnect()
}
}
private fun sha1(value: String): String =
MessageDigest.getInstance("SHA-1")
.digest(value.toByteArray())
.joinToString("") { "%02x".format(it) }
override fun getType(uri: Uri): String = "image/*"
override fun query(
File diff suppressed because it is too large Load Diff
@@ -1,30 +0,0 @@
package expo.modules.astracar
import android.content.Context
import android.database.sqlite.SQLiteDatabase
/**
* Shared read access to the op-sqlite library database (`astra-library.db`), used by
* both the browse catalog and the artwork ContentProvider. op-sqlite stores the file at
* [Context.getDatabasePath] (the same dir we read here).
*/
object AstraCarDb {
const val DB_NAME = "astra-library.db"
/**
* Opens the library DB for reading. The JS side runs it in WAL mode
* (`PRAGMA journal_mode = WAL`), and a strict `OPEN_READONLY` open of a live WAL
* database can throw ("could not open in read/write mode" — read-only WAL needs a
* writable `-shm`/`-wal`). Try read-only first, then fall back to read/write (we only
* ever run `SELECT`s). Returns null if the DB is missing or can't be opened.
*/
fun openReadable(context: Context): SQLiteDatabase? {
val file = context.getDatabasePath(DB_NAME)
if (!file.exists()) return null
return runCatching {
SQLiteDatabase.openDatabase(file.absolutePath, null, SQLiteDatabase.OPEN_READONLY)
}.recoverCatching {
SQLiteDatabase.openDatabase(file.absolutePath, null, SQLiteDatabase.OPEN_READWRITE)
}.getOrNull()
}
}