Add AbsLogger plugin, persist logs to db, logs page for android

This commit is contained in:
advplyr 2025-04-19 17:26:32 -05:00
parent b9e3ccd0c1
commit 390388fe83
10 changed files with 208 additions and 10 deletions

View file

@ -18,6 +18,7 @@ import com.audiobookshelf.app.plugins.AbsAudioPlayer
import com.audiobookshelf.app.plugins.AbsDatabase
import com.audiobookshelf.app.plugins.AbsDownloader
import com.audiobookshelf.app.plugins.AbsFileSystem
import com.audiobookshelf.app.plugins.AbsLogger
import com.getcapacitor.BridgeActivity
@ -57,6 +58,7 @@ class MainActivity : BridgeActivity() {
registerPlugin(AbsDownloader::class.java)
registerPlugin(AbsFileSystem::class.java)
registerPlugin(AbsDatabase::class.java)
registerPlugin(AbsLogger::class.java)
super.onCreate(savedInstanceState)
Log.d(tag, "onCreate")

View file

@ -4,6 +4,7 @@ import android.content.Context
import android.util.Log
import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.models.DownloadItem
import com.audiobookshelf.app.plugins.AbsLog
import io.paperdb.Paper
import java.io.File
@ -287,4 +288,17 @@ class DbManager {
}
return sessions
}
fun saveLog(log:AbsLog) {
Paper.book("log").write(log.id, log)
}
fun getAllLogs() : List<AbsLog> {
val logs:MutableList<AbsLog> = mutableListOf()
Paper.book("log").allKeys.forEach { logId ->
Paper.book("log").read<AbsLog>(logId)?.let {
logs.add(it)
}
}
return logs
}
}

View file

