Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.simplecityapps.shuttle.appinitializers

import android.app.Application
import com.simplecityapps.playback.PlaybackWatcher
import com.simplecityapps.shuttle.playback.PlaybackReporter
import javax.inject.Inject
import timber.log.Timber

/**
* Initializes the PlaybackReporter by registering it with the PlaybackWatcher.
*
* This allows the PlaybackReporter to listen for playback events and report them
* to remote media servers (Jellyfin, Emby, Plex).
*/
class PlaybackReporterInitializer
@Inject
constructor(
private val playbackWatcher: PlaybackWatcher,
private val playbackReporter: PlaybackReporter
) : AppInitializer {
override fun init(application: Application) {
Timber.v("PlaybackReporterInitializer.init()")
playbackWatcher.addCallback(playbackReporter)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.simplecityapps.shuttle.appinitializers.AppInitializer
import com.simplecityapps.shuttle.appinitializers.CrashReportingInitializer
import com.simplecityapps.shuttle.appinitializers.MediaProviderInitializer
import com.simplecityapps.shuttle.appinitializers.PlaybackInitializer
import com.simplecityapps.shuttle.appinitializers.PlaybackReporterInitializer
import com.simplecityapps.shuttle.appinitializers.RemoteConfigInitializer
import com.simplecityapps.shuttle.appinitializers.ShortcutInitializer
import com.simplecityapps.shuttle.appinitializers.TimberInitializer
Expand All @@ -30,6 +31,10 @@ abstract class AppModuleBinds {
@IntoSet
abstract fun providePlaybackInitializer(bind: PlaybackInitializer): AppInitializer

@Binds
@IntoSet
abstract fun providePlaybackReporterInitializer(bind: PlaybackReporterInitializer): AppInitializer

@Binds
@IntoSet
abstract fun provideWidgetInitializer(bind: WidgetInitializer): AppInitializer
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.simplecityapps.provider.emby.EmbyMediaProvider
import com.simplecityapps.provider.emby.http.EmbyTranscodeService
import com.simplecityapps.provider.emby.http.ItemsService
import com.simplecityapps.provider.emby.http.LoginCredentials
import com.simplecityapps.provider.emby.http.PlaybackReportingService
import com.simplecityapps.provider.emby.http.UserService
import com.simplecityapps.shuttle.persistence.SecurePreferenceManager
import com.squareup.moshi.Moshi
Expand Down Expand Up @@ -67,6 +68,12 @@ open class EmbyMediaProviderModule {
@Named("EmbyRetrofit") retrofit: Retrofit
): EmbyTranscodeService = retrofit.create()

@Provides
@Singleton
fun providePlaybackReportingService(
@Named("EmbyRetrofit") retrofit: Retrofit
): PlaybackReportingService = retrofit.create()

@Provides
@Singleton
fun provideCredentialStore(securePreferenceManager: SecurePreferenceManager): CredentialStore = CredentialStore(securePreferenceManager).apply {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.simplecityapps.provider.emby.http

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class PlaybackStartInfo(
@Json(name = "ItemId")
val itemId: String,
@Json(name = "SessionId")
val sessionId: String,
@Json(name = "CanSeek")
val canSeek: Boolean,
@Json(name = "IsPaused")
val isPaused: Boolean,
@Json(name = "IsMuted")
val isMuted: Boolean,
@Json(name = "VolumeLevel")
val volumeLevel: Int,
@Json(name = "PlayMethod")
val playMethod: String,
@Json(name = "QueueableMediaTypes")
val queueableMediaTypes: String
)

@JsonClass(generateAdapter = true)
data class PlaybackProgressInfo(
@Json(name = "ItemId")
val itemId: String,
@Json(name = "SessionId")
val sessionId: String,
@Json(name = "PositionTicks")
val positionTicks: Long,
@Json(name = "CanSeek")
val canSeek: Boolean,
@Json(name = "IsPaused")
val isPaused: Boolean,
@Json(name = "IsMuted")
val isMuted: Boolean,
@Json(name = "VolumeLevel")
val volumeLevel: Int,
@Json(name = "PlayMethod")
val playMethod: String
)

@JsonClass(generateAdapter = true)
data class PlaybackStopInfo(
@Json(name = "ItemId")
val itemId: String,
@Json(name = "SessionId")
val sessionId: String,
@Json(name = "PositionTicks")
val positionTicks: Long
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.simplecityapps.provider.emby.http

import com.simplecityapps.networking.retrofit.NetworkResult
import retrofit2.http.Body
import retrofit2.http.Header
import retrofit2.http.Headers
import retrofit2.http.POST
import retrofit2.http.Url

interface PlaybackReportingService {
@POST
@Headers(
"Accept: application/json",
"Content-Type: application/json"
)
suspend fun reportPlaybackStart(
@Url url: String,
@Header("X-Emby-Token") token: String,
@Body body: PlaybackStartInfo
): NetworkResult<Unit>

@POST
@Headers(
"Accept: application/json",
"Content-Type: application/json"
)
suspend fun reportPlaybackProgress(
@Url url: String,
@Header("X-Emby-Token") token: String,
@Body body: PlaybackProgressInfo
): NetworkResult<Unit>

@POST
@Headers(
"Accept: application/json",
"Content-Type: application/json"
)
suspend fun reportPlaybackStopped(
@Url url: String,
@Header("X-Emby-Token") token: String,
@Body body: PlaybackStopInfo
): NetworkResult<Unit>
}

suspend fun PlaybackReportingService.playbackStart(
serverUrl: String,
token: String,
itemId: String,
sessionId: String,
userId: String
): NetworkResult<Unit> = reportPlaybackStart(
url = "$serverUrl/Sessions/Playing",
token = token,
body = PlaybackStartInfo(
itemId = itemId,
sessionId = sessionId,
canSeek = true,
isPaused = false,
isMuted = false,
volumeLevel = 100,
playMethod = "DirectPlay",
queueableMediaTypes = "Audio"
)
)

suspend fun PlaybackReportingService.playbackProgress(
serverUrl: String,
token: String,
itemId: String,
sessionId: String,
positionTicks: Long,
isPaused: Boolean
): NetworkResult<Unit> = reportPlaybackProgress(
url = "$serverUrl/Sessions/Playing/Progress",
token = token,
body = PlaybackProgressInfo(
itemId = itemId,
sessionId = sessionId,
positionTicks = positionTicks,
canSeek = true,
isPaused = isPaused,
isMuted = false,
volumeLevel = 100,
playMethod = "DirectPlay"
)
)

suspend fun PlaybackReportingService.playbackStopped(
serverUrl: String,
token: String,
itemId: String,
sessionId: String,
positionTicks: Long
): NetworkResult<Unit> = reportPlaybackStopped(
url = "$serverUrl/Sessions/Playing/Stopped",
token = token,
body = PlaybackStopInfo(
itemId = itemId,
sessionId = sessionId,
positionTicks = positionTicks
)
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.simplecityapps.provider.jellyfin.JellyfinMediaProvider
import com.simplecityapps.provider.jellyfin.http.ItemsService
import com.simplecityapps.provider.jellyfin.http.JellyfinTranscodeService
import com.simplecityapps.provider.jellyfin.http.LoginCredentials
import com.simplecityapps.provider.jellyfin.http.PlaybackReportingService
import com.simplecityapps.provider.jellyfin.http.UserService
import com.simplecityapps.shuttle.persistence.SecurePreferenceManager
import com.squareup.moshi.Moshi
Expand Down Expand Up @@ -67,6 +68,12 @@ open class JellyfinMediaProviderModule {
@Named("JellyfinRetrofit") retrofit: Retrofit
): JellyfinTranscodeService = retrofit.create()

@Provides
@Singleton
fun providePlaybackReportingService(
@Named("JellyfinRetrofit") retrofit: Retrofit
): PlaybackReportingService = retrofit.create()

@Provides
@Singleton
fun provideCredentialStore(securePreferenceManager: SecurePreferenceManager): CredentialStore = CredentialStore(securePreferenceManager).apply {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.simplecityapps.provider.jellyfin.http

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class PlaybackStartInfo(
@Json(name = "ItemId")
val itemId: String,
@Json(name = "SessionId")
val sessionId: String,
@Json(name = "CanSeek")
val canSeek: Boolean,
@Json(name = "IsPaused")
val isPaused: Boolean,
@Json(name = "IsMuted")
val isMuted: Boolean,
@Json(name = "VolumeLevel")
val volumeLevel: Int,
@Json(name = "PlayMethod")
val playMethod: String,
@Json(name = "QueueableMediaTypes")
val queueableMediaTypes: String
)

@JsonClass(generateAdapter = true)
data class PlaybackProgressInfo(
@Json(name = "ItemId")
val itemId: String,
@Json(name = "SessionId")
val sessionId: String,
@Json(name = "PositionTicks")
val positionTicks: Long,
@Json(name = "CanSeek")
val canSeek: Boolean,
@Json(name = "IsPaused")
val isPaused: Boolean,
@Json(name = "IsMuted")
val isMuted: Boolean,
@Json(name = "VolumeLevel")
val volumeLevel: Int,
@Json(name = "PlayMethod")
val playMethod: String
)

@JsonClass(generateAdapter = true)
data class PlaybackStopInfo(
@Json(name = "ItemId")
val itemId: String,
@Json(name = "SessionId")
val sessionId: String,
@Json(name = "PositionTicks")
val positionTicks: Long
)
Loading
Loading