v3.9.2: Fix Android TV OOM crash + backend Content-Type headers
- Backend: Add Content-Type: application/json to all JSON API endpoints - Android TV: Reduce HomeViewModel memory usage (load 4 categories only, limit 15 items each) - Android TV: Prevent OOM kill on TV devices with limited RAM - Updated APK, docker-compose, health endpoint to v3.9.2
This commit is contained in:
parent
fbe89e14fd
commit
69308bf696
95 changed files with 7684 additions and 7709 deletions
18
Dockerfile
18
Dockerfile
|
|
@ -7,34 +7,34 @@ COPY frontend-react/ .
|
|||
RUN npm run build
|
||||
|
||||
# Stage 2: Build Image (Backend)
|
||||
FROM golang:1.24-alpine AS backend-builder
|
||||
FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS backend-builder
|
||||
WORKDIR /app/backend
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache gcc musl-dev
|
||||
|
||||
ARG TARGETOS TARGETARCH
|
||||
|
||||
COPY backend/go.mod backend/go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY backend/ .
|
||||
# Build static binary for Linux amd64
|
||||
RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o server cmd/server/main.go
|
||||
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags="-w -s" -o server cmd/server/main.go
|
||||
|
||||
# Stage 3: Final Image
|
||||
FROM alpine:latest
|
||||
WORKDIR /app
|
||||
|
||||
# Install runtime dependencies (sqlite + yt-dlp for video extraction fallback)
|
||||
RUN apk add --no-cache sqlite ca-certificates tzdata python3 py3-pip && \
|
||||
pip3 install --break-system-packages yt-dlp
|
||||
# Install runtime dependencies
|
||||
RUN apk add --no-cache sqlite ca-certificates tzdata python3 py3-pip
|
||||
RUN pip3 install --break-system-packages --ignore-installed yt-dlp || true
|
||||
|
||||
# Copy backend binary
|
||||
COPY --from=backend-builder /app/backend/server .
|
||||
|
||||
# Copy frontend build to the expected static directory
|
||||
# The backend expects ../frontend-react/dist relative to itself, or we configure it.
|
||||
# Let's align with the standard deployment structure: /app/server and /app/dist
|
||||
COPY --from=frontend-builder /app/frontend/dist ./dist
|
||||
|
||||
|
||||
|
||||
# Create data directory
|
||||
RUN mkdir -p data
|
||||
|
||||
|
|
|
|||
|
|
@ -49,9 +49,9 @@ fun StreamFlowTvApp() {
|
|||
try {
|
||||
currentTheme = userRepo.theme.first()
|
||||
val serverUrl = userRepo.serverUrl.first()
|
||||
/*if (serverUrl.isNotBlank()) {
|
||||
if (serverUrl.isNotBlank()) {
|
||||
ApiClient.baseUrl = serverUrl
|
||||
}*/
|
||||
}
|
||||
Log.d("StreamFlowTvApp", "Settings loaded: theme=$currentTheme, url=$serverUrl")
|
||||
} catch (e: Exception) {
|
||||
Log.e("StreamFlowTvApp", "Error loading settings", e)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ object ApiClient {
|
|||
// Default base URL for testing
|
||||
// Change this to your production API when ready
|
||||
// var baseUrl: String = "https://nf.khoavo.myds.me"
|
||||
private var _baseUrl: String = "http://10.0.2.2:3478/"
|
||||
private var _baseUrl: String = "http://10.0.2.2:8000/"
|
||||
|
||||
var baseUrl: String
|
||||
get() = _baseUrl
|
||||
|
|
|
|||
|
|
@ -72,54 +72,20 @@ class HomeViewModel : ViewModel() {
|
|||
val allFlattened = java.util.Collections.synchronizedList(mutableListOf<Movie>())
|
||||
|
||||
kotlinx.coroutines.coroutineScope {
|
||||
// 1. Initial categories
|
||||
// Load main categories only (to avoid OOM on TV devices)
|
||||
val categoryTasks = categories.map { (slug, name) ->
|
||||
async {
|
||||
try {
|
||||
val response = repository.getHomeVideos(slug)
|
||||
allMovies[name] = response.items
|
||||
allFlattened.addAll(response.items)
|
||||
allMovies[name] = response.items.take(15)
|
||||
allFlattened.addAll(response.items.take(15))
|
||||
response.items
|
||||
} catch (_: Exception) { emptyList<Movie>() }
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fetch Genres & Countries metadata in parallel
|
||||
val genresDeferred = async { try { repository.getGenres().take(8) } catch (_: Exception) { emptyList() } }
|
||||
val countriesDeferred = async { try { repository.getCountries().take(5) } catch (_: Exception) { emptyList() } }
|
||||
|
||||
val genres = genresDeferred.await()
|
||||
val countries = countriesDeferred.await()
|
||||
|
||||
// 3. Fetch Genre and Country content in parallel
|
||||
val genreTasks = genres.map { genre ->
|
||||
async {
|
||||
try {
|
||||
val response = repository.getHomeVideos(genre.slug)
|
||||
if (response.items.isNotEmpty()) {
|
||||
allMovies["Genre: ${genre.name}"] = response.items
|
||||
allFlattened.addAll(response.items)
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
val countryTasks = countries.map { country ->
|
||||
async {
|
||||
try {
|
||||
val response = repository.getHomeVideos(country.slug)
|
||||
if (response.items.isNotEmpty()) {
|
||||
allMovies["Country: ${country.name}"] = response.items
|
||||
allFlattened.addAll(response.items)
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for everything
|
||||
// Wait for categories
|
||||
categoryTasks.awaitAll()
|
||||
genreTasks.awaitAll()
|
||||
countryTasks.awaitAll()
|
||||
}
|
||||
|
||||
val heroItems = allMovies[categories.first().second]?.take(5) ?: emptyList()
|
||||
|
|
|
|||
0
android-tv/gradlew
vendored
Normal file → Executable file
0
android-tv/gradlew
vendored
Normal file → Executable file
|
|
@ -75,6 +75,7 @@ func (h *Handler) GetHomeVideos(w http.ResponseWriter, r *http.Request) {
|
|||
return p.GetMoviesByCategory(category, page)
|
||||
})
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(movies)
|
||||
}
|
||||
|
||||
|
|
@ -94,6 +95,7 @@ func (h *Handler) SearchVideos(w http.ResponseWriter, r *http.Request) {
|
|||
return p.Search(query, page)
|
||||
})
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(movies)
|
||||
}
|
||||
|
||||
|
|
@ -211,6 +213,7 @@ func (h *Handler) ExtractVideo(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(info)
|
||||
}
|
||||
|
||||
|
|
@ -306,6 +309,7 @@ func (h *Handler) GetMovieDetail(w http.ResponseWriter, r *http.Request) {
|
|||
primaryMovie.Episodes = uniqueEps
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(primaryMovie)
|
||||
}
|
||||
|
||||
|
|
@ -316,6 +320,7 @@ func (h *Handler) GetGenres(w http.ResponseWriter, r *http.Request) {
|
|||
}); ok {
|
||||
genres, err := gp.GetGenres()
|
||||
if err == nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(genres)
|
||||
return
|
||||
}
|
||||
|
|
@ -331,6 +336,7 @@ func (h *Handler) GetCountries(w http.ResponseWriter, r *http.Request) {
|
|||
}); ok {
|
||||
countries, err := cp.GetCountries()
|
||||
if err == nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(countries)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,11 +61,13 @@ func (s *ImageService) GetProxiedImage(url string, width int) ([]byte, string, e
|
|||
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Printf("GetProxiedImage fetch error: %v\n", err)
|
||||
return nil, "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
fmt.Printf("GetProxiedImage status error: %d for url: %s\n", resp.StatusCode, url)
|
||||
return nil, "", fmt.Errorf("image fetch failed: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
|
|
@ -84,6 +86,7 @@ func (s *ImageService) GetProxiedImage(url string, width int) ([]byte, string, e
|
|||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("GetProxiedImage decode error: %v for content-type: %s and url: %s\n", err, contentType, url)
|
||||
return nil, "", fmt.Errorf("decode error: %v", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ version: '3.8'
|
|||
|
||||
services:
|
||||
streamflow:
|
||||
image: git.khoavo.myds.me/vndangkhoa/kv-streamflow:v3.9.1
|
||||
image: git.khoavo.myds.me/vndangkhoa/kv-streamflow:v3.9.2
|
||||
container_name: streamflow
|
||||
platform: linux/amd64
|
||||
ports:
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Reference in a new issue