Add Timber file logging and Jellyfin log upload

This commit is contained in:
2026-05-14 18:06:54 +02:00
parent f95fbf72b3
commit 4b87f93dee
21 changed files with 430 additions and 100 deletions

View File

@@ -1,6 +1,5 @@
package hu.bbara.purefin.data.catalog
import android.util.Log
import androidx.datastore.core.DataStore
import hu.bbara.purefin.core.data.HomeRepository
import hu.bbara.purefin.core.data.NetworkMonitor
@@ -41,6 +40,7 @@ import javax.inject.Inject
import javax.inject.Singleton
import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import timber.log.Timber
@Singleton
class InMemoryAppContentRepository @Inject constructor(
@@ -82,7 +82,7 @@ class InMemoryAppContentRepository @Inject constructor(
if (!initialized.compareAndSet(expectedValue = false, newValue = true)) {
return
}
Log.d(TAG, "Initializing home repository")
Timber.tag(TAG).d("Initializing home repository")
scope.launch { loadHomeCache() }
scope.launch { refreshHomeData() }
}
@@ -91,7 +91,7 @@ class InMemoryAppContentRepository @Inject constructor(
val job = synchronized(this) {
refreshJob?.takeIf { it.isActive } ?: scope.launch {
runCatching {
Log.d(TAG, "Refreshing home data")
Timber.tag(TAG).d("Refreshing home data")
if (!networkMonitor.isOnline.first()) {
return@runCatching
}
@@ -100,10 +100,10 @@ class InMemoryAppContentRepository @Inject constructor(
loadContinueWatching()
loadNextUp()
loadLatestLibraryContent()
Log.d(TAG, "Home refresh successful")
Timber.tag(TAG).d("Home refresh successful")
persistHomeCache()
}.onFailure { error ->
Log.w(TAG, "Home refresh failed; keeping cached content", error)
Timber.tag(TAG).w(error, "Home refresh failed; keeping cached content")
}
}.also { refreshJob = it }
}
@@ -111,7 +111,7 @@ class InMemoryAppContentRepository @Inject constructor(
}
private suspend fun loadHomeCache() {
Log.d(TAG, "Loading home cache")
Timber.tag(TAG).d("Loading home cache")
val cache = homeCacheDataStore.data.first()
if (cache.libraries.isNotEmpty()) {
val libraries = cache.libraries.mapNotNull { it.toLibrary() }
@@ -147,7 +147,7 @@ class InMemoryAppContentRepository @Inject constructor(
if (cache.episodes.isNotEmpty()) {
onlineMediaRepository.upsertEpisodes(cache.episodes.mapNotNull { it.toEpisode() })
}
Log.d(TAG, "Home cache loaded")
Timber.tag(TAG).d("Home cache loaded")
}
private suspend fun persistHomeCache() {
@@ -200,7 +200,7 @@ class InMemoryAppContentRepository @Inject constructor(
private suspend fun loadLibraries() {
val librariesItem = runCatching { jellyfinApiClient.getLibraries() }
.getOrElse { error ->
Log.w(TAG, "Unable to load libraries", error)
Timber.tag(TAG).w(error, "Unable to load libraries")
return
}
val filteredLibraries = librariesItem.filter {
@@ -222,7 +222,7 @@ class InMemoryAppContentRepository @Inject constructor(
private suspend fun loadLibrary(library: Library): Library {
val contentItem = runCatching { jellyfinApiClient.getLibraryContent(library.id) }
.getOrElse { error ->
Log.w(TAG, "Unable to load library ${library.id}", error)
Timber.tag(TAG).w(error, "Unable to load library ${library.id}")
return library
}
return when (library.type) {
@@ -240,7 +240,7 @@ class InMemoryAppContentRepository @Inject constructor(
private suspend fun loadSuggestions() {
val suggestionsItems = runCatching { jellyfinApiClient.getSuggestions() }
.getOrElse { error ->
Log.w(TAG, "Unable to load suggestions", error)
Timber.tag(TAG).w(error, "Unable to load suggestions")
return
}
suggestionsState.value = suggestionsItems.mapNotNull { item ->
@@ -261,7 +261,7 @@ class InMemoryAppContentRepository @Inject constructor(
private suspend fun loadContinueWatching() {
val continueWatchingItems = runCatching { jellyfinApiClient.getContinueWatching() }
.getOrElse { error ->
Log.w(TAG, "Unable to load continue watching", error)
Timber.tag(TAG).w(error, "Unable to load continue watching")
return
}
continueWatchingState.value = continueWatchingItems.mapNotNull { item ->
@@ -282,7 +282,7 @@ class InMemoryAppContentRepository @Inject constructor(
private suspend fun loadNextUp() {
val nextUpItems = runCatching { jellyfinApiClient.getNextUpEpisodes() }
.getOrElse { error ->
Log.w(TAG, "Unable to load next up", error)
Timber.tag(TAG).w(error, "Unable to load next up")
return
}
nextUpState.value = nextUpItems.map { item ->
@@ -297,7 +297,7 @@ class InMemoryAppContentRepository @Inject constructor(
private suspend fun loadLatestLibraryContent() {
val librariesItem = runCatching { jellyfinApiClient.getLibraries() }
.getOrElse { error ->
Log.w(TAG, "Unable to load latest library content", error)
Timber.tag(TAG).w(error, "Unable to load latest library content")
return
}
val filteredLibraries = librariesItem.filter {
@@ -306,7 +306,7 @@ class InMemoryAppContentRepository @Inject constructor(
val latestLibraryContents = filteredLibraries.associate { library ->
val latestFromLibrary = runCatching { jellyfinApiClient.getLatestFromLibrary(library.id) }
.getOrElse { error ->
Log.w(TAG, "Unable to load latest items for library ${library.id}", error)
Timber.tag(TAG).w(error, "Unable to load latest items for library ${library.id}")
emptyList()
}
library.id to when (library.collectionType) {

View File

@@ -1,6 +1,5 @@
package hu.bbara.purefin.data.catalog
import android.util.Log
import hu.bbara.purefin.core.data.LocalMediaRepository
import hu.bbara.purefin.core.data.UserSessionRepository
import hu.bbara.purefin.data.converter.toEpisode
@@ -25,6 +24,7 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import org.jellyfin.sdk.model.api.BaseItemKind
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
@@ -52,7 +52,7 @@ class InMemoryLocalMediaRepository @Inject constructor(
if (!moviesState.value.containsKey(id)) {
jellyfinApiClient.getItemInfo(id)?.let { item ->
if (item.type != BaseItemKind.MOVIE) {
Log.d("InMemoryMediaRepository", "Item is not an movie: ${item.type}")
Timber.tag(TAG).d("Item is not an movie: ${item.type}")
return flowOf(null)
}
val movie = item.toMovie(serverUrl.first())
@@ -66,7 +66,7 @@ class InMemoryLocalMediaRepository @Inject constructor(
if (!seriesState.value.containsKey(id)) {
jellyfinApiClient.getItemInfo(id)?.let { item ->
if (item.type != BaseItemKind.SERIES) {
Log.d("InMemoryMediaRepository", "Item is not an series: ${item.type}")
Timber.tag(TAG).d("Item is not an series: ${item.type}")
return flowOf(null)
}
val series = item.toSeries(serverUrl.first())
@@ -80,7 +80,7 @@ class InMemoryLocalMediaRepository @Inject constructor(
if (!episodesState.value.containsKey(id)) {
jellyfinApiClient.getItemInfo(id)?.let { item ->
if (item.type != BaseItemKind.EPISODE) {
Log.d("InMemoryMediaRepository", "Item is not an episode: ${item.type}")
Timber.tag(TAG).d("Item is not an episode: ${item.type}")
return flowOf(null)
}
val episode = item.toEpisode(serverUrl.first())
@@ -110,7 +110,7 @@ class InMemoryLocalMediaRepository @Inject constructor(
} catch (error: CancellationException) {
throw error
} catch (error: Exception) {
Log.e("InMemoryMediaRepository", "Failed to load content for series $seriesId", error)
Timber.tag(TAG).e(error, "Failed to load content for series $seriesId")
throw error
}
}
@@ -121,7 +121,7 @@ class InMemoryLocalMediaRepository @Inject constructor(
} catch (error: CancellationException) {
throw error
} catch (error: Exception) {
Log.w("InMemoryMediaRepository", "Unable to preload seasons for episode series $seriesId", error)
Timber.tag(TAG).w(error, "Unable to preload seasons for episode series $seriesId")
}
}
@@ -231,4 +231,8 @@ class InMemoryLocalMediaRepository @Inject constructor(
}
}
}
private companion object {
const val TAG = "InMemoryMediaRepository"
}
}

View File

@@ -1,6 +1,5 @@
package hu.bbara.purefin.data.jellyfin
import android.util.Log
import androidx.annotation.OptIn
import androidx.core.net.toUri
import androidx.media3.common.MediaItem
@@ -36,6 +35,7 @@ import org.jellyfin.sdk.model.api.MediaSegmentType.OUTRO
import org.jellyfin.sdk.model.api.MediaSegmentType.PREVIEW
import org.jellyfin.sdk.model.api.MediaSegmentType.RECAP
import org.jellyfin.sdk.model.api.MediaSourceInfo
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
@@ -72,7 +72,7 @@ class DefaultPlayableMediaRepository @Inject constructor(
?: getOfflineDownloadedPlayableMedia(mediaId, downloadedMediaItem)
}.getOrElse { error ->
if (error is CancellationException) throw error
Log.w("PlayableMediaRepo", "Unable to load online metadata for downloaded media $mediaId", error)
Timber.tag(TAG).w(error, "Unable to load online metadata for downloaded media $mediaId")
getOfflineDownloadedPlayableMedia(mediaId, downloadedMediaItem)
}
}
@@ -232,7 +232,7 @@ class DefaultPlayableMediaRepository @Inject constructor(
getPlayableMedia(id)
}
}.getOrElse { error ->
Log.w("PlayableMediaRepo", "Unable to load next-up items for $episodeId", error)
Timber.tag(TAG).w(error, "Unable to load next-up items for $episodeId")
emptyList()
}
}
@@ -325,4 +325,8 @@ class DefaultPlayableMediaRepository @Inject constructor(
endMs = endTicks / 10_000L
)
}
private companion object {
const val TAG = "PlayableMediaRepo"
}
}

View File

@@ -2,7 +2,6 @@ package hu.bbara.purefin.data.jellyfin.client
import android.content.Context
import android.os.SystemClock
import android.util.Log
import dagger.hilt.android.qualifiers.ApplicationContext
import hu.bbara.purefin.core.data.PlaybackMethod
import hu.bbara.purefin.core.data.PlaybackReportContext
@@ -19,6 +18,7 @@ import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.Response
import org.jellyfin.sdk.api.client.extensions.authenticateWithQuickConnect
import org.jellyfin.sdk.api.client.extensions.authenticateUserByName
import org.jellyfin.sdk.api.client.extensions.clientLogApi
import org.jellyfin.sdk.api.client.extensions.genresApi
import org.jellyfin.sdk.api.client.extensions.itemsApi
import org.jellyfin.sdk.api.client.extensions.mediaInfoApi
@@ -57,6 +57,7 @@ import org.jellyfin.sdk.model.api.RepeatMode
import org.jellyfin.sdk.model.api.request.GetItemsRequest
import org.jellyfin.sdk.model.api.request.GetNextUpRequest
import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
@@ -194,7 +195,7 @@ class JellyfinApiClient @Inject constructor(
searchTerm = searchTerm,
recursive = true
)
Log.d("searchBySearchTerm", response.content.toString())
Timber.tag("searchBySearchTerm").d(response.content.toString())
response.content.items
}
}
@@ -210,7 +211,7 @@ class JellyfinApiClient @Inject constructor(
genres = genres,
recursive = true
)
Log.d("searchByGenre", response.content.toString())
Timber.tag("searchByGenre").d(response.content.toString())
response.content.items
}
}
@@ -225,7 +226,7 @@ class JellyfinApiClient @Inject constructor(
presetViews = listOf(CollectionType.MOVIES, CollectionType.TVSHOWS),
includeHidden = false,
)
Log.d("getLibraries", response.content.toString())
Timber.tag("getLibraries").d(response.content.toString())
response.content.items
}
}
@@ -245,7 +246,7 @@ class JellyfinApiClient @Inject constructor(
recursive = true,
)
val response = api.itemsApi.getItems(getItemsRequest)
Log.d("getLibraryContent", response.content.toString())
Timber.tag("getLibraryContent").d(response.content.toString())
response.content.items
}
}
@@ -263,7 +264,7 @@ class JellyfinApiClient @Inject constructor(
limit = 8,
enableTotalRecordCount = true,
)
Log.d("getSuggestions", response.content.toString())
Timber.tag("getSuggestions").d(response.content.toString())
response.content.items
}
}
@@ -282,7 +283,7 @@ class JellyfinApiClient @Inject constructor(
startIndex = 0,
)
val response: Response<BaseItemDtoQueryResult> = api.itemsApi.getResumeItems(getResumeItemsRequest)
Log.d("getContinueWatching", response.content.toString())
Timber.tag("getContinueWatching").d(response.content.toString())
response.content.items
}
}
@@ -298,7 +299,7 @@ class JellyfinApiClient @Inject constructor(
enableResumable = false,
)
val result = api.tvShowsApi.getNextUp(getNextUpRequest)
Log.d("getNextUpEpisodes", result.content.toString())
Timber.tag("getNextUpEpisodes").d(result.content.toString())
result.content.items
}
}
@@ -315,7 +316,7 @@ class JellyfinApiClient @Inject constructor(
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.EPISODE, BaseItemKind.SEASON),
limit = 10,
)
Log.d("getLatestFromLibrary", response.content.toString())
Timber.tag("getLatestFromLibrary").d(response.content.toString())
response.content
}
}
@@ -326,7 +327,7 @@ class JellyfinApiClient @Inject constructor(
return@logRequest null
}
val result = api.userLibraryApi.getItem(itemId = mediaId, userId = getUserId())
Log.d("getItemInfo", result.content.toString())
Timber.tag("getItemInfo").d(result.content.toString())
result.content
}
}
@@ -342,7 +343,7 @@ class JellyfinApiClient @Inject constructor(
fields = defaultItemFields,
enableUserData = true,
)
Log.d("getSeasons", result.content.toString())
Timber.tag("getSeasons").d(result.content.toString())
result.content.items
}
}
@@ -359,7 +360,7 @@ class JellyfinApiClient @Inject constructor(
fields = defaultItemFields + ItemFields.OVERVIEW,
enableUserData = true,
)
Log.d("getEpisodesInSeason", result.content.toString())
Timber.tag("getEpisodesInSeason").d(result.content.toString())
result.content.items
}
}
@@ -379,7 +380,7 @@ class JellyfinApiClient @Inject constructor(
limit = count,
)
val nextUpEpisodes = nextUpEpisodesResult.content.items
Log.d("getNextEpisodes", nextUpEpisodes.toString())
Timber.tag("getNextEpisodes").d(nextUpEpisodes.toString())
nextUpEpisodes
}
}
@@ -393,7 +394,7 @@ class JellyfinApiClient @Inject constructor(
userId = getUserId(),
parentId = id,
)
Log.d("getGenres", result.toString())
Timber.tag("getGenres").d(result.toString())
result.content.items
}
}
@@ -435,7 +436,7 @@ class JellyfinApiClient @Inject constructor(
maxStreamingBitrate = 100_000_000,
),
)
Log.d("getMediaSources", result.toString())
Timber.tag("getMediaSources").d(result.toString())
result.content.mediaSources
}
}
@@ -449,7 +450,7 @@ class JellyfinApiClient @Inject constructor(
itemId = mediaId,
//includeSegmentTypes = listOf(MediaSegmentType.INTRO)
)
Log.d("getMediaSegments", result.toString())
Timber.tag("getMediaSegments").d(result.toString())
result.content.items
}
}
@@ -496,10 +497,19 @@ class JellyfinApiClient @Inject constructor(
liveStreamId = liveStreamId,
)
} catch (error: Exception) {
Log.e(TAG, "getVideoStreamUrl($itemId) failed", error)
Timber.tag(TAG).e(error, "getVideoStreamUrl($itemId) failed")
throw error
}
suspend fun uploadLogFile(data: String): String? = withContext(Dispatchers.IO) {
logRequest("uploadLogFile") {
if (!ensureConfigured()) {
return@logRequest null
}
api.clientLogApi.logFile(data).content.fileName
}
}
suspend fun getPublicSystemInfoVersion(): String? = withContext(Dispatchers.IO) {
logRequest("getPublicSystemInfoVersion") {
if (!ensureConfigured()) {
@@ -589,8 +599,7 @@ class JellyfinApiClient @Inject constructor(
return try {
val result = block()
val elapsedMs = SystemClock.elapsedRealtime() - startedAt
Log.d(
TAG,
Timber.tag(TAG).d(
"$operation finished in ${elapsedMs}ms, " +
"fetched ${result.approximateSizeBytes()} bytes${result.itemCountLogText()}"
)
@@ -599,7 +608,7 @@ class JellyfinApiClient @Inject constructor(
throw error
} catch (error: Exception) {
val elapsedMs = SystemClock.elapsedRealtime() - startedAt
Log.e(TAG, "$operation failed after ${elapsedMs}ms", error)
Timber.tag(TAG).e(error, "$operation failed after ${elapsedMs}ms")
throw error
}
}

View File

@@ -1,7 +1,7 @@
package hu.bbara.purefin.data.jellyfin.playback
import android.media.MediaCodecList
import android.util.Log
import timber.log.Timber
/**
* Helper to debug available audio/video codecs on the device.
@@ -17,34 +17,34 @@ object CodecDebugHelper {
fun logAvailableDecoders() {
try {
val codecList = MediaCodecList(MediaCodecList.ALL_CODECS)
Log.d(TAG, "=== Available Audio Decoders ===")
Timber.tag(TAG).d("=== Available Audio Decoders ===")
codecList.codecInfos
.filter { !it.isEncoder }
.forEach { codecInfo ->
codecInfo.supportedTypes.forEach { mimeType ->
if (mimeType.startsWith("audio/")) {
Log.d(TAG, "${codecInfo.name}: $mimeType")
Timber.tag(TAG).d("${codecInfo.name}: $mimeType")
if (mimeType.contains("dts", ignoreCase = true) ||
mimeType.contains("truehd", ignoreCase = true)) {
Log.w(TAG, " ^^^ DTS/TrueHD decoder found! ^^^")
Timber.tag(TAG).w(" ^^^ DTS/TrueHD decoder found! ^^^")
}
}
}
}
Log.d(TAG, "=== Available Video Decoders ===")
Timber.tag(TAG).d("=== Available Video Decoders ===")
codecList.codecInfos
.filter { !it.isEncoder }
.forEach { codecInfo ->
codecInfo.supportedTypes.forEach { mimeType ->
if (mimeType.startsWith("video/")) {
Log.d(TAG, "${codecInfo.name}: $mimeType")
Timber.tag(TAG).d("${codecInfo.name}: $mimeType")
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Failed to list codecs", e)
Timber.tag(TAG).e(e, "Failed to list codecs")
}
}

View File

@@ -1,6 +1,5 @@
package hu.bbara.purefin.data.jellyfin.playback
import android.util.Log
import hu.bbara.purefin.core.data.UserSessionRepository
import hu.bbara.purefin.data.jellyfin.client.JellyfinApiClient
import java.util.UUID
@@ -11,6 +10,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.ServerVersion
import timber.log.Timber
@Singleton
class JellyfinPlaybackResolver @Inject constructor(
@@ -33,7 +33,7 @@ class JellyfinPlaybackResolver @Inject constructor(
) ?: return@withContext null
if (playbackInfo.errorCode != null) {
Log.w(TAG, "Playback info failed for $mediaId with ${playbackInfo.errorCode}")
Timber.tag(TAG).w("Playback info failed for $mediaId with ${playbackInfo.errorCode}")
return@withContext null
}
@@ -54,9 +54,9 @@ class JellyfinPlaybackResolver @Inject constructor(
)
if (decision == null) {
Log.w(TAG, "No compatible playback path for $mediaId")
Timber.tag(TAG).w("No compatible playback path for $mediaId")
} else {
Log.d(TAG, "Playback decision for $mediaId resolved as ${decision.reportContext.playMethod}")
Timber.tag(TAG).d("Playback decision for $mediaId resolved as ${decision.reportContext.playMethod}")
}
decision
}
@@ -67,7 +67,7 @@ class JellyfinPlaybackResolver @Inject constructor(
val resolvedVersion = runCatching {
jellyfinApiClient.getPublicSystemInfoVersion()?.let(ServerVersion::fromString)
}.onFailure { error ->
Log.w(TAG, "Unable to fetch server version for $serverUrl", error)
Timber.tag(TAG).w(error, "Unable to fetch server version for $serverUrl")
}.getOrNull() ?: PlaybackProfileDefaults.fallbackServerVersion
serverVersionCache[serverUrl] = resolvedVersion

View File

@@ -1,6 +1,5 @@
package hu.bbara.purefin.data.jellyfin.session
import android.util.Log
import hu.bbara.purefin.core.data.AuthenticationRepository
import hu.bbara.purefin.core.data.JellyfinServerCandidate
import hu.bbara.purefin.core.data.QuickConnectSession
@@ -8,6 +7,7 @@ import hu.bbara.purefin.core.data.UserSessionRepository
import hu.bbara.purefin.data.jellyfin.client.JellyfinApiClient
import kotlinx.coroutines.flow.Flow
import org.jellyfin.sdk.model.api.AuthenticationResult
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@@ -21,22 +21,22 @@ class JellyfinAuthenticationRepository @Inject constructor(
override suspend fun findServer(input: String): JellyfinServerCandidate? =
runCatching { jellyfinApiClient.findServer(input) }
.onFailure { Log.e(TAG, "Server search failed", it) }
.onFailure { Timber.tag(TAG).e(it, "Server search failed") }
.getOrNull()
override suspend fun isQuickConnectEnabled(url: String): Boolean =
runCatching { jellyfinApiClient.isQuickConnectEnabled(url) }
.onFailure { Log.e(TAG, "Quick Connect check failed", it) }
.onFailure { Timber.tag(TAG).e(it, "Quick Connect check failed") }
.getOrDefault(false)
override suspend fun initiateQuickConnect(url: String): QuickConnectSession? =
runCatching { jellyfinApiClient.initiateQuickConnect(url) }
.onFailure { Log.e(TAG, "Quick Connect initiation failed", it) }
.onFailure { Timber.tag(TAG).e(it, "Quick Connect initiation failed") }
.getOrNull()
override suspend fun getQuickConnectState(url: String, secret: String): QuickConnectSession? =
runCatching { jellyfinApiClient.getQuickConnectState(url, secret) }
.onFailure { Log.e(TAG, "Quick Connect state check failed", it) }
.onFailure { Timber.tag(TAG).e(it, "Quick Connect state check failed") }
.getOrNull()
override suspend fun login(url: String, username: String, password: String): Boolean {
@@ -45,7 +45,7 @@ class JellyfinAuthenticationRepository @Inject constructor(
?: return false
saveAuthenticationResult(authResult)
} catch (e: Exception) {
Log.e(TAG, "Login failed", e)
Timber.tag(TAG).e(e, "Login failed")
false
}
}
@@ -56,7 +56,7 @@ class JellyfinAuthenticationRepository @Inject constructor(
?: return false
saveAuthenticationResult(authResult)
} catch (e: Exception) {
Log.e(TAG, "Quick Connect login failed", e)
Timber.tag(TAG).e(e, "Quick Connect login failed")
false
}
}

View File

@@ -0,0 +1,17 @@
package hu.bbara.purefin.data.logging
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet
import hu.bbara.purefin.core.settings.SettingsGroupProvider
@Module
@InstallIn(SingletonComponent::class)
abstract class LogUploadSettingsModule {
@Binds
@IntoSet
abstract fun bindLogUploadSettingsProvider(impl: LogUploadSettingsProvider): SettingsGroupProvider
}

View File

@@ -0,0 +1,58 @@
package hu.bbara.purefin.data.logging
import hu.bbara.purefin.core.logging.PurefinLogger
import hu.bbara.purefin.core.settings.SettingGroup
import hu.bbara.purefin.core.settings.SettingsGroupProvider
import hu.bbara.purefin.core.settings.VoidSetting
import hu.bbara.purefin.data.jellyfin.client.JellyfinApiClient
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class LogUploadSettingsProvider @Inject constructor(
private val jellyfinApiClient: JellyfinApiClient,
) : SettingsGroupProvider {
private val uploadMutex = Mutex()
override val settingGroups: Flow<List<SettingGroup>> = flowOf(
listOf(
SettingGroup(
title = "Logs",
options = listOf(
VoidSetting(
key = UPLOAD_LOGS_KEY,
title = "Upload logs",
onClick = { uploadLogs() }
)
)
)
)
)
private suspend fun uploadLogs() {
uploadMutex.withLock {
val logFiles = PurefinLogger.prepareFilesForUpload()
if (logFiles.isEmpty()) {
Timber.tag(TAG).d("No log files to upload")
return
}
logFiles.forEach { logFile ->
val uploadedName = jellyfinApiClient.uploadLogFile(logFile.data)
?: error("Log upload failed")
PurefinLogger.deleteUploadedFile(logFile)
Timber.tag(TAG).d("Uploaded log file ${logFile.name} as $uploadedName and deleted it locally")
}
}
}
private companion object {
const val TAG = "LogUploadSettings"
const val UPLOAD_LOGS_KEY = "upload_logs"
}
}