mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-03 03:17:21 +00:00
fix: Wavelog sync via v1 API logbook_get_worked_grids (3.0.2 has no API v2)
This commit is contained in:
1 parent
eb1a5d51a2
commit
13df0dfcec
4 files changed
+40
-36
No files matched your search
+35
-31
@@ -20,64 +20,68 @@ package com.rtbishop.look4sat.core.data.repository
|
|||||||
import com.rtbishop.look4sat.core.domain.repository.IWavelogRepository
|
import com.rtbishop.look4sat.core.domain.repository.IWavelogRepository
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the list of worked gridsquares from a self-hosted Wavelog instance.
|
* Fetches the list of worked gridsquares from a self-hosted Wavelog instance.
|
||||||
*
|
*
|
||||||
* Uses the Wavelog API v2 endpoint (Wavelog >= 3.1.0):
|
* Uses the Wavelog v1 API endpoint (verified against Wavelog 3.0.2):
|
||||||
* GET {base}/index.php/api/v2/lookup?grid=all
|
* POST {base}/index.php/api/logbook_get_worked_grids
|
||||||
* Authorization: Bearer wl2_...
|
* Body: {"key": "<api key>", "logbook_id": "<logbook id>"}
|
||||||
* Response: {"data": {"grids": ["JN47", "JO30"], "count": 2}, ...}
|
* Response 201: JSON array of 4-char gridsquares, e.g. ["JN47","JO30"]
|
||||||
|
* (see Wavelog Api.php logbook_get_worked_grids / Api_model::get_grids_worked_in_logbook)
|
||||||
*
|
*
|
||||||
* Uses a plain HttpURLConnection (bearer auth header) — IRemoteSource does not
|
* The token field carries "<api_key>:<logbook_id>" so one credential line
|
||||||
* support custom headers, and no feature module touches this class directly.
|
* configures both values (the dialog explains this to the user).
|
||||||
*/
|
*/
|
||||||
class WavelogRepository : IWavelogRepository {
|
class WavelogRepository : IWavelogRepository {
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch worked grids. Returns the grid set, or null on any failure
|
|
||||||
* (network error, non-200, malformed response, bad token).
|
|
||||||
*/
|
|
||||||
override suspend fun fetchWorkedGrids(url: String, token: String): Set<String>? = withContext(Dispatchers.IO) {
|
override suspend fun fetchWorkedGrids(url: String, token: String): Set<String>? = withContext(Dispatchers.IO) {
|
||||||
if (url.isBlank() || token.isBlank()) return@withContext null
|
val parts = token.trim().split(":")
|
||||||
|
val apiKey = parts.getOrNull(0)?.trim().orEmpty()
|
||||||
|
val logbookId = parts.getOrNull(1)?.trim().orEmpty()
|
||||||
|
if (url.isBlank() || apiKey.isBlank() || logbookId.isBlank()) return@withContext null
|
||||||
val base = url.trim().trimEnd('/')
|
val base = url.trim().trimEnd('/')
|
||||||
val requestUrl = "$base/index.php/api/v2/lookup?grid=all"
|
val requestUrl = "$base/index.php/api/logbook_get_worked_grids"
|
||||||
try {
|
try {
|
||||||
// IRemoteSource.getNetworkStream does not support custom headers, so
|
|
||||||
// run the bearer-token request directly here via java.net (simple GET,
|
|
||||||
// no OkHttp dependency added to this module's public API).
|
|
||||||
val connection = java.net.URL(requestUrl).openConnection() as java.net.HttpURLConnection
|
val connection = java.net.URL(requestUrl).openConnection() as java.net.HttpURLConnection
|
||||||
connectionTimeouts(connection)
|
connection.connectTimeout = 10_000
|
||||||
connection.setRequestProperty("Authorization", "Bearer ${token.trim()}")
|
connection.readTimeout = 15_000
|
||||||
connection.requestMethod = "GET"
|
connection.requestMethod = "POST"
|
||||||
|
connection.doOutput = true
|
||||||
|
connection.setRequestProperty("Content-Type", "application/json")
|
||||||
|
val body = JSONObject().apply {
|
||||||
|
put("key", apiKey)
|
||||||
|
put("logbook_id", logbookId)
|
||||||
|
}.toString().toByteArray()
|
||||||
|
connection.outputStream.use { it.write(body) }
|
||||||
val code = connection.responseCode
|
val code = connection.responseCode
|
||||||
if (code != 200) {
|
if (code !in 200..299) {
|
||||||
connection.disconnect()
|
connection.disconnect()
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
val body = connection.inputStream.bufferedReader().use { it.readText() }
|
val response = connection.inputStream.bufferedReader().use { it.readText() }
|
||||||
connection.disconnect()
|
connection.disconnect()
|
||||||
parseWorkedGrids(body)
|
parseWorkedGrids(response)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("WavelogRepository fetch failure: $e")
|
println("WavelogRepository fetch failure: $e")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun connectionTimeouts(connection: java.net.HttpURLConnection) {
|
|
||||||
connection.connectTimeout = 10_000
|
|
||||||
connection.readTimeout = 15_000
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun parseWorkedGrids(body: String): Set<String>? {
|
internal fun parseWorkedGrids(body: String): Set<String>? {
|
||||||
return try {
|
return try {
|
||||||
val obj = JSONObject(body)
|
val array = when {
|
||||||
val data = obj.optJSONObject("data") ?: return null
|
body.trimStart().startsWith("[") -> JSONArray(body)
|
||||||
val grids = data.optJSONArray("grids") ?: return null
|
else -> {
|
||||||
|
// Tolerate {"grids": [...]} wrappers too
|
||||||
|
JSONObject(body).optJSONArray("grids") ?: return null
|
||||||
|
}
|
||||||
|
}
|
||||||
val result = mutableSetOf<String>()
|
val result = mutableSetOf<String>()
|
||||||
for (i in 0 until grids.length()) {
|
for (i in 0 until array.length()) {
|
||||||
val grid = grids.optString(i, "").trim().uppercase()
|
val grid = array.optString(i, "").trim().uppercase()
|
||||||
// Keep only well-formed 4-character Maidenhead squares (e.g. OL62)
|
// Keep only well-formed 4-character Maidenhead squares (e.g. OL62)
|
||||||
if (grid.length == 4 && grid[0] in 'A'..'R' && grid[1] in 'A'..'R'
|
if (grid.length == 4 && grid[0] in 'A'..'R' && grid[1] in 'A'..'R'
|
||||||
&& grid[2] in '0'..'9' && grid[3] in '0'..'9'
|
&& grid[2] in '0'..'9' && grid[3] in '0'..'9'
|
||||||
|
|||||||
@@ -144,8 +144,8 @@
|
|||||||
<string name="prefs_wavelog_configured">已从 Wavelog 同步 — 通联 %1$d 个网格</string>
|
<string name="prefs_wavelog_configured">已从 Wavelog 同步 — 通联 %1$d 个网格</string>
|
||||||
<string name="prefs_wavelog_not_configured">未配置 — 连接你的 Wavelog 日志以在地图上高亮已通联网格</string>
|
<string name="prefs_wavelog_not_configured">未配置 — 连接你的 Wavelog 日志以在地图上高亮已通联网格</string>
|
||||||
<string name="prefs_wavelog_url">Wavelog 地址</string>
|
<string name="prefs_wavelog_url">Wavelog 地址</string>
|
||||||
<string name="prefs_wavelog_token">API 令牌</string>
|
<string name="prefs_wavelog_token">API key :日志 ID</string>
|
||||||
<string name="prefs_wavelog_hint">已同步网格数:%1$d。请先保存,再点同步。</string>
|
<string name="prefs_wavelog_hint">已同步网格数:%1$d。格式:API key:日志ID。请先保存,再点同步。</string>
|
||||||
<string name="prefs_wavelog_sync">立即同步</string>
|
<string name="prefs_wavelog_sync">立即同步</string>
|
||||||
<string name="prefs_wavelog_syncing">同步中…</string>
|
<string name="prefs_wavelog_syncing">同步中…</string>
|
||||||
<string name="prefs_locator_text">使用梅登黑德网格设置站点位置</string>
|
<string name="prefs_locator_text">使用梅登黑德网格设置站点位置</string>
|
||||||
|
|||||||
@@ -179,8 +179,8 @@
|
|||||||
<string name="prefs_wavelog_configured">Synced from Wavelog — %1$d grids worked</string>
|
<string name="prefs_wavelog_configured">Synced from Wavelog — %1$d grids worked</string>
|
||||||
<string name="prefs_wavelog_not_configured">Not configured — connect your Wavelog logbook to highlight worked grids on the map</string>
|
<string name="prefs_wavelog_not_configured">Not configured — connect your Wavelog logbook to highlight worked grids on the map</string>
|
||||||
<string name="prefs_wavelog_url">Wavelog URL</string>
|
<string name="prefs_wavelog_url">Wavelog URL</string>
|
||||||
<string name="prefs_wavelog_token">API token</string>
|
<string name="prefs_wavelog_token">API key : logbook ID</string>
|
||||||
<string name="prefs_wavelog_hint">Worked grids synced: %1$d. Save first, then tap Sync.</string>
|
<string name="prefs_wavelog_hint">Worked grids synced: %1$d. Format: API key, colon, logbook ID. Save first, then tap Sync.</string>
|
||||||
<string name="prefs_wavelog_sync">Sync now</string>
|
<string name="prefs_wavelog_sync">Sync now</string>
|
||||||
<string name="prefs_wavelog_syncing">Syncing…</string>
|
<string name="prefs_wavelog_syncing">Syncing…</string>
|
||||||
<string name="prefs_locator_text">Set station\'s position using locator</string>
|
<string name="prefs_locator_text">Set station\'s position using locator</string>
|
||||||
|
|||||||
+1
-1
@@ -188,7 +188,7 @@ fun WavelogDialog(
|
|||||||
value = token.value,
|
value = token.value,
|
||||||
onValueChange = { token.value = it },
|
onValueChange = { token.value = it },
|
||||||
label = { Text(text = stringResource(id = R.string.prefs_wavelog_token)) },
|
label = { Text(text = stringResource(id = R.string.prefs_wavelog_token)) },
|
||||||
placeholder = { Text(text = "wl2_...") },
|
placeholder = { Text(text = "abcdef123456:1") },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = LocalSpacing.current.large),
|
modifier = Modifier.fillMaxWidth().padding(horizontal = LocalSpacing.current.large),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in new issue
Block a user