Add integration with Jellyseerr/Seerr (#558)

## Description
This PR add the ability to integrate with a [Jellyseerr/Seerr
server](https://github.com/seerr-team/seerr).

### Features
- Login to the Seerr server using API Key, Jellyfin credentials, or
local Seerr credentials
    - Separate Seerr logins per user profile
- Search Seerr from the search page
- Discover media from Seerr on the nav drawer `Discover` page
    - Also from movie, series, or person pages
- Seamless integration with existing media, i.e. selecting media found
from Seerr that already exists on the Jellyfin server, will go directly
to the regular media page
- Mostly consistent UI between Jellyfin media & Seerr media
- Submit requests to Seerr
- Cancel submitted requests

### Screenshots

![discover](https://github.com/user-attachments/assets/f830b48f-e2f1-43f4-9680-9c6d4e071765)

### Related issues
Closes #54

## Acknowledgements

The `app/src/main/seerr/seerr-api.yml` file is copied & modified from
https://github.com/seerr-team/seerr/blob/develop/seerr-api.yml. I hope
to try to upstream some of the changes in the future.
This commit is contained in:
Ray 2026-01-12 16:35:27 -05:00 committed by GitHub
parent f4e1b0e171
commit 4c7c465c67
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
68 changed files with 13369 additions and 137 deletions

View file

@ -4,6 +4,8 @@ import com.github.damontecres.wholphin.data.model.BaseItem
/**
* Generic state for loading something from the API
*
* @see DataLoadingState
*/
sealed interface LoadingState {
data object Pending : LoadingState
@ -68,3 +70,28 @@ sealed interface HomeRowLoadingState {
listOfNotNull(message, exception?.localizedMessage).joinToString(" - ")
}
}
/**
* Generic state for loading something from the API
*
* @see LoadingState
*/
sealed interface DataLoadingState<out T> {
data object Pending : DataLoadingState<Nothing>
data object Loading : DataLoadingState<Nothing>
data class Success<T>(
val data: T,
) : DataLoadingState<T>
data class Error(
val message: String? = null,
val exception: Throwable? = null,
) : DataLoadingState<Nothing> {
constructor(exception: Throwable) : this(null, exception)
val localizedMessage: String =
listOfNotNull(message, exception?.localizedMessage).joinToString(" - ")
}
}

View file

@ -0,0 +1,24 @@
package com.github.damontecres.wholphin.util
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.time.LocalDate
import java.time.format.DateTimeFormatter
object LocalDateSerializer : KSerializer<LocalDate> {
override val descriptor: SerialDescriptor
get() = SerialDescriptor("LocalDate", String.serializer().descriptor)
override fun serialize(
encoder: Encoder,
value: LocalDate,
) {
encoder.encodeString(DateTimeFormatter.ISO_LOCAL_DATE.format(value))
}
override fun deserialize(decoder: Decoder): LocalDate =
decoder.decodeString().let { LocalDate.parse(it, DateTimeFormatter.ISO_LOCAL_DATE) }
}