mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-08-05 02:25:45 +02:00
Add AbsLogger plugin, persist logs to db, logs page for android
This commit is contained in:
parent
b9e3ccd0c1
commit
390388fe83
10 changed files with 208 additions and 10 deletions
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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))))
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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
60
pages/logs.vue
Normal 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>
|
42
plugins/capacitor/AbsLogger.js
Normal file
42
plugins/capacitor/AbsLogger.js
Normal 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 }
|
|
@ -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 }
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
"ButtonLatest": "Latest",
|
||||
"ButtonLibrary": "Library",
|
||||
"ButtonLocalMedia": "Local Media",
|
||||
"ButtonLogs": "Logs",
|
||||
"ButtonManageLocalFiles": "Manage Local Files",
|
||||
"ButtonNewFolder": "New Folder",
|
||||
"ButtonNextEpisode": "Next Episode",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue