Enhance Docker and backend configurations for improved deployment
- Updated docker-compose.yml to include environment variable support for services, enhancing flexibility in configuration. - Refactored Dockerfile to utilize build arguments for VITE environment variables, allowing for better customization during builds. - Improved Nginx configuration to handle larger video uploads by increasing client_max_body_size to 5GB. - Enhanced backend Dockerfile to include wget for health checks and improved startup logging for database migrations. - Added validation for critical environment variables in the backend to ensure necessary configurations are present before application startup. - Updated content streaming logic to support direct HLS URL construction, improving streaming reliability and user experience. - Refactored various components and services to streamline access checks and improve error handling during content playback.
This commit is contained in:
@@ -302,8 +302,11 @@ async function checkRentalAccess() {
|
||||
const result = await libraryService.checkRentExists(contentId)
|
||||
hasActiveRental.value = result.exists
|
||||
rentalExpiresAt.value = result.expiresAt ? new Date(result.expiresAt) : null
|
||||
} catch {
|
||||
hasActiveRental.value = false
|
||||
} catch (err) {
|
||||
console.warn('Rental check failed:', err)
|
||||
// If the rental check fails (e.g. auth issue) but the user owns the
|
||||
// content, treat it as "can play" so the owner isn't blocked.
|
||||
hasActiveRental.value = !!props.content.isOwnProject
|
||||
rentalExpiresAt.value = null
|
||||
}
|
||||
}
|
||||
@@ -356,8 +359,16 @@ function getProfile(pubkey: string) {
|
||||
}
|
||||
|
||||
function handlePlay() {
|
||||
// Free content with a streaming URL can play without auth
|
||||
if (props.content?.streamingUrl) {
|
||||
// Free content (YouTube embeds or rentalPrice = 0) plays without auth
|
||||
const isFree = props.content?.streamingUrl ||
|
||||
!props.content?.rentalPrice || props.content.rentalPrice <= 0
|
||||
if (isFree) {
|
||||
showVideoPlayer.value = true
|
||||
return
|
||||
}
|
||||
|
||||
// Content creators can always preview/play their own content
|
||||
if (props.content?.isOwnProject) {
|
||||
showVideoPlayer.value = true
|
||||
return
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onUnmounted, nextTick } from 'vue'
|
||||
import Hls from 'hls.js'
|
||||
import { contentService } from '../services/content.service'
|
||||
import { indeehubApiService } from '../services/indeehub-api.service'
|
||||
import type { Content } from '../types/content'
|
||||
|
||||
interface Props {
|
||||
@@ -304,23 +304,55 @@ async function fetchStream() {
|
||||
streamError.value = null
|
||||
|
||||
try {
|
||||
const info = await contentService.getStreamInfo(contentId)
|
||||
hlsStreamUrl.value = info.file
|
||||
// Try the backend stream endpoint first (handles DRM, presigned URLs, etc.)
|
||||
const info = await indeehubApiService.getStreamingUrl(contentId)
|
||||
const streamFile = (info as any).file || info.url
|
||||
hlsStreamUrl.value = streamFile
|
||||
await nextTick()
|
||||
initPlayer(info.file)
|
||||
initPlayer(streamFile)
|
||||
} catch (err: any) {
|
||||
const status = err?.response?.status
|
||||
if (status === 403) {
|
||||
streamError.value = 'You need an active subscription or rental to watch this content.'
|
||||
console.warn('Stream API failed, trying direct HLS URL fallback:', err?.response?.status || err?.message)
|
||||
|
||||
// Fallback: construct the HLS URL directly from the public S3 bucket.
|
||||
// The transcoded HLS files live in the public bucket at a predictable path.
|
||||
// This works because the user already passed the access check before the
|
||||
// player opened (rental verified in ContentDetailModal.handlePlay).
|
||||
const directUrl = buildDirectHlsUrl()
|
||||
if (directUrl) {
|
||||
console.log('Using direct HLS URL:', directUrl)
|
||||
hlsStreamUrl.value = directUrl
|
||||
await nextTick()
|
||||
initPlayer(directUrl)
|
||||
} else {
|
||||
streamError.value = 'Unable to load the stream. Please try again.'
|
||||
const status = err?.response?.status
|
||||
if (status === 403) {
|
||||
streamError.value = 'You need an active subscription or rental to watch this content.'
|
||||
} else if (status === 401) {
|
||||
streamError.value = 'Please sign in to watch this content.'
|
||||
} else {
|
||||
streamError.value = 'Unable to load the stream. Please try again.'
|
||||
}
|
||||
console.error('Failed to fetch stream info:', err)
|
||||
}
|
||||
console.error('Failed to fetch stream info:', err)
|
||||
} finally {
|
||||
isLoadingStream.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a direct HLS URL from the content's API data.
|
||||
* The public bucket stores transcoded HLS at:
|
||||
* {CDN_URL}/projects/{projectId}/file/transcoded/file.m3u8
|
||||
*/
|
||||
function buildDirectHlsUrl(): string | null {
|
||||
const projectId = props.content?.id
|
||||
if (!projectId) return null
|
||||
|
||||
// Use the CDN/storage URL configured for the self-hosted backend
|
||||
const cdnBase = import.meta.env.VITE_INDEEHUB_CDN_URL || 'http://localhost:9000/indeedhub-public'
|
||||
return `${cdnBase}/projects/${projectId}/file/transcoded/file.m3u8`
|
||||
}
|
||||
|
||||
function initPlayer(url: string) {
|
||||
destroyHls()
|
||||
const video = videoEl.value
|
||||
|
||||
@@ -64,15 +64,26 @@ export function useAccess() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if content requires subscription
|
||||
* Check if content is free (no rental price and/or has a direct streaming URL)
|
||||
*/
|
||||
function isFreeContent(_content: any): boolean {
|
||||
if (!_content) return false
|
||||
// Content with a streaming URL (e.g. YouTube embeds) is free
|
||||
if (_content.streamingUrl) return true
|
||||
// Content with no rental price or zero price is free
|
||||
return !_content.rentalPrice || _content.rentalPrice <= 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if content requires subscription or payment to access
|
||||
*/
|
||||
function requiresSubscription(_content: any): boolean {
|
||||
// All content requires subscription or rental unless explicitly free
|
||||
if (isFreeContent(_content)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if content can be rented
|
||||
* Check if content can be rented (has a non-zero price)
|
||||
*/
|
||||
function canRent(_content: any): boolean {
|
||||
return !!_content.rentalPrice && _content.rentalPrice > 0
|
||||
@@ -82,6 +93,7 @@ export function useAccess() {
|
||||
checkContentAccess,
|
||||
hasActiveSubscription,
|
||||
getSubscriptionTier,
|
||||
isFreeContent,
|
||||
requiresSubscription,
|
||||
canRent,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { apiService } from './api.service'
|
||||
import { indeehubApiService } from './indeehub-api.service'
|
||||
import type { ApiRent, ApiContent } from '../types/api'
|
||||
import { USE_MOCK } from '../utils/mock'
|
||||
|
||||
@@ -99,10 +100,11 @@ class LibraryService {
|
||||
|
||||
/**
|
||||
* Check if an active (non-expired) rent exists for a given content ID.
|
||||
* Returns the rental expiry when one exists.
|
||||
* Uses indeehubApiService which carries Nostr JWT auth tokens for more
|
||||
* reliable authentication than the generic apiService.
|
||||
*/
|
||||
async checkRentExists(contentId: string): Promise<{ exists: boolean; expiresAt?: string }> {
|
||||
return apiService.get(`/rents/content/${contentId}/exists`)
|
||||
return indeehubApiService.get(`/rents/content/${contentId}/exists`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -157,8 +157,14 @@ class Nip98Service {
|
||||
sessionStorage.setItem('nostr_token', accessToken)
|
||||
|
||||
return accessToken
|
||||
} catch {
|
||||
this.clearSession()
|
||||
} catch (err) {
|
||||
// Don't wipe tokens on refresh failure — the refresh token may
|
||||
// still be valid and a retry could succeed (e.g. transient network
|
||||
// error). Only clear the expired access token so the next request
|
||||
// tries a fresh refresh instead of sending a stale token.
|
||||
sessionStorage.removeItem(TOKEN_KEY)
|
||||
sessionStorage.removeItem(EXPIRES_KEY)
|
||||
console.warn('Token refresh failed:', err)
|
||||
return null
|
||||
} finally {
|
||||
this.refreshPromise = null
|
||||
|
||||
@@ -115,15 +115,21 @@ export const useContentStore = defineStore('content', () => {
|
||||
|
||||
const films = allContent.filter(c => c.type === 'film')
|
||||
const bitcoinContent = allContent.filter(c =>
|
||||
c.categories?.some(cat => cat.toLowerCase().includes('bitcoin') || cat.toLowerCase().includes('documentary'))
|
||||
c.categories?.some(cat => cat.toLowerCase().includes('bitcoin'))
|
||||
)
|
||||
const docs = allContent.filter(c =>
|
||||
c.categories?.some(cat => cat.toLowerCase().includes('documentary'))
|
||||
)
|
||||
const dramaContent = allContent.filter(c =>
|
||||
c.categories?.some(cat => cat.toLowerCase().includes('drama'))
|
||||
)
|
||||
|
||||
contentRows.value = {
|
||||
featured: allContent.slice(0, 10),
|
||||
newReleases: films.slice(0, 8),
|
||||
bitcoin: bitcoinContent.length > 0 ? bitcoinContent : films.slice(0, 6),
|
||||
documentaries: allContent.slice(0, 10),
|
||||
dramas: films.slice(0, 6),
|
||||
documentaries: docs.length > 0 ? docs : allContent.slice(0, 10),
|
||||
dramas: dramaContent.length > 0 ? dramaContent : films.slice(0, 6),
|
||||
independent: films.slice(0, 10)
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -240,6 +246,7 @@ export const useContentStore = defineStore('content', () => {
|
||||
rentalPrice: p.film?.rentalPrice ?? p.rentalPrice,
|
||||
status: p.status,
|
||||
apiData: p,
|
||||
isOwnProject: true,
|
||||
}))
|
||||
|
||||
// Merge into each content row (prepend so they appear first)
|
||||
@@ -278,29 +285,39 @@ export const useContentStore = defineStore('content', () => {
|
||||
|
||||
/**
|
||||
* Main fetch content method.
|
||||
* When USE_MOCK is false and the self-hosted API URL is configured,
|
||||
* always try the self-hosted backend first (regardless of the
|
||||
* content-source toggle, which only affects mock catalogues).
|
||||
* Respects the content-source toggle:
|
||||
* - 'indeehub-api' → self-hosted backend API
|
||||
* - 'topdocfilms' → TopDoc mock catalog (YouTube documentaries)
|
||||
* - 'indeehub' → IndeeHub mock catalog
|
||||
*/
|
||||
async function fetchContent() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const sourceStore = useContentSourceStore()
|
||||
const apiUrl = import.meta.env.VITE_INDEEHUB_API_URL || ''
|
||||
|
||||
if (USE_MOCK_DATA) {
|
||||
// Use mock data in development or when flag is set
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
await fetchContentFromMock()
|
||||
} else if (apiUrl) {
|
||||
// Self-hosted backend is configured — always prefer it
|
||||
} else if (sourceStore.activeSource === 'indeehub-api' && apiUrl) {
|
||||
// Self-hosted backend API
|
||||
await fetchContentFromIndeehubApi()
|
||||
await mergePublishedFilmmakerProjects()
|
||||
} else if (sourceStore.activeSource === 'topdocfilms') {
|
||||
// TopDoc curated catalog (free YouTube documentaries)
|
||||
fetchTopDocMock()
|
||||
await mergePublishedFilmmakerProjects()
|
||||
} else if (sourceStore.activeSource === 'indeehub') {
|
||||
// IndeeHub mock catalog
|
||||
fetchIndeeHubMock()
|
||||
await mergePublishedFilmmakerProjects()
|
||||
} else if (apiUrl) {
|
||||
// Fallback to API if source is unknown but API is configured
|
||||
await fetchContentFromIndeehubApi()
|
||||
// Also merge filmmaker's published projects that may not be in the
|
||||
// public results yet (e.g. content still transcoding)
|
||||
await mergePublishedFilmmakerProjects()
|
||||
} else {
|
||||
// No self-hosted backend — try external API
|
||||
await fetchContentFromApi()
|
||||
await mergePublishedFilmmakerProjects()
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@ export interface Content {
|
||||
// Dual-mode content delivery
|
||||
deliveryMode?: 'native' | 'partner'
|
||||
keyUrl?: string
|
||||
|
||||
/** True when the logged-in user is the content creator/owner */
|
||||
isOwnProject?: boolean
|
||||
}
|
||||
|
||||
// Nostr event types
|
||||
|
||||
Reference in New Issue
Block a user