# Cross-App Service Porting (TMDB Metadata, Person Detail, In-Flight Dedup)

## Overview

When porting feature services from one Android TV app to another (e.g., NuvioTV → Nexstream/Unspooled), the work spans:
1. **Service layer copy** — Retrofit interfaces, repository classes, ViewModel logic
2. **DI wiring** — Hilt `@Module`/`@Provides`, constructor injection
3. **Dependency audit** — shared models, utilities, network config
4. **Screen wiring** — connecting the new services to existing screens

## Service Porting Checklist

### 1. Retrofit Interface Copy

Copy the Retrofit interface from the source app. Key considerations:
- Verify the base URL matches the target app's config
- Check for `@Header` vs `@Query` parameter styles
- Ensure serialization annotations match (e.g., `@SerializedName`)
- Verify the API key/auth interceptor is already in place

```kotlin
// Source: NuvioTV's TmdbMetadataService.kt
interface TmdbMetadataApi {
    @GET("movie/{movieId}/credits")
    suspend fun getMovieCredits(@Path("movieId") id: Int): CreditsResponse

    @GET("movie/{movieId}/videos")
    suspend fun getMovieVideos(@Path("movieId") id: Int): VideosResponse

    @GET("movie/{movieId}/similar")
    suspend fun getSimilarMovies(@Path("movieId") id: Int): MovieListResponse
}
```

### 2. Repository / Service Class Copy

Copy the repository/service class that wraps the Retrofit interface. Key patterns:
- **In-flight request dedup** — use `CompletableDeferred` to prevent duplicate concurrent requests
- **Caching** — Room cache for frequently accessed data (cast, trailers, similar)
- **Error handling** — narrow exception catching (IOException, HttpException, SerializationException)

```kotlin
@Singleton
class TmdbMetadataService @Inject constructor(
    private val api: TmdbMetadataApi,
    private val cacheDao: TmdbCacheDao,
) {
    // In-flight dedup map
    private val inFlight = mutableMapOf<String, CompletableDeferred<Result<*>>>()

    suspend fun getMovieCredits(movieId: Int): Result<CreditsResponse> {
        val key = "credits_$movieId"
        return inFlight.getOrPut(key) {
            CompletableDeferred(api.getMovieCredits(movieId)).also { def ->
                def.invokeOnCompletion { inFlight.remove(key) }
            }
        }.await()
    }
}
```

### 3. Hilt Module Wiring

Add a `@Module` with `@Provides` for the new service:

```kotlin
@Module
@InstallIn(SingletonComponent::class)
object TmdbMetadataModule {
    @Provides
    @Singleton
    fun provideTmdbMetadataApi(@TmdbClient client: OkHttpClient): TmdbMetadataApi {
        return Retrofit.Builder()
            .baseUrl("https://api.themoviedb.org/3/")
            .client(client)
            .addConverterFactory(MoshiConverterFactory.create())
            .build()
            .create(TmdbMetadataApi::class.java)
    }

    @Provides
    @Singleton
    fun provideTmdbMetadataService(
        api: TmdbMetadataApi,
        cacheDao: TmdbCacheDao,
    ): TmdbMetadataService {
        return TmdbMetadataService(api, cacheDao)
    }
}
```

### 4. Dependency Audit

Before copying, audit what the source service depends on:
- **Shared models** — `CreditsResponse`, `VideosResponse`, `Person`, `Movie` — copy these too
- **Network config** — OkHttp client, interceptors (auth, rate limit, error handling)
- **Database DAOs** — Room DAOs for caching
- **ViewModel utilities** — helper functions, state builders

### 5. Screen Wiring

After the service is wired, connect it to screens:
- Inject into ViewModels via `@Inject constructor`
- Expose via `StateFlow`/`StateFlow` in the ViewModel
- Wire Compose screens to consume the new state

## Common Pitfalls

1. **Hilt `NonExistentClass`** — when adding new `@Inject` classes in different packages, KSP may fail. Move the class to the same package as the consumer, or use `dagger.Lazy<T>`.
2. **Missing imports** — subagents frequently omit imports for `CompletableDeferred`, `Result`, `MoshiConverterFactory`, `OkHttpClient`. Always verify imports after copying.
3. **Retrofit base URL mismatch** — the source app may use a different TMDB key type (v3 query param vs v4 Bearer). Verify the auth interceptor handles the target app's key format.
4. **Room schema conflicts** — if the target app already has Room tables, ensure new table names don't collide. Use descriptive table names (e.g., `tmdb_credits`, `tmdb_videos`).
5. **Coroutine scope mismatch** — in-flight dedup uses `CompletableDeferred` which must be cleaned up on completion. Use `invokeOnCompletion` to remove from the map.

## Reference: NuvioTV Service Sizes

| Service | Lines | Key Features |
|---------|-------|--------------|
| `TmdbMetadataService` | ~1437 | cast, trailers, similar, in-flight dedup |
| `TmdbService` | ~294 | IMDB↔TMDB caching, key rotation |
| `PersonDetailService` | ~200 | person detail, filmography |
| `EpisodeEnrichmentService` | ~150 | episode-level metadata enrichment |
| `ContinueWatchingEnricher` | ~100 | ContinueWatching + TMDB metadata merge |