@ -219,6 +219,7 @@ class AbsDatabase : Plugin() {
@PluginMethod
fun syncLocalSessionsWithServer(call:PluginCall) {
AbsLogger.info("[AbsDatabase] syncLocalSessionsWithServer")
if (DeviceManager.serverConnectionConfig == null) {
Log.e(tag, "syncLocalSessionsWithServer not connected to server")
return call.resolve()
@ -226,6 +227,7 @@ class AbsDatabase : Plugin() {
apiHandler.syncLocalMediaProgressForUser {
Log.d(tag, "Finished syncing local media progress for user")
AbsLogger.info("[AbsDatabase] Finished syncing local media progress for user")
val savedSessions = DeviceManager.dbManager.getPlaybackSessions().filter { it.serverConnectionConfigId == DeviceManager.serverConnectionConfigId }
if (savedSessions.isNotEmpty()) {

View file

@ -0,0 +1,61 @@
package com.audiobookshelf.app.plugins
import android.util.Log
import com.audiobookshelf.app.device.DeviceManager
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.getcapacitor.JSObject
import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin
import java.util.UUID
data class AbsLog(
var id:String,
var level:String,
var message:String,
var timestamp:Long
)
data class AbsLogList(val value:List<AbsLog>)
@CapacitorPlugin(name = "AbsLogger")
class AbsLogger : Plugin() {
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
override fun load() {
Log.i("AbsLogger", "Initialize AbsLogger plugin")
}
companion object {
fun info(message:String) {
Log.i("AbsLogger", message)
DeviceManager.dbManager.saveLog(AbsLog(id = UUID.randomUUID().toString(), level = "info", message, timestamp = System.currentTimeMillis()))
}
fun error(message:String) {
Log.e("AbsLogger", message)
DeviceManager.dbManager.saveLog(AbsLog(id = UUID.randomUUID().toString(), level = "error", message, timestamp = System.currentTimeMillis()))
}
}
@PluginMethod
fun info(call: PluginCall) {
val msg = call.getString("message") ?: return call.reject("No message")
info(msg)
call.resolve()
}
@PluginMethod
fun error(call: PluginCall) {
val msg = call.getString("message") ?: return call.reject("No message")
error(msg)
call.resolve()
}
@PluginMethod
fun getAllLogs(call: PluginCall) {
val absLogs = DeviceManager.dbManager.getAllLogs()
call.resolve(JSObject(jacksonMapper.writeValueAsString(AbsLogList(absLogs))))
}
}

View file

@ -134,6 +134,15 @@ export default {
to: '/settings'
})
if (this.$platform !== 'ios') {
items.push({
icon: 'bug_report',
iconOutlined: true,
text: this.$strings.ButtonLogs,
to: '/logs'
})
}
if (this.serverConnectionConfig) {
items.push({
icon: 'language',

View file

@ -16,6 +16,7 @@
<script>
import { CapacitorHttp } from '@capacitor/core'
import { AbsLogger } from '@/plugins/capacitor'
export default {
data() {
@ -72,6 +73,9 @@ export default {
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
},
currentLibraryName() {
return this.$store.getters['libraries/getCurrentLibraryName']
},
attemptingConnection: {
get() {
return this.$store.state.attemptingConnection
@ -114,6 +118,7 @@ export default {
console.warn('[default] attemptConnection')
if (!this.networkConnected) {
console.warn('[default] No network connection')
AbsLogger.info({ message: '[default] attemptConnection: No network connection' })
return
}
if (this.attemptingConnection) {
@ -134,13 +139,14 @@ export default {
if (!serverConfig) {
// No last server config set
this.attemptingConnection = false
AbsLogger.info({ message: `[default] attemptConnection: No last server config set` })
return
}
console.log(`[default] Got server config, attempt authorize ${serverConfig.address}`)
AbsLogger.info({ message: `[default] attemptConnection: Got server config, attempt authorize (${serverConfig.name})` })
const authRes = await this.postRequest(`${serverConfig.address}/api/authorize`, null, { Authorization: `Bearer ${serverConfig.token}` }, 6000).catch((error) => {
console.error('[default] Server auth failed', error)
AbsLogger.error({ message: `[default] attemptConnection: Server auth failed (${serverConfig.name})` })
return false
})
if (!authRes) {
@ -166,7 +172,7 @@ export default {
this.$socket.connect(serverConnectionConfig.address, serverConnectionConfig.token)
console.log('[default] Successful connection on last saved connection config', JSON.stringify(serverConnectionConfig))
AbsLogger.info({ message: `[default] attemptConnection: Successful connection to last saved server config (${serverConnectionConfig.name})` })
await this.initLibraries()
this.attemptingConnection = false
},
@ -186,7 +192,8 @@ export default {
}
this.inittingLibraries = true
await this.$store.dispatch('libraries/load')
console.log(`[default] initLibraries loaded ${this.currentLibraryId}`)
AbsLogger.info({ message: `[default] initLibraries loading library ${this.currentLibraryName}` })
await this.$store.dispatch('libraries/fetch', this.currentLibraryId)
this.$eventBus.$emit('library-changed')
this.inittingLibraries = false
@ -197,7 +204,7 @@ export default {
return
}
console.log('[default] Calling syncLocalSessions')
AbsLogger.info({ message: '[default] Calling syncLocalSessions' })
const response = await this.$db.syncLocalSessionsWithServer(isFirstSync)
if (response?.error) {
console.error('[default] Failed to sync local sessions', response.error)
@ -310,6 +317,7 @@ export default {
this.$socket.on('user_media_progress_updated', this.userMediaProgressUpdated)
if (this.$store.state.isFirstLoad) {
AbsLogger.info({ message: `[default] mounted: first load` })
this.$store.commit('setIsFirstLoad', false)
this.loadSavedSettings()
@ -322,8 +330,10 @@ export default {
await this.$store.dispatch('setupNetworkListener')
if (this.$store.state.user.serverConnectionConfig) {
AbsLogger.info({ message: `[default] Server connected, init libraries (ServerConfigName: ${this.$store.getters['user/getServerConfigName']})` })
await this.initLibraries()
} else {
AbsLogger.info({ message: `[default] Server not connected, attempt connection` })
await this.attemptConnection()
}

60
pages/logs.vue Normal file
View file

@ -0,0 +1,60 @@
<template>
<div class="w-full h-full p-4">
<div class="flex items-center justify-between mb-2">
<p class="text-lg font-bold">{{ $strings.ButtonLogs }}</p>
<ui-icon-btn outlined borderless icon="content_copy" @click="copyToClipboard" />
</div>
<div class="w-full h-[calc(100%-40px)] overflow-y-auto relative" ref="logContainer">
<div v-if="hasScrolled" class="sticky top-0 left-0 w-full h-10 bg-gradient-to-t from-transparent to-bg z-10 pointer-events-none"></div>
<div v-for="log in logs" :key="log.id" class="py-1">
<div class="flex items-center space-x-4 mb-1">
<div class="text-xs uppercase font-bold" :class="{ 'text-error': log.level === 'error', 'text-blue-600': log.level === 'info' }">{{ log.level }}</div>
<div class="text-xs text-gray-400">{{ new Date(log.timestamp).toLocaleString() }}</div>
</div>
<div class="text-xs">{{ log.message }}</div>
</div>
</div>
</div>
</template>
<script>
import { AbsLogger } from '@/plugins/capacitor'
export default {
data() {
return {
logs: [],
hasScrolled: false
}
},
computed: {},
methods: {
copyToClipboard() {
this.$copyToClipboard(
this.logs
.map((log) => {
return `${log.timestamp} [${log.level}] ${log.message}`
})
.join('\n')
)
},
scrollToBottom() {
this.$refs.logContainer.scrollTop = this.$refs.logContainer.scrollHeight
this.hasScrolled = this.$refs.logContainer.scrollTop > 0
},
loadLogs() {
AbsLogger.getAllLogs().then((logData) => {
const logs = logData.value || []
this.logs = logs
this.$nextTick(() => {
this.scrollToBottom()
})
})
}
},
mounted() {
this.loadLogs()
}
}
</script>

View file

@ -0,0 +1,42 @@
import { registerPlugin, WebPlugin } from '@capacitor/core'
class AbsLoggerWeb extends WebPlugin {
constructor() {
super()
this.logs = []
}
saveLog(level, message) {
this.logs.push({
id: Math.random().toString(36).substring(2, 15),
timestamp: Date.now(),
level: level,
message: message
})
}
async info(data) {
if (data?.message) {
this.saveLog('info', data.message)
console.log('AbsLogger: info', data.message)
}
}
async error(data) {
if (data?.message) {
this.saveLog('error', data.message)
console.error('AbsLogger: error', data.message)
}
}
async getAllLogs() {
return this.logs
}
}
const AbsLogger = registerPlugin('AbsLogger', {
web: () => new AbsLoggerWeb()
})
export { AbsLogger }

View file

@ -2,12 +2,9 @@ import Vue from 'vue'
import { AbsAudioPlayer } from './AbsAudioPlayer'
import { AbsDownloader } from './AbsDownloader'
import { AbsFileSystem } from './AbsFileSystem'
import { AbsLogger } from './AbsLogger'
import { Capacitor } from '@capacitor/core'
Vue.prototype.$platform = Capacitor.getPlatform()
export {
AbsAudioPlayer,
AbsDownloader,
AbsFileSystem
}
export { AbsAudioPlayer, AbsDownloader, AbsFileSystem, AbsLogger }

View file

@ -26,6 +26,7 @@
"ButtonLatest": "Latest",
"ButtonLibrary": "Library",
"ButtonLocalMedia": "Local Media",
"ButtonLogs": "Logs",
"ButtonManageLocalFiles": "Manage Local Files",
"ButtonNewFolder": "New Folder",
"ButtonNextEpisode": "Next Episode",