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
120 changes: 120 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

AnyStream is a self-hosted streaming service for media collections built with Kotlin Multiplatform. It consists of a Ktor-based server and multiple client implementations using Compose Multiplatform.

## Architecture

### Server Components
- **server/application** - Main web server built with Ktor, serves API and web client
- **server/library-manager** - Manages media library scanning and organization
- **server/metadata-manager** - Handles media metadata fetching (TMDB integration)
- **server/stream-service** - Media streaming and transcoding with FFmpeg
- **server/db-models** - Database schema, migrations (Flyway), and jOOQ-generated models
- **server/shared** - Shared server utilities and common code

### Client Components
- **client/data-models** - Shared data models between server and all clients (uses Poko for data classes)
- **client/core** - Multiplatform client infrastructure built with Mobius.kt
- **client/presentation** - Shared presentation logic across clients
- **client/ui** - Shared Compose Multiplatform UI components
- **client/web** - Web client implementation with Compose HTML
- **client/android** - Android app with Jetpack Compose
- **client/ios** - iOS app with Compose Multiplatform + SwiftUI
- **client/desktop** - Desktop app with Compose Multiplatform (experimental)

### Database
- Uses SQLite as the single database with Flyway migrations
- jOOQ provides typesafe SQL DSL
- Single Table Inheritance pattern for media metadata
- Migration files: `server/db-models/src/main/resources/db/migration`

## Development Commands

### Building and Running
```bash
# Build entire project
./gradlew build

# Run server (includes web client build)
./gradlew :server:application:run

# Run web client in development mode with hot reload
./gradlew :client:web:jsBrowserRun

# Build production server JAR
./gradlew :server:application:shadowJar

# Run tests
./gradlew test

# Run tests for specific module
./gradlew :server:application:test

# Generate jOOQ database classes
./gradlew :server:db-models:jooq-generator:generateJooq
```

### Environment Variables
When running the server:
- `WEB_CLIENT_PATH` - Path to web client build output
- `DATABASE_URL` - Database file location (defaults to `./anystream.db`)

### Documentation
```bash
# Install MkDocs dependencies
pip install -r docs/requirements.txt

# Serve documentation locally
mkdocs serve

# View docs at http://127.0.0.1:8000
```

## Code Style and Patterns

### Kotlin Multiplatform Structure
- Server uses JVM target with Ktor framework
- Web client uses Kotlin/JS with Compose HTML
- Mobile clients use Kotlin Multiplatform with Compose Multiplatform
- Shared code in `client/core` and `client/data-models`

### Database Patterns
- Use jOOQ DSL for database queries
- Coroutines support for async database operations
- Flyway handles schema migrations automatically
- SQLite with full-text search (FTS5) and JSON support

### Dependency Injection
- Uses Koin for dependency injection across server and clients
- Configuration in respective build.gradle.kts files

### State Management
- Client state management with Mobius.kt (MVI pattern)
- Shared presentation logic in `client/presentation`

## Key Libraries and Frameworks

**Server:**
- Ktor (web framework)
- jOOQ (database queries)
- Flyway (migrations)
- Koin (DI)
- TMDB API (metadata)
- Jaffree (FFmpeg wrapper)

**Clients:**
- Compose Multiplatform (UI)
- Mobius.kt (state management)
- Ktor Client (HTTP)
- kotlinx.serialization (JSON)

## Testing
- Test files alongside source in `src/test/kotlin`
- Uses Kotlin test framework
- Database testing utilities in `server/db-models/testing`
- Run with `./gradlew test` or `./gradlew :module:test`
- rememebr when creating a copyright section, always use the current year
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,8 @@ sealed class Routes {
data object Profile : Routes() {
override val path: String = "profile"
}

data object Search : Routes() {
override val path: String = "search"
}
}
16 changes: 16 additions & 0 deletions client/ui/src/commonMain/kotlin/anystream/ui/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import anystream.ui.media.MediaScreen
import anystream.ui.media.LibraryScreen
import anystream.ui.profile.DevicePairingScannerScreen
import anystream.ui.profile.ProfileScreen
import anystream.ui.search.SearchScreen
import anystream.ui.theme.AppTheme
import anystream.ui.util.LocalImageProvider
import anystream.ui.util.asImageProvider
Expand Down Expand Up @@ -220,6 +221,9 @@ private fun DisplayRoute(
onViewTvShowsClicked = { libraryId ->
stack.push(Routes.Library(libraryId))
},
onSearchClicked = {
stack.push(Routes.Search)
},
)
}

Expand Down Expand Up @@ -288,5 +292,17 @@ private fun DisplayRoute(
onPairDeviceClicked = { stack.push(Routes.PairingScanner) }
)
}

Routes.Search -> {
SearchScreen(
modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding),
onBackClicked = { stack.pop() },
onMetadataClick = { metadataId ->
stack.push(Routes.Details(metadataId))
}
)
}
}
}
20 changes: 20 additions & 0 deletions client/ui/src/commonMain/kotlin/anystream/ui/home/HomeScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
Expand Down Expand Up @@ -57,6 +59,7 @@ fun HomeScreen(
onPlayClick: (mediaLinkId: String) -> Unit,
onViewMoviesClicked: (libraryId: String) -> Unit,
onViewTvShowsClicked: (libraryId: String) -> Unit,
onSearchClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
val (modelState, eventConsumer) = rememberMobiusLoop(HomeScreenModel.Loading, HomeScreenInit) {
Expand All @@ -81,6 +84,7 @@ fun HomeScreen(
onViewMoviesClicked = onViewMoviesClicked,
onViewTvShowsClicked = onViewTvShowsClicked,
onContinueWatchingClick = onPlayClick,
onSearchClicked = onSearchClicked,
)

is HomeScreenModel.LoadingFailed -> Unit // TODO: add error view
Expand All @@ -101,6 +105,7 @@ private fun HomeScreenContent(
onViewMoviesClicked: (String) -> Unit,
onViewTvShowsClicked: (String) -> Unit,
onContinueWatchingClick: (mediaLinkId: String) -> Unit,
onSearchClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
Expand All @@ -121,6 +126,21 @@ private fun HomeScreenContent(
}

CarouselAutoPlayHandler(pagerState, populars.count())

Button(
onClick = onSearchClicked,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(top = 8.dp)
) {
Icon(
Icons.Default.Search,
contentDescription = null,
modifier = Modifier.padding(end = 8.dp)
)
Text("Search")
}

if (currentlyWatching.playbackStates.isNotEmpty()) {
SectionHeader(title = "Continue Watching")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
Expand All @@ -60,7 +59,6 @@ import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import org.jetbrains.compose.resources.painterResource


@Composable
fun MediaScreen(
mediaId: String,
Expand Down Expand Up @@ -243,13 +241,6 @@ private fun BaseDetailsView(
)
}
Spacer(modifier = Modifier.weight(1f))
IconButton(onClick = { /*TODO*/ }) {
Icon(
Icons.Default.Search,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
Spacer(modifier = Modifier.weight(1f))
Row(
Expand Down
Loading