feat(usenet): implement full NNTP download pipeline
Complete usenet download support for unarr CLI: - NZB XML parser with password extraction from <head> meta - yEnc decoder with CRC32 verification - NNTP client with TLS, auth, and connection pool (up to 10 conns) - Segment downloader with parallel workers and progress reporting - Post-processing: par2 verify/repair, unrar/7z extraction with password support - Agent client methods: SearchNzbs, DownloadNzb, GetUsenetCredentials - UsenetDownloader implementing full Downloader interface - Daemon wiring: UsenetDownloader passed to Manager E2E tested: Oppenheimer 1080p (2.94 GB) downloaded via NNTP in 77.6s.
This commit is contained in:
parent
5f337eebd7
commit
e332c0a6e4
15 changed files with 3016 additions and 23 deletions
65
internal/usenet/postprocess/par2.go
Normal file
65
internal/usenet/postprocess/par2.go
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
package postprocess
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Par2Available checks if par2cmdline is installed.
|
||||
func Par2Available() bool {
|
||||
_, err := exec.LookPath("par2")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Par2Verify verifies files using a par2 file.
|
||||
// Returns nil if verification passes, error otherwise.
|
||||
func Par2Verify(par2File string) error {
|
||||
if !Par2Available() {
|
||||
log.Printf("[usenet] par2 not installed, skipping verification")
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := exec.Command("par2", "verify", par2File)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
outStr := string(output)
|
||||
// Check if repair is possible
|
||||
if strings.Contains(outStr, "Repair is possible") {
|
||||
return &Par2RepairableError{Par2File: par2File}
|
||||
}
|
||||
if strings.Contains(outStr, "Repair is not possible") {
|
||||
return fmt.Errorf("par2: verification failed and repair not possible:\n%s", outStr)
|
||||
}
|
||||
return fmt.Errorf("par2 verify: %w\n%s", err, outStr)
|
||||
}
|
||||
|
||||
log.Printf("[usenet] par2: verification OK")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Par2Repair attempts to repair files using par2 parity data.
|
||||
func Par2Repair(par2File string) error {
|
||||
if !Par2Available() {
|
||||
return fmt.Errorf("par2 not installed")
|
||||
}
|
||||
|
||||
cmd := exec.Command("par2", "repair", par2File)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("par2 repair: %w\n%s", err, output)
|
||||
}
|
||||
|
||||
log.Printf("[usenet] par2: repair successful")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Par2RepairableError indicates verification failed but repair is possible.
|
||||
type Par2RepairableError struct {
|
||||
Par2File string
|
||||
}
|
||||
|
||||
func (e *Par2RepairableError) Error() string {
|
||||
return fmt.Sprintf("par2: verification failed, repair possible: %s", e.Par2File)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue