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.preferences.UserPreferences
import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.FavoriteWatchManager 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.NavigationManager
import com.github.damontecres.wholphin.ui.AspectRatios import com.github.damontecres.wholphin.ui.AspectRatios
import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus
@ -120,6 +121,7 @@ class CollectionFolderViewModel
private val favoriteWatchManager: FavoriteWatchManager, private val favoriteWatchManager: FavoriteWatchManager,
private val backdropService: BackdropService, private val backdropService: BackdropService,
val navigationManager: NavigationManager, val navigationManager: NavigationManager,
val mediaReportService: MediaReportService,
@Assisted itemId: String, @Assisted itemId: String,
@Assisted initialSortAndDirection: SortAndDirection?, @Assisted initialSortAndDirection: SortAndDirection?,
@Assisted("recursive") private val recursive: Boolean, @Assisted("recursive") private val recursive: Boolean,
@ -664,6 +666,7 @@ fun CollectionFolderGrid(
playlistViewModel.loadPlaylists(MediaType.VIDEO) playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog.makePresent(it) showPlaylistDialog.makePresent(it)
}, },
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
), ),
), ),
onDismissRequest = { moreDialog.makeAbsent() }, 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.preferences.UserPreferences
import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.FavoriteWatchManager 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.NavigationManager
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
@ -46,6 +47,7 @@ abstract class RecommendedViewModel(
val context: Context, val context: Context,
val navigationManager: NavigationManager, val navigationManager: NavigationManager,
val favoriteWatchManager: FavoriteWatchManager, val favoriteWatchManager: FavoriteWatchManager,
val mediaReportService: MediaReportService,
private val backdropService: BackdropService, private val backdropService: BackdropService,
) : ViewModel() { ) : ViewModel() {
abstract fun init() abstract fun init()
@ -190,6 +192,7 @@ fun RecommendedContent(
playlistViewModel.loadPlaylists(MediaType.VIDEO) playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog.makePresent(it) showPlaylistDialog.makePresent(it)
}, },
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
), ),
), ),
onDismissRequest = { moreDialog.makeAbsent() }, 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.preferences.UserPreferences
import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.FavoriteWatchManager 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.NavigationManager
import com.github.damontecres.wholphin.services.SuggestionService import com.github.damontecres.wholphin.services.SuggestionService
import com.github.damontecres.wholphin.services.SuggestionsResource import com.github.damontecres.wholphin.services.SuggestionsResource
@ -58,8 +59,15 @@ class RecommendedMovieViewModel
@Assisted val parentId: UUID, @Assisted val parentId: UUID,
navigationManager: NavigationManager, navigationManager: NavigationManager,
favoriteWatchManager: FavoriteWatchManager, favoriteWatchManager: FavoriteWatchManager,
mediaReportService: MediaReportService,
backdropService: BackdropService, backdropService: BackdropService,
) : RecommendedViewModel(context, navigationManager, favoriteWatchManager, backdropService) { ) : RecommendedViewModel(
context,
navigationManager,
favoriteWatchManager,
mediaReportService,
backdropService,
) {
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
fun create(parentId: UUID): RecommendedMovieViewModel 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.BackdropService
import com.github.damontecres.wholphin.services.FavoriteWatchManager import com.github.damontecres.wholphin.services.FavoriteWatchManager
import com.github.damontecres.wholphin.services.LatestNextUpService 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.NavigationManager
import com.github.damontecres.wholphin.services.SuggestionService import com.github.damontecres.wholphin.services.SuggestionService
import com.github.damontecres.wholphin.services.SuggestionsResource import com.github.damontecres.wholphin.services.SuggestionsResource
@ -62,8 +63,15 @@ class RecommendedTvShowViewModel
@Assisted val parentId: UUID, @Assisted val parentId: UUID,
navigationManager: NavigationManager, navigationManager: NavigationManager,
favoriteWatchManager: FavoriteWatchManager, favoriteWatchManager: FavoriteWatchManager,
mediaReportService: MediaReportService,
backdropService: BackdropService, backdropService: BackdropService,
) : RecommendedViewModel(context, navigationManager, favoriteWatchManager, backdropService) { ) : RecommendedViewModel(
context,
navigationManager,
favoriteWatchManager,
mediaReportService,
backdropService,
) {
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
fun create(parentId: UUID): RecommendedTvShowViewModel 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.api.client.extensions.clientLogApi
import org.jellyfin.sdk.model.ClientInfo import org.jellyfin.sdk.model.ClientInfo
import org.jellyfin.sdk.model.DeviceInfo import org.jellyfin.sdk.model.DeviceInfo
import timber.log.Timber
import java.io.BufferedReader import java.io.BufferedReader
import java.io.InputStreamReader import java.io.InputStreamReader
import javax.inject.Inject import javax.inject.Inject
@ -137,9 +138,13 @@ class DebugViewModel
Send App Logs Send App Logs
clientInfo=$clientInfo clientInfo=$clientInfo
deviceInfo=$deviceInfo deviceInfo=$deviceInfo
manufacturer=${Build.MANUFACTURER}
model=${Build.MODEL}
apiLevel=${Build.VERSION.SDK_INT}
""".trimIndent() + logcat """.trimIndent()
val response by api.clientLogApi.logFile(body) Timber.w(body)
val response by api.clientLogApi.logFile(body + logcat)
showToast(context, "Sent! Filename=${response.fileName}") showToast(context, "Sent! Filename=${response.fileName}")
} }
} }
@ -253,6 +258,7 @@ fun DebugPage(
"DeviceInfo: ${viewModel.deviceInfo}", "DeviceInfo: ${viewModel.deviceInfo}",
"Manufacturer: ${Build.MANUFACTURER}", "Manufacturer: ${Build.MANUFACTURER}",
"Model: ${Build.MODEL}", "Model: ${Build.MODEL}",
"API Level: ${Build.VERSION.SDK_INT}",
"Display Modes:", "Display Modes:",
*viewModel.refreshRateService.supportedDisplayModes, *viewModel.refreshRateService.supportedDisplayModes,
).forEach { ).forEach {

View file

@ -25,9 +25,10 @@ import kotlin.time.Duration.Companion.seconds
data class MoreDialogActions( data class MoreDialogActions(
val navigateTo: (Destination) -> Unit, val navigateTo: (Destination) -> Unit,
var onClickWatch: (UUID, Boolean) -> Unit, val onClickWatch: (UUID, Boolean) -> Unit,
var onClickFavorite: (UUID, Boolean) -> Unit, val onClickFavorite: (UUID, Boolean) -> Unit,
var onClickAddPlaylist: (UUID) -> Unit, val onClickAddPlaylist: (UUID) -> Unit,
val onSendMediaInfo: (UUID) -> Unit,
) )
enum class ClearChosenStreams { 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( 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( fun buildMoreDialogItemsForPerson(

View file

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

View file

@ -97,6 +97,7 @@ fun EpisodeDetails(
playlistViewModel.loadPlaylists(MediaType.VIDEO) playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog.makePresent(itemId) showPlaylistDialog.makePresent(itemId)
}, },
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
) )
when (val state = loading) { 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.preferences.ThemeSongVolume
import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.FavoriteWatchManager 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.NavigationManager
import com.github.damontecres.wholphin.services.StreamChoiceService import com.github.damontecres.wholphin.services.StreamChoiceService
import com.github.damontecres.wholphin.services.ThemeSongPlayer import com.github.damontecres.wholphin.services.ThemeSongPlayer
@ -48,6 +49,7 @@ class EpisodeViewModel
val serverRepository: ServerRepository, val serverRepository: ServerRepository,
val itemPlaybackRepository: ItemPlaybackRepository, val itemPlaybackRepository: ItemPlaybackRepository,
val streamChoiceService: StreamChoiceService, val streamChoiceService: StreamChoiceService,
val mediaReportService: MediaReportService,
private val themeSongPlayer: ThemeSongPlayer, private val themeSongPlayer: ThemeSongPlayer,
private val favoriteWatchManager: FavoriteWatchManager, private val favoriteWatchManager: FavoriteWatchManager,
private val userPreferencesService: UserPreferencesService, private val userPreferencesService: UserPreferencesService,

View file

@ -125,6 +125,7 @@ fun MovieDetails(
playlistViewModel.loadPlaylists(MediaType.VIDEO) playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog.makePresent(itemId) showPlaylistDialog.makePresent(itemId)
}, },
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
) )
when (val state = loading) { 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.BackdropService
import com.github.damontecres.wholphin.services.ExtrasService import com.github.damontecres.wholphin.services.ExtrasService
import com.github.damontecres.wholphin.services.FavoriteWatchManager 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.NavigationManager
import com.github.damontecres.wholphin.services.PeopleFavorites import com.github.damontecres.wholphin.services.PeopleFavorites
import com.github.damontecres.wholphin.services.SeerrService import com.github.damontecres.wholphin.services.SeerrService
@ -64,6 +65,7 @@ class MovieViewModel
val serverRepository: ServerRepository, val serverRepository: ServerRepository,
val itemPlaybackRepository: ItemPlaybackRepository, val itemPlaybackRepository: ItemPlaybackRepository,
val streamChoiceService: StreamChoiceService, val streamChoiceService: StreamChoiceService,
val mediaReportService: MediaReportService,
private val themeSongPlayer: ThemeSongPlayer, private val themeSongPlayer: ThemeSongPlayer,
private val favoriteWatchManager: FavoriteWatchManager, private val favoriteWatchManager: FavoriteWatchManager,
private val peopleFavorites: PeopleFavorites, private val peopleFavorites: PeopleFavorites,

View file

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

View file

@ -215,6 +215,7 @@ fun SeriesOverview(
playlistViewModel.loadPlaylists(MediaType.VIDEO) playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog = it showPlaylistDialog = it
}, },
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
), ),
onChooseVersion = { onChooseVersion = {
chooseVersion = 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.BackdropService
import com.github.damontecres.wholphin.services.ExtrasService import com.github.damontecres.wholphin.services.ExtrasService
import com.github.damontecres.wholphin.services.FavoriteWatchManager 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.NavigationManager
import com.github.damontecres.wholphin.services.PeopleFavorites import com.github.damontecres.wholphin.services.PeopleFavorites
import com.github.damontecres.wholphin.services.SeerrService import com.github.damontecres.wholphin.services.SeerrService
@ -85,6 +86,7 @@ class SeriesViewModel
private val trailerService: TrailerService, private val trailerService: TrailerService,
private val extrasService: ExtrasService, private val extrasService: ExtrasService,
val streamChoiceService: StreamChoiceService, val streamChoiceService: StreamChoiceService,
val mediaReportService: MediaReportService,
private val userPreferencesService: UserPreferencesService, private val userPreferencesService: UserPreferencesService,
private val backdropService: BackdropService, private val backdropService: BackdropService,
private val seerrService: SeerrService, private val seerrService: SeerrService,

View file

@ -138,6 +138,7 @@ fun HomePage(
playlistViewModel.loadPlaylists(MediaType.VIDEO) playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog = itemId showPlaylistDialog = itemId
}, },
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
), ),
) )
dialog = 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.DatePlayedService
import com.github.damontecres.wholphin.services.FavoriteWatchManager import com.github.damontecres.wholphin.services.FavoriteWatchManager
import com.github.damontecres.wholphin.services.LatestNextUpService 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.NavigationManager
import com.github.damontecres.wholphin.services.UserPreferencesService import com.github.damontecres.wholphin.services.UserPreferencesService
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
@ -44,6 +45,7 @@ class HomeViewModel
val navigationManager: NavigationManager, val navigationManager: NavigationManager,
val serverRepository: ServerRepository, val serverRepository: ServerRepository,
val navDrawerItemRepository: NavDrawerItemRepository, val navDrawerItemRepository: NavDrawerItemRepository,
val mediaReportService: MediaReportService,
private val favoriteWatchManager: FavoriteWatchManager, private val favoriteWatchManager: FavoriteWatchManager,
private val datePlayedService: DatePlayedService, private val datePlayedService: DatePlayedService,
private val latestNextUpService: LatestNextUpService, private val latestNextUpService: LatestNextUpService,

View file

@ -484,6 +484,7 @@
<string name="zoom_out">Zoom out</string> <string name="zoom_out">Zoom out</string>
<string name="slideshow_duration">Slideshow duration</string> <string name="slideshow_duration">Slideshow duration</string>
<string name="play_videos_during_slideshow">Play videos during slideshow</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"> <string-array name="theme_song_volume">
<item>Disabled</item> <item>Disabled</item>