Add option to send relevant media info to server for bug reports (#840)

## Description
Adds a item in the More menu to send information about the media item to
the server (and app logs).

This includes the "media sources" which is info about the files and
stream/tracks in the files. It also includes device info, the playback
preferences, and the device's transcoding profile. All of this info is
useful for debugging playback issues.

### Related issues
N/A

### Testing
Emulator testing

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Ray 2026-02-08 13:38:09 -05:00 committed by GitHub
parent d5602aa88c
commit 72e5e6ae86
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 147 additions and 7 deletions

View file

@ -0,0 +1,80 @@
package com.github.damontecres.wholphin.services
import android.content.Context
import android.os.Build
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.services.hilt.IoCoroutineScope
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.util.ExceptionHandler
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.serialization.json.Json
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.clientLogApi
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.model.ClientInfo
import org.jellyfin.sdk.model.DeviceInfo
import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.BaseItemDto
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class MediaReportService
@Inject
constructor(
@param:ApplicationContext private val context: Context,
private val api: ApiClient,
private val serverRepository: ServerRepository,
private val userPreferencesService: UserPreferencesService,
private val clientInfo: ClientInfo,
private val deviceInfo: DeviceInfo,
private val deviceProfileService: DeviceProfileService,
@param:IoCoroutineScope private val ioScope: CoroutineScope,
) {
val json =
Json {
encodeDefaults = false
}
fun sendReportFor(itemId: UUID) {
ioScope.launchIO(ExceptionHandler(autoToast = true)) {
val item = api.userLibraryApi.getItem(itemId = itemId).content
sendReportFor(item)
}
}
suspend fun sendReportFor(item: BaseItemDto) {
val sources =
item.mediaSources ?: api.userLibraryApi
.getItem(itemId = item.id)
.content.mediaSources
val sourcesJson = json.encodeToString(sources)
val playbackPrefs = userPreferencesService.getCurrent().appPreferences.playbackPreferences
val serverVersion = serverRepository.currentServer.value?.serverVersion
val deviceProfile =
deviceProfileService.getOrCreateDeviceProfile(playbackPrefs, serverVersion)
val deviceProfileJson = json.encodeToString(deviceProfile)
val body =
"""
Send media info
serverVersion=$serverVersion
clientInfo=$clientInfo
deviceInfo=$deviceInfo
manufacturer=${Build.MANUFACTURER}
model=${Build.MODEL}
apiLevel=${Build.VERSION.SDK_INT}
playbackPrefs=${playbackPrefs.toString().replace("\n", ", ").replace("\t", " ")}
deviceProfile=$deviceProfileJson
mediaSources=$sourcesJson
""".trimIndent()
Timber.w(body)
val response by api.clientLogApi.logFile(body)
showToast(context, "Sent! Filename=${response.fileName}")
}
}

View file

@ -58,6 +58,7 @@ import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.FavoriteWatchManager
import com.github.damontecres.wholphin.services.MediaReportService
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.ui.AspectRatios
import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus
@ -120,6 +121,7 @@ class CollectionFolderViewModel
private val favoriteWatchManager: FavoriteWatchManager,
private val backdropService: BackdropService,
val navigationManager: NavigationManager,
val mediaReportService: MediaReportService,
@Assisted itemId: String,
@Assisted initialSortAndDirection: SortAndDirection?,
@Assisted("recursive") private val recursive: Boolean,
@ -664,6 +666,7 @@ fun CollectionFolderGrid(
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog.makePresent(it)
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
),
),
onDismissRequest = { moreDialog.makeAbsent() },

View file

@ -22,6 +22,7 @@ import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.FavoriteWatchManager
import com.github.damontecres.wholphin.services.MediaReportService
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
@ -46,6 +47,7 @@ abstract class RecommendedViewModel(
val context: Context,
val navigationManager: NavigationManager,
val favoriteWatchManager: FavoriteWatchManager,
val mediaReportService: MediaReportService,
private val backdropService: BackdropService,
) : ViewModel() {
abstract fun init()
@ -190,6 +192,7 @@ fun RecommendedContent(
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog.makePresent(it)
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
),
),
onDismissRequest = { moreDialog.makeAbsent() },

View file

@ -14,6 +14,7 @@ import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.FavoriteWatchManager
import com.github.damontecres.wholphin.services.MediaReportService
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.SuggestionService
import com.github.damontecres.wholphin.services.SuggestionsResource
@ -58,8 +59,15 @@ class RecommendedMovieViewModel
@Assisted val parentId: UUID,
navigationManager: NavigationManager,
favoriteWatchManager: FavoriteWatchManager,
mediaReportService: MediaReportService,
backdropService: BackdropService,
) : RecommendedViewModel(context, navigationManager, favoriteWatchManager, backdropService) {
) : RecommendedViewModel(
context,
navigationManager,
favoriteWatchManager,
mediaReportService,
backdropService,
) {
@AssistedFactory
interface Factory {
fun create(parentId: UUID): RecommendedMovieViewModel

View file

@ -14,6 +14,7 @@ import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.FavoriteWatchManager
import com.github.damontecres.wholphin.services.LatestNextUpService
import com.github.damontecres.wholphin.services.MediaReportService
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.SuggestionService
import com.github.damontecres.wholphin.services.SuggestionsResource
@ -62,8 +63,15 @@ class RecommendedTvShowViewModel
@Assisted val parentId: UUID,
navigationManager: NavigationManager,
favoriteWatchManager: FavoriteWatchManager,
mediaReportService: MediaReportService,
backdropService: BackdropService,
) : RecommendedViewModel(context, navigationManager, favoriteWatchManager, backdropService) {
) : RecommendedViewModel(
context,
navigationManager,
favoriteWatchManager,
mediaReportService,
backdropService,
) {
@AssistedFactory
interface Factory {
fun create(parentId: UUID): RecommendedTvShowViewModel

View file

@ -49,6 +49,7 @@ import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.clientLogApi
import org.jellyfin.sdk.model.ClientInfo
import org.jellyfin.sdk.model.DeviceInfo
import timber.log.Timber
import java.io.BufferedReader
import java.io.InputStreamReader
import javax.inject.Inject
@ -137,9 +138,13 @@ class DebugViewModel
Send App Logs
clientInfo=$clientInfo
deviceInfo=$deviceInfo
manufacturer=${Build.MANUFACTURER}
model=${Build.MODEL}
apiLevel=${Build.VERSION.SDK_INT}
""".trimIndent() + logcat
val response by api.clientLogApi.logFile(body)
""".trimIndent()
Timber.w(body)
val response by api.clientLogApi.logFile(body + logcat)
showToast(context, "Sent! Filename=${response.fileName}")
}
}
@ -253,6 +258,7 @@ fun DebugPage(
"DeviceInfo: ${viewModel.deviceInfo}",
"Manufacturer: ${Build.MANUFACTURER}",
"Model: ${Build.MODEL}",
"API Level: ${Build.VERSION.SDK_INT}",
"Display Modes:",
*viewModel.refreshRateService.supportedDisplayModes,
).forEach {

View file

@ -25,9 +25,10 @@ import kotlin.time.Duration.Companion.seconds
data class MoreDialogActions(
val navigateTo: (Destination) -> Unit,
var onClickWatch: (UUID, Boolean) -> Unit,
var onClickFavorite: (UUID, Boolean) -> Unit,
var onClickAddPlaylist: (UUID) -> Unit,
val onClickWatch: (UUID, Boolean) -> Unit,
val onClickFavorite: (UUID, Boolean) -> Unit,
val onClickAddPlaylist: (UUID) -> Unit,
val onSendMediaInfo: (UUID) -> Unit,
)
enum class ClearChosenStreams {
@ -205,6 +206,14 @@ fun buildMoreDialogItems(
)
},
)
add(
DialogItem(
text = R.string.send_media_info_log_to_server,
iconStringRes = R.string.fa_file_video,
) {
actions.onSendMediaInfo.invoke(item.id)
},
)
}
fun buildMoreDialogItemsForHome(
@ -314,6 +323,14 @@ fun buildMoreDialogItemsForHome(
},
)
}
add(
DialogItem(
text = R.string.send_media_info_log_to_server,
iconStringRes = R.string.fa_file_video,
) {
actions.onSendMediaInfo.invoke(itemId)
},
)
}
fun buildMoreDialogItemsForPerson(

View file

@ -108,6 +108,7 @@ fun DiscoverMovieDetails(
onClickWatch = { itemId, watched -> },
onClickFavorite = { itemId, favorite -> },
onClickAddPlaylist = { itemId -> },
onSendMediaInfo = {},
)
when (val state = loading) {

View file

@ -97,6 +97,7 @@ fun EpisodeDetails(
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog.makePresent(itemId)
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
)
when (val state = loading) {

View file

@ -12,6 +12,7 @@ import com.github.damontecres.wholphin.data.model.ItemPlayback
import com.github.damontecres.wholphin.preferences.ThemeSongVolume
import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.FavoriteWatchManager
import com.github.damontecres.wholphin.services.MediaReportService
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.StreamChoiceService
import com.github.damontecres.wholphin.services.ThemeSongPlayer
@ -48,6 +49,7 @@ class EpisodeViewModel
val serverRepository: ServerRepository,
val itemPlaybackRepository: ItemPlaybackRepository,
val streamChoiceService: StreamChoiceService,
val mediaReportService: MediaReportService,
private val themeSongPlayer: ThemeSongPlayer,
private val favoriteWatchManager: FavoriteWatchManager,
private val userPreferencesService: UserPreferencesService,

View file

@ -125,6 +125,7 @@ fun MovieDetails(
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog.makePresent(itemId)
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
)
when (val state = loading) {

View file

@ -18,6 +18,7 @@ import com.github.damontecres.wholphin.preferences.ThemeSongVolume
import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.ExtrasService
import com.github.damontecres.wholphin.services.FavoriteWatchManager
import com.github.damontecres.wholphin.services.MediaReportService
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.PeopleFavorites
import com.github.damontecres.wholphin.services.SeerrService
@ -64,6 +65,7 @@ class MovieViewModel
val serverRepository: ServerRepository,
val itemPlaybackRepository: ItemPlaybackRepository,
val streamChoiceService: StreamChoiceService,
val mediaReportService: MediaReportService,
private val themeSongPlayer: ThemeSongPlayer,
private val favoriteWatchManager: FavoriteWatchManager,
private val peopleFavorites: PeopleFavorites,

View file

@ -230,6 +230,7 @@ fun SeriesDetails(
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog.makePresent(itemId)
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
),
)
if (showWatchConfirmation) {

View file

@ -215,6 +215,7 @@ fun SeriesOverview(
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog = it
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
),
onChooseVersion = {
chooseVersion =

View file

@ -16,6 +16,7 @@ import com.github.damontecres.wholphin.data.model.Trailer
import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.ExtrasService
import com.github.damontecres.wholphin.services.FavoriteWatchManager
import com.github.damontecres.wholphin.services.MediaReportService
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.PeopleFavorites
import com.github.damontecres.wholphin.services.SeerrService
@ -85,6 +86,7 @@ class SeriesViewModel
private val trailerService: TrailerService,
private val extrasService: ExtrasService,
val streamChoiceService: StreamChoiceService,
val mediaReportService: MediaReportService,
private val userPreferencesService: UserPreferencesService,
private val backdropService: BackdropService,
private val seerrService: SeerrService,

View file

@ -138,6 +138,7 @@ fun HomePage(
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog = itemId
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
),
)
dialog =

View file

@ -13,6 +13,7 @@ import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.DatePlayedService
import com.github.damontecres.wholphin.services.FavoriteWatchManager
import com.github.damontecres.wholphin.services.LatestNextUpService
import com.github.damontecres.wholphin.services.MediaReportService
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.UserPreferencesService
import com.github.damontecres.wholphin.ui.launchIO
@ -44,6 +45,7 @@ class HomeViewModel
val navigationManager: NavigationManager,
val serverRepository: ServerRepository,
val navDrawerItemRepository: NavDrawerItemRepository,
val mediaReportService: MediaReportService,
private val favoriteWatchManager: FavoriteWatchManager,
private val datePlayedService: DatePlayedService,
private val latestNextUpService: LatestNextUpService,

View file

@ -484,6 +484,7 @@
<string name="zoom_out">Zoom out</string>
<string name="slideshow_duration">Slideshow duration</string>
<string name="play_videos_during_slideshow">Play videos during slideshow</string>
<string name="send_media_info_log_to_server">Send media info log to server</string>
<string-array name="theme_song_volume">
<item>Disabled</item>