Skip to content

Added wholphin://play Intent#738

Open
sysmoon14 wants to merge 3 commits intodamontecres:mainfrom
sysmoon14:feat/deep-link-automation
Open

Added wholphin://play Intent#738
sysmoon14 wants to merge 3 commits intodamontecres:mainfrom
sysmoon14:feat/deep-link-automation

Conversation

@sysmoon14
Copy link
Copy Markdown

@sysmoon14 sysmoon14 commented Jan 21, 2026

Description

This PR implements new deep linking functionality for media playback, enabling external tools (e.g., ADB, Home Assistant) to trigger content directly.

Previously, there was no support for deep linking directly into playback. This PR introduces the wholphin://play/ scheme and adds specific intent parameters (userId, serverId) to bypass the login and server selection screens entirely, allowing for seamless automation from a cold or warm start.

Changes Implemented:

  • New Deep Link Support: Added handling for wholphin://play/{itemId} URIs.
  • Auth Bypass for Automation: Implemented logic to accept userId and serverId via intent extras.
    • If these IDs are present, the app skips the standard startup flow (Server/User Select) and forces a session restoration for the requested user.
    • This works even if the app is already running (via onNewIntent), allowing for instant user switching.
  • Robust State Handling:
    • Cold Start: App initializes, logs in silently, and starts playback.
    • Warm Start / Wrong User: If the app is already open but on a setup screen or logged into a different user, the activity is restarted to ensure a clean authentication flow.
    • Same User: If the app is already logged in as the target user, navigation happens instantly without reloading.
  • UUID Helper: Added robust parsing to handle UUIDs with or without dashes (common in external tools).
  • Duplicate Prevention: Added logic to ignore the playback command if the requested item is already playing.

Related issues

Implements deep linking support for home automation integration.

Steps to verify:

  1. Close the app (or leave it on the User Select screen).
  2. Send the following ADB command:
    adb shell am start -W -a android.intent.action.VIEW \
    -d "wholphin://play/{ITEM_UUID}?autoplay=true" \
    --es "userId" "{USER_UUID}" \
    --es "serverId" "{SERVER_UUID}" \
    -n com.github.damontecres.wholphin/.MainActivity
  3. Observe that the app launches, bypasses all selection screens, and immediately starts playing the content.

Screenshots

N/A - Logic implementation only.

AI/LLM usage

I utilized an LLM (Gemini) to assist with the architecture of the appStart authentication flow and the intent handling lifecycle (onNewIntent) to ensuring smooth transitions between cold and warm states. Validated via ADB debugging and Home Assistant integration.

@sysmoon14 sysmoon14 changed the title implemented changes so that new intent works Added whoplhin://play Intent Jan 21, 2026
@sysmoon14 sysmoon14 changed the title Added whoplhin://play Intent Added wholphin://play Intent Jan 21, 2026
@damontecres
Copy link
Copy Markdown
Owner

Hi, thanks for the contribution! Can you elaborate on the use case for this PR?

Is there already a defined standard (maybe just for arguments?) for starting app playback in Home Assistant? I know the Jellyfin server has websocket commands for browsing and playing media that Wholphin does not implement yet.

@sysmoon14
Copy link
Copy Markdown
Author

Hi, yes of course.

Home assistant has an integration with Jellyfin already, however it can only initiate playback to a client device if the client is already in the Jellyfin app and waiting for a command.

The integration is useful for managing playback media that has already started, or state based automations (e.g. dim the lights when a movie starts), but it's not good for initiating playback to begin with unless you're already in the app.

Enter android intents, this allows me to use the Home Assistant "Android Debug Bridge" to send ADB commands to my TV device.

In my case I have some NFC cards for my little girl that she can tap on a sensor, doing so starts the movie printed on the card. The official Jellyfin and Plex clients support android intents that will start their respective apps and navigate to the media item ID provided.

The limitation with those clients is that they both stop at the "User Select" screen if you have multiple users, and even if you don't, they require an "Ok/Enter" command to be sent to press play on the item it navigated to.

This PR takes it a step further by adding support for autoplay, sending a user ID and sending a server ID, meaning the command can be a one shot!

Some links for context below

jellyfin/jellyfin-androidtv#3452 (reply in thread)

https://simplyexplained.com/blog/how-i-built-an-nfc-movie-library-for-my-kids/

benmooney92 and others added 2 commits January 23, 2026 12:27
Enhanced wholphin://play/ intent handling to work reliably from any
app state (server select, user select, or logged in). Added autoplay
support for Series (next unwatched episode) and Season (first episode).
Fixed resume position handling for user switches and prevented error
screen flash when playing seasons.

- Refactored handleIntent() to suspend function for async handling
- Enhanced createDestinationFromIntentData() to resolve seasons/series
  to episodes before creating Playback destinations
- Improved error handling for unresolvable SEASON/SERIES types
- Code cleanup and comment simplification
@voc0der
Copy link
Copy Markdown
Contributor

voc0der commented Jan 27, 2026

Adding that I am willing to help test this. I use HomeAssistant for this exact purpose. (in my case, I have a voice assistant I can ask to play a movie/ TV show), etc.

@Hukuma1
Copy link
Copy Markdown

Hukuma1 commented Jan 29, 2026

Would love to test this so that I can integrate a button into Home Assistant to quickly load specific content. Would this work with playlists too? Could it also do a randomize sort perhaps? (e.g. load random cartoon from playlist)

Copy link
Copy Markdown
Owner

@damontecres damontecres left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot of hard to follow code here, so it needs some TLC before it can be considered.

}

if (autoplay) {
val currentUser = viewModel.serverRepository.currentUser.value
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of this logic is already implemented in SeriesViewModel, so would be good reuse that maybe by creating a service class.

}
}

private fun parseDeepLink(uri: Uri?): Destination? {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this function for? There seems to be several new functions for parsing information from the URI and/or intent, but its unclear what each is for or how they are used.

}
}

private fun parseUUID(input: String?): UUID? {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String.toUUIDOrNull() from the jellyfin sdk already handles this.

Comment on lines +786 to +788
var pendingRequestedDestination: Destination? = null
var lastProcessedItemId: UUID? = null
var pendingIntentData: MainActivity.IntentData? = null
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be using state or passing these around in functions. Just parking variables in the ViewModel isn't great.


LaunchedEffect(appContentKey) {
if (appContentKey != null) {
delay(200)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why a delay?

appPreferences.updateUrl,
)
} catch (ex: Exception) {
if (ex is kotlinx.coroutines.CancellationException) throw ex
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for?

Comment on lines +333 to +334
viewModel.pendingRequestedDestination
?.also { viewModel.pendingRequestedDestination = null }
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like code smell. See my comment about using state in the ViewModel.

fun EpisodeDetails(
preferences: UserPreferences,
destination: Destination.MediaItem,
autoPlayOnLoad: Boolean = false,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like the intention here to load this page and have it auto play so you can go back to this page?

If so, a better way would be to navigate twice (or set the backstack to two items in MainActivity), to this page followed by the Playback destination.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants