Adding processing skeleton
This commit is contained in:
parent
99594597db
commit
8223a3ac99
136
CLAUDE.md
136
CLAUDE.md
@ -4,45 +4,98 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
This is a Go-based HLS (HTTP Live Streaming) downloader that monitors M3U8 playlists and downloads video segments in real-time. The program takes a master M3U8 playlist URL, parses all available stream variants (different qualities/bitrates), and continuously monitors each variant's chunklist for new segments to download.
|
This is a Go-based HLS (HTTP Live Streaming) recorder that monitors M3U8 playlists and downloads video segments in real-time with automatic NAS transfer capabilities. The program takes a master M3U8 playlist URL, parses all available stream variants (different qualities/bitrates), continuously monitors each variant's chunklist for new segments, downloads them locally, and optionally transfers them to network storage for long-term archival.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
The project follows a modular architecture with clear separation of concerns:
|
The project follows a modular architecture with clear separation of concerns:
|
||||||
|
|
||||||
- **cmd/**: Entry points for different execution modes
|
- **cmd/**: Entry points for different execution modes
|
||||||
- **downloader/main.go**: Main downloader application that orchestrates variant downloading
|
- **main/main.go**: Primary CLI entry point with URL input and event naming
|
||||||
- **proc/main.go**: Alternative processor entry point (currently minimal)
|
- **downloader/download.go**: Core download orchestration logic with transfer service integration
|
||||||
|
- **processor/process.go**: Alternative processing entry point
|
||||||
- **pkg/**: Core packages containing the application logic
|
- **pkg/**: Core packages containing the application logic
|
||||||
- **media/**: HLS streaming and download logic
|
- **media/**: HLS streaming and download logic
|
||||||
- **stream.go**: Stream variant parsing and downloading orchestration (`GetAllVariants`, `VariantDownloader`)
|
- **stream.go**: Stream variant parsing and downloading orchestration (`GetAllVariants`, `VariantDownloader`)
|
||||||
- **playlist.go**: M3U8 playlist loading and parsing (`LoadMediaPlaylist`)
|
- **playlist.go**: M3U8 playlist loading and parsing (`LoadMediaPlaylist`)
|
||||||
- **segment.go**: Individual segment downloading logic (`DownloadSegment`, `SegmentJob`)
|
- **segment.go**: Individual segment downloading logic (`DownloadSegment`, `SegmentJob`)
|
||||||
- **constants/constants.go**: Configuration constants (URLs, timeouts, output paths)
|
- **manifest.go**: Manifest generation and segment tracking (`ManifestWriter`, `ManifestItem`)
|
||||||
|
- **transfer/**: NAS transfer system (complete implementation available)
|
||||||
|
- **service.go**: Transfer service orchestration
|
||||||
|
- **watcher.go**: File system monitoring for new downloads
|
||||||
|
- **queue.go**: Priority queue with worker pool management
|
||||||
|
- **nas.go**: NAS file transfer with retry logic
|
||||||
|
- **cleanup.go**: Local file cleanup after successful transfer
|
||||||
|
- **types.go**: Transfer system data structures
|
||||||
|
- **constants/constants.go**: Configuration constants (paths, timeouts, NAS settings)
|
||||||
- **httpClient/error.go**: HTTP error handling utilities
|
- **httpClient/error.go**: HTTP error handling utilities
|
||||||
|
|
||||||
## Core Functionality
|
## Core Functionality
|
||||||
|
|
||||||
The main workflow is:
|
### Download Workflow
|
||||||
1. **Parse Master Playlist**: `GetAllVariants()` fetches and parses the master M3U8 to extract all stream variants with different qualities/bitrates
|
1. **Parse Master Playlist**: `GetAllVariants()` fetches and parses the master M3U8 to extract all stream variants with different qualities/bitrates
|
||||||
2. **Concurrent Monitoring**: Each variant gets its own goroutine running `VariantDownloader()` that continuously polls for playlist updates
|
2. **Concurrent Monitoring**: Each variant gets its own goroutine running `VariantDownloader()` that continuously polls for playlist updates
|
||||||
3. **Segment Detection**: When new segments appear in a variant's playlist, they are queued for download
|
3. **Segment Detection**: When new segments appear in a variant's playlist, they are queued for download
|
||||||
4. **Parallel Downloads**: Segments are downloaded concurrently with configurable worker pools and retry logic
|
4. **Parallel Downloads**: Segments are downloaded concurrently with configurable worker pools and retry logic
|
||||||
5. **Quality Organization**: Downloaded segments are organized by resolution (1080p, 720p, etc.) in separate directories
|
5. **Quality Organization**: Downloaded segments are organized by resolution (1080p, 720p, etc.) in separate directories
|
||||||
|
6. **Manifest Generation**: `ManifestWriter` tracks all downloaded segments with sequence numbers and resolutions
|
||||||
|
|
||||||
|
### NAS Transfer Workflow (Optional)
|
||||||
|
1. **File Watching**: `FileWatcher` monitors download directories for new `.ts` files
|
||||||
|
2. **Transfer Queuing**: New files are added to a priority queue after a settling delay
|
||||||
|
3. **Background Transfer**: Worker pool transfers files to NAS with retry logic and verification
|
||||||
|
4. **Local Cleanup**: Successfully transferred files are automatically cleaned up locally
|
||||||
|
5. **State Persistence**: Queue state is persisted to survive crashes and restarts
|
||||||
|
|
||||||
## Key Data Structures
|
## Key Data Structures
|
||||||
|
|
||||||
- `StreamVariant`: Represents a stream quality variant with URL, bandwidth, resolution, and output directory
|
- `StreamVariant`: Represents a stream quality variant with URL, bandwidth, resolution, output directory, and manifest writer
|
||||||
- `SegmentJob`: Represents a segment download task with URI, sequence number, and variant info
|
- `SegmentJob`: Represents a segment download task with URI, sequence number, and variant info
|
||||||
|
- `ManifestWriter`: Tracks downloaded segments and generates JSON manifests
|
||||||
|
- `ManifestItem`: Individual segment record with sequence number and resolution
|
||||||
|
- `TransferItem`: Transfer queue item with source, destination, retry count, and status
|
||||||
|
- `TransferService`: Orchestrates file watching, queuing, transfer, and cleanup
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Key configuration is managed in `pkg/constants/constants.go`:
|
||||||
|
|
||||||
|
### Core Settings
|
||||||
|
- `WorkerCount`: Number of concurrent segment downloaders per variant (4)
|
||||||
|
- `RefreshDelay`: How often to check for playlist updates (3 seconds)
|
||||||
|
- `LocalOutputDirPath`: Base directory for local downloads (`./data/`)
|
||||||
|
- `ManifestPath`: Directory for manifest JSON files (`./data`)
|
||||||
|
|
||||||
|
### HTTP Settings
|
||||||
|
- `HTTPUserAgent`: User agent string for HTTP requests
|
||||||
|
- `REFERRER`: Referer header for HTTP requests
|
||||||
|
|
||||||
|
### NAS Transfer Settings
|
||||||
|
- `EnableNASTransfer`: Enable/disable automatic NAS transfer (true)
|
||||||
|
- `NASOutputPath`: UNC path to NAS storage (`\\HomeLabNAS\dci\streams`)
|
||||||
|
- `NASUsername`/`NASPassword`: NAS credentials (empty for current user)
|
||||||
|
- `TransferWorkerCount`: Concurrent transfer workers (2)
|
||||||
|
- `TransferRetryLimit`: Max retry attempts per file (3)
|
||||||
|
- `TransferTimeout`: Timeout per file transfer (30 seconds)
|
||||||
|
- `FileSettlingDelay`: Wait before queuing new files (5 seconds)
|
||||||
|
- `PersistencePath`: Transfer queue state file (`./data/transfer_queue.json`)
|
||||||
|
|
||||||
|
### Cleanup Settings
|
||||||
|
- `CleanupAfterTransfer`: Delete local files after NAS transfer (true)
|
||||||
|
- `CleanupBatchSize`: Files processed per cleanup batch (10)
|
||||||
|
- `RetainLocalHours`: Hours to keep local files (0 = immediate cleanup)
|
||||||
|
|
||||||
## Common Development Commands
|
## Common Development Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build the downloader application
|
# Build the main application
|
||||||
go build -o stream-recorder ./cmd/downloader
|
go build -o stream-recorder ./cmd/main
|
||||||
|
|
||||||
# Run the downloader
|
# Run with URL prompt
|
||||||
go run ./cmd/downloader/main.go
|
go run ./cmd/main/main.go
|
||||||
|
|
||||||
|
# Run with command line arguments
|
||||||
|
go run ./cmd/main/main.go -url="https://example.com/playlist.m3u8" -event="my-event" -debug=true
|
||||||
|
|
||||||
# Run with module support
|
# Run with module support
|
||||||
go mod tidy
|
go mod tidy
|
||||||
@ -54,24 +107,69 @@ go test ./...
|
|||||||
go fmt ./...
|
go fmt ./...
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Command Line Options
|
||||||
|
|
||||||
Key configuration is managed in `pkg/constants/constants.go`:
|
- `-url`: M3U8 playlist URL (if not provided, prompts for input)
|
||||||
- `MasterURL`: The master M3U8 playlist URL to monitor
|
- `-event`: Event name for organizing downloads (defaults to current date)
|
||||||
- `WorkerCount`: Number of concurrent segment downloaders per variant
|
- `-debug`: Debug mode (only downloads 1080p variant for easier testing)
|
||||||
- `RefreshDelay`: How often to check for playlist updates (3 seconds)
|
|
||||||
- `OutputDirPath`: Base directory for downloaded segments
|
|
||||||
- HTTP headers for requests (User-Agent, Referer)
|
|
||||||
|
|
||||||
## Monitoring and Downloads
|
## Monitoring and Downloads
|
||||||
|
|
||||||
The application implements real-time stream monitoring:
|
The application implements comprehensive real-time stream monitoring:
|
||||||
|
|
||||||
|
### Download Features
|
||||||
- **Continuous Polling**: Each variant playlist is checked every 3 seconds for new segments
|
- **Continuous Polling**: Each variant playlist is checked every 3 seconds for new segments
|
||||||
- **Deduplication**: Uses segment URIs and sequence numbers to avoid re-downloading
|
- **Deduplication**: Uses segment URIs and sequence numbers to avoid re-downloading
|
||||||
- **Graceful Shutdown**: Responds to SIGINT/SIGTERM signals for clean exit
|
- **Graceful Shutdown**: Responds to SIGINT/SIGTERM signals for clean exit
|
||||||
- **Error Resilience**: Retries failed downloads and handles HTTP 403 errors specially
|
- **Error Resilience**: Retries failed downloads and handles HTTP 403 errors specially
|
||||||
- **Quality Detection**: Automatically determines resolution from bandwidth or explicit resolution data
|
- **Quality Detection**: Automatically determines resolution from bandwidth or explicit resolution data
|
||||||
|
- **Context Cancellation**: Proper timeout and cancellation handling for clean shutdowns
|
||||||
|
|
||||||
|
### Transfer Features (when enabled)
|
||||||
|
- **Real-time Transfer**: Files are transferred to NAS as soon as they're downloaded
|
||||||
|
- **Queue Persistence**: Transfer queue survives application restarts
|
||||||
|
- **Retry Logic**: Failed transfers are retried with exponential backoff
|
||||||
|
- **Verification**: File sizes are verified after transfer
|
||||||
|
- **Automatic Cleanup**: Local files are removed after successful NAS transfer
|
||||||
|
- **Statistics Reporting**: Transfer progress and statistics are logged regularly
|
||||||
|
|
||||||
|
### Manifest Generation
|
||||||
|
- **Segment Tracking**: All downloaded segments are tracked with sequence numbers
|
||||||
|
- **Resolution Mapping**: Segments are associated with their quality variants
|
||||||
|
- **JSON Output**: Manifest files are generated as sorted JSON arrays for easy processing
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
The implementation uses proper Go error handling patterns with custom HTTP error types. Failed downloads are logged with clear status indicators (✓ for success, ✗ for failure).
|
The implementation uses proper Go error handling patterns:
|
||||||
|
- **Custom HTTP Errors**: Structured error types for HTTP failures
|
||||||
|
- **Context-Aware Cancellation**: Proper handling of shutdown scenarios
|
||||||
|
- **Retry Logic**: Exponential backoff for transient failures
|
||||||
|
- **Logging**: Clear status indicators (✓ for success, ✗ for failure)
|
||||||
|
- **Graceful Degradation**: Transfer service failures don't stop downloads
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `github.com/grafov/m3u8`: M3U8 playlist parsing
|
||||||
|
- `github.com/fsnotify/fsnotify`: File system event monitoring for NAS transfers
|
||||||
|
|
||||||
|
## Data Organization
|
||||||
|
|
||||||
|
Downloaded files are organized as:
|
||||||
|
```
|
||||||
|
./data/
|
||||||
|
├── {event-name}.json # Manifest file
|
||||||
|
├── {event-name}/ # Event-specific directory
|
||||||
|
│ ├── 1080p/ # High quality segments
|
||||||
|
│ ├── 720p/ # Medium quality segments
|
||||||
|
│ └── 480p/ # Lower quality segments
|
||||||
|
└── transfer_queue.json # Transfer queue state
|
||||||
|
```
|
||||||
|
|
||||||
|
NAS files mirror the local structure:
|
||||||
|
```
|
||||||
|
\\HomeLabNAS\dci\streams\
|
||||||
|
└── {event-name}/
|
||||||
|
├── 1080p/
|
||||||
|
├── 720p/
|
||||||
|
└── 480p/
|
||||||
|
```
|
||||||
@ -5,17 +5,32 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"m3u8-downloader/cmd/downloader"
|
"m3u8-downloader/cmd/downloader"
|
||||||
|
"m3u8-downloader/cmd/processor"
|
||||||
|
"m3u8-downloader/cmd/transfer"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
url := flag.String("url", "", "M3U8 playlist URL")
|
url := flag.String("url", "", "M3U8 playlist URL")
|
||||||
eventName := flag.String("event", time.Now().Format("2006-01-02"), "Event name")
|
eventName := flag.String("event", time.Now().Format("2006-01-02"), "Event name")
|
||||||
debug := flag.Bool("debug", false, "Enable debug mode")
|
debug := flag.Bool("debug", false, "Enable debug mode")
|
||||||
|
transferOnly := flag.Bool("transfer", false, "Transfer-only mode: transfer existing files without downloading")
|
||||||
|
processOnly := flag.Bool("process", false, "Process-only mode: process existing files without downloading")
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
if *transferOnly {
|
||||||
|
transfer.RunTransferOnly(*eventName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if *processOnly {
|
||||||
|
processor.Process()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if *url == "" {
|
if *url == "" {
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
fmt.Print("Enter M3U8 playlist URL: ")
|
fmt.Print("Enter M3U8 playlist URL: ")
|
||||||
@ -26,5 +41,4 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
downloader.Download(*url, *eventName, *debug)
|
downloader.Download(*url, *eventName, *debug)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,15 @@
|
|||||||
package processor
|
package processor
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"m3u8-downloader/pkg/processing"
|
||||||
|
)
|
||||||
|
|
||||||
func Process() {
|
func Process() {
|
||||||
fmt.Println("Process")
|
|
||||||
|
config := processing.ProcessConfig{
|
||||||
|
WorkerCount: 4,
|
||||||
|
DestinationPath: "/Users/andrey/Downloads",
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
processing.NewProcessingService(&config, nil).Start(nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ const (
|
|||||||
|
|
||||||
HTTPUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
|
HTTPUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
|
||||||
REFERRER = "https://www.flomarching.com"
|
REFERRER = "https://www.flomarching.com"
|
||||||
LocalOutputDirPath = "./data/"
|
LocalOutputDirPath = "../data/"
|
||||||
|
|
||||||
EnableNASTransfer = true
|
EnableNASTransfer = true
|
||||||
NASOutputPath = "\\\\HomeLabNAS\\dci\\streams"
|
NASOutputPath = "\\\\HomeLabNAS\\dci\\streams"
|
||||||
@ -18,12 +18,16 @@ const (
|
|||||||
TransferRetryLimit = 3
|
TransferRetryLimit = 3
|
||||||
TransferTimeout = 30 * time.Second
|
TransferTimeout = 30 * time.Second
|
||||||
FileSettlingDelay = 5 * time.Second
|
FileSettlingDelay = 5 * time.Second
|
||||||
PersistencePath = "./data/transfer_queue.json"
|
PersistencePath = "../data/transfer_queue.json"
|
||||||
TransferQueueSize = 1000
|
TransferQueueSize = 100000
|
||||||
BatchSize = 10
|
BatchSize = 10
|
||||||
ManifestPath = "./data"
|
ManifestPath = "../data"
|
||||||
|
|
||||||
CleanupAfterTransfer = true
|
CleanupAfterTransfer = true
|
||||||
CleanupBatchSize = 10
|
CleanupBatchSize = 10
|
||||||
RetainLocalHours = 0
|
RetainLocalHours = 0
|
||||||
|
|
||||||
|
AutoProcess = true
|
||||||
|
ProcessingEnabled = true
|
||||||
|
ProcessWorkerCount = 2
|
||||||
)
|
)
|
||||||
|
|||||||
12
pkg/nas/config.go
Normal file
12
pkg/nas/config.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package nas
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type NASConfig struct {
|
||||||
|
Path string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
Timeout time.Duration
|
||||||
|
RetryLimit int
|
||||||
|
VerifySize bool
|
||||||
|
}
|
||||||
41
pkg/processing/service.go
Normal file
41
pkg/processing/service.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package processing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"m3u8-downloader/pkg/nas"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProcessingService struct {
|
||||||
|
config *ProcessConfig
|
||||||
|
nas *nas.NASConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProcessingService(config *ProcessConfig, nas *nas.NASConfig) *ProcessingService {
|
||||||
|
return &ProcessingService{
|
||||||
|
config: config,
|
||||||
|
nas: nas,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *ProcessingService) Start(ctx context.Context) error {
|
||||||
|
if !ps.config.Enabled {
|
||||||
|
log.Println("Processing service disabled")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
}()
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *ProcessingService) ProcessEvent(eventName string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
11
pkg/processing/types.go
Normal file
11
pkg/processing/types.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package processing
|
||||||
|
|
||||||
|
type ProcessJob struct {
|
||||||
|
EventName string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProcessConfig struct {
|
||||||
|
WorkerCount int
|
||||||
|
DestinationPath string
|
||||||
|
Enabled bool
|
||||||
|
}
|
||||||
@ -5,16 +5,17 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"m3u8-downloader/pkg/nas"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NASTransfer struct {
|
type NASTransfer struct {
|
||||||
config NASConfig
|
config nas.NASConfig
|
||||||
connected bool
|
connected bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNASTransfer(config NASConfig) *NASTransfer {
|
func NewNASTransfer(config nas.NASConfig) *NASTransfer {
|
||||||
nt := &NASTransfer{
|
nt := &NASTransfer{
|
||||||
config: config,
|
config: config,
|
||||||
}
|
}
|
||||||
@ -127,3 +128,37 @@ func (nt *NASTransfer) TestConnection() error {
|
|||||||
func (nt *NASTransfer) IsConnected() bool {
|
func (nt *NASTransfer) IsConnected() bool {
|
||||||
return nt.connected
|
return nt.connected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FileExists checks if a file already exists on the NAS and optionally verifies size
|
||||||
|
func (nt *NASTransfer) FileExists(destinationPath string, expectedSize int64) (bool, error) {
|
||||||
|
fullDestPath := filepath.Join(nt.config.Path, destinationPath)
|
||||||
|
|
||||||
|
destInfo, err := os.Stat(fullDestPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return false, nil // File doesn't exist, no error
|
||||||
|
}
|
||||||
|
return false, fmt.Errorf("failed to stat NAS file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// File exists, check size if expected size is provided
|
||||||
|
if expectedSize > 0 && destInfo.Size() != expectedSize {
|
||||||
|
log.Printf("NAS file size mismatch for %s: expected=%d, actual=%d",
|
||||||
|
fullDestPath, expectedSize, destInfo.Size())
|
||||||
|
return false, nil // File exists but wrong size, treat as not existing
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFileSize returns the size of a file on the NAS
|
||||||
|
func (nt *NASTransfer) GetFileSize(destinationPath string) (int64, error) {
|
||||||
|
fullDestPath := filepath.Join(nt.config.Path, destinationPath)
|
||||||
|
|
||||||
|
destInfo, err := os.Stat(fullDestPath)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to stat NAS file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return destInfo.Size(), nil
|
||||||
|
}
|
||||||
|
|||||||
@ -145,6 +145,24 @@ func (tq *TransferQueue) dispatchWork() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (tq *TransferQueue) processItem(ctx context.Context, item TransferItem) {
|
func (tq *TransferQueue) processItem(ctx context.Context, item TransferItem) {
|
||||||
|
// Check if file already exists on NAS before attempting transfer
|
||||||
|
if exists, err := tq.nasTransfer.FileExists(item.DestinationPath, item.FileSize); err != nil {
|
||||||
|
log.Printf("Failed to check if file exists on NAS for %s: %v", item.SourcePath, err)
|
||||||
|
// Continue with transfer attempt on error
|
||||||
|
} else if exists {
|
||||||
|
log.Printf("File already exists on NAS, skipping transfer: %s", item.SourcePath)
|
||||||
|
item.Status = StatusCompleted
|
||||||
|
tq.stats.IncrementCompleted(item.FileSize)
|
||||||
|
|
||||||
|
// Schedule for cleanup
|
||||||
|
if tq.cleanup != nil {
|
||||||
|
if err := tq.cleanup.ScheduleCleanup(item.SourcePath); err != nil {
|
||||||
|
log.Printf("Failed to schedule cleanup for existing file %s: %v", item.SourcePath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
maxRetries := 3
|
maxRetries := 3
|
||||||
|
|
||||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||||
|
|||||||
@ -5,6 +5,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"m3u8-downloader/pkg/constants"
|
"m3u8-downloader/pkg/constants"
|
||||||
|
nas2 "m3u8-downloader/pkg/nas"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -18,8 +22,8 @@ type TransferService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewTrasferService(outputDir string, eventName string) (*TransferService, error) {
|
func NewTrasferService(outputDir string, eventName string) (*TransferService, error) {
|
||||||
nasConfig := NASConfig{
|
nasConfig := nas2.NASConfig{
|
||||||
Path: outputDir + "/" + eventName,
|
Path: outputDir,
|
||||||
Username: constants.NASUsername,
|
Username: constants.NASUsername,
|
||||||
Password: constants.NASPassword,
|
Password: constants.NASPassword,
|
||||||
Timeout: constants.TransferTimeout,
|
Timeout: constants.TransferTimeout,
|
||||||
@ -34,9 +38,9 @@ func NewTrasferService(outputDir string, eventName string) (*TransferService, er
|
|||||||
|
|
||||||
cleanupConfig := CleanupConfig{
|
cleanupConfig := CleanupConfig{
|
||||||
Enabled: constants.CleanupAfterTransfer,
|
Enabled: constants.CleanupAfterTransfer,
|
||||||
RetentionPeriod: constants.RetainLocalHours,
|
RetentionPeriod: time.Duration(constants.RetainLocalHours) * time.Hour,
|
||||||
BatchSize: constants.CleanupBatchSize,
|
BatchSize: constants.CleanupBatchSize,
|
||||||
CheckInterval: constants.FileSettlingDelay,
|
CheckInterval: 1 * time.Minute,
|
||||||
}
|
}
|
||||||
cleanup := NewCleanupService(cleanupConfig)
|
cleanup := NewCleanupService(cleanupConfig)
|
||||||
|
|
||||||
@ -48,7 +52,13 @@ func NewTrasferService(outputDir string, eventName string) (*TransferService, er
|
|||||||
}
|
}
|
||||||
queue := NewTransferQueue(queueConfig, nas, cleanup)
|
queue := NewTransferQueue(queueConfig, nas, cleanup)
|
||||||
|
|
||||||
watcher, err := NewFileWatcher(constants.LocalOutputDirPath+"/"+eventName, queue)
|
// Create local output directory if it doesn't exist
|
||||||
|
localOutputPath := constants.LocalOutputDirPath + "/" + eventName
|
||||||
|
if err := os.MkdirAll(localOutputPath, 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to create local output directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
watcher, err := NewFileWatcher(localOutputPath, queue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to create file watcher: %w", err)
|
return nil, fmt.Errorf("Failed to create file watcher: %w", err)
|
||||||
}
|
}
|
||||||
@ -134,3 +144,102 @@ func (ts *TransferService) Shutdown(ctx context.Context) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QueueExistingFiles scans a directory for .ts files and queues them for transfer
|
||||||
|
func (ts *TransferService) QueueExistingFiles(localEventPath string) error {
|
||||||
|
log.Printf("Scanning for existing files in: %s", localEventPath)
|
||||||
|
|
||||||
|
var fileCount, alreadyTransferred, scheduledForCleanup int
|
||||||
|
|
||||||
|
// Extract event name from path for NAS destination
|
||||||
|
eventName := filepath.Base(localEventPath)
|
||||||
|
|
||||||
|
err := filepath.Walk(localEventPath, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error accessing path %s: %v", path, err)
|
||||||
|
return nil // Continue walking
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only process .ts files
|
||||||
|
if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), ".ts") {
|
||||||
|
// Extract resolution from directory path
|
||||||
|
resolution := ts.extractResolutionFromPath(path)
|
||||||
|
|
||||||
|
// Get relative path from event directory
|
||||||
|
relPath, err := filepath.Rel(localEventPath, path)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to get relative path for %s: %v", path, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build NAS destination path (eventName/relPath)
|
||||||
|
nasDestPath := filepath.Join(eventName, relPath)
|
||||||
|
|
||||||
|
// Check if file already exists on NAS with matching size
|
||||||
|
exists, err := ts.nas.FileExists(nasDestPath, info.Size())
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to check NAS file existence for %s: %v", path, err)
|
||||||
|
// Continue with transfer attempt on error
|
||||||
|
} else if exists {
|
||||||
|
log.Printf("File already exists on NAS: %s (%s, %d bytes)", path, resolution, info.Size())
|
||||||
|
alreadyTransferred++
|
||||||
|
|
||||||
|
// Schedule for cleanup if cleanup is enabled
|
||||||
|
if constants.CleanupAfterTransfer {
|
||||||
|
if err := ts.cleanup.ScheduleCleanup(path); err != nil {
|
||||||
|
log.Printf("Failed to schedule cleanup for already-transferred file %s: %v", path, err)
|
||||||
|
} else {
|
||||||
|
scheduledForCleanup++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil // Skip queuing this file
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create transfer item
|
||||||
|
item := TransferItem{
|
||||||
|
ID: ts.generateTransferID(),
|
||||||
|
SourcePath: path,
|
||||||
|
DestinationPath: nasDestPath,
|
||||||
|
Resolution: resolution,
|
||||||
|
Timestamp: info.ModTime(),
|
||||||
|
Status: StatusPending,
|
||||||
|
FileSize: info.Size(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to queue
|
||||||
|
if err := ts.queue.Add(item); err != nil {
|
||||||
|
log.Printf("Failed to queue file %s: %v", path, err)
|
||||||
|
} else {
|
||||||
|
log.Printf("Queued file: %s (%s, %d bytes)", path, resolution, info.Size())
|
||||||
|
fileCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to walk directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("File scan completed - Queued: %d, Already transferred: %d, Scheduled for cleanup: %d",
|
||||||
|
fileCount, alreadyTransferred, scheduledForCleanup)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TransferService) extractResolutionFromPath(filePath string) string {
|
||||||
|
dir := filepath.Dir(filePath)
|
||||||
|
parts := strings.Split(dir, string(filepath.Separator))
|
||||||
|
|
||||||
|
for _, part := range parts {
|
||||||
|
if strings.HasSuffix(part, "p") {
|
||||||
|
return part
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TransferService) generateTransferID() string {
|
||||||
|
return fmt.Sprintf("transfer_existing_%d", time.Now().UnixNano())
|
||||||
|
}
|
||||||
|
|||||||
@ -44,15 +44,6 @@ func (s TransferStatus) String() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type NASConfig struct {
|
|
||||||
Path string
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
Timeout time.Duration
|
|
||||||
RetryLimit int
|
|
||||||
VerifySize bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type QueueConfig struct {
|
type QueueConfig struct {
|
||||||
WorkerCount int
|
WorkerCount int
|
||||||
PersistencePath string
|
PersistencePath string
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"m3u8-downloader/pkg/constants"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -32,7 +33,7 @@ func NewFileWatcher(outputDir string, queue *TransferQueue) (*FileWatcher, error
|
|||||||
outputDir: outputDir,
|
outputDir: outputDir,
|
||||||
queue: queue,
|
queue: queue,
|
||||||
watcher: watcher,
|
watcher: watcher,
|
||||||
settingDelay: time.Second,
|
settingDelay: constants.FileSettlingDelay,
|
||||||
pendingFiles: make(map[string]*time.Timer),
|
pendingFiles: make(map[string]*time.Timer),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user