diff --git a/internal/app.go b/internal/app.go index 5ad9b21..b5a7855 100644 --- a/internal/app.go +++ b/internal/app.go @@ -102,7 +102,11 @@ func MainApp(configPath string) { log.Named("download_client"), ) - workerPool, err := ants.NewPool(config.GetNumberOfWorkers()) + workerCount := config.GetNumberOfWorkers() + if workerCount < 10 { + workerCount = 10 + } + workerPool, err := ants.NewPool(workerCount) if err != nil { zurglog.Errorf("Failed to create worker pool: %v", err) os.Exit(1) @@ -140,7 +144,7 @@ func MainApp(configPath string) { log.Named("repair"), ) - downloader := universal.NewDownloader(downloadClient) + downloader := universal.NewDownloader(downloadClient, workerPool) router := chi.NewRouter() handlers.AttachHandlers( diff --git a/internal/handlers/home.go b/internal/handlers/home.go index 4efe8f1..5a4cd6c 100644 --- a/internal/handlers/home.go +++ b/internal/handlers/home.go @@ -29,7 +29,9 @@ type RootResponse struct { Infuse string `json:"infuse"` Logs string `json:"logs"` UserInfo *realdebrid.User `json:"user_info"` - RDTrafficUsed uint64 `json:"rd_traffic_used"` + TrafficLogged uint64 `json:"traffic_logged"` + RequestedMB uint64 `json:"requested_mb"` + ServedMB uint64 `json:"served_mb"` LibrarySize int `json:"library_size"` // Number of torrents in the library TorrentsToRepair string `json:"repair_queue"` // List of torrents in the repair queue MemAlloc uint64 `json:"mem_alloc"` // Memory allocation in MB @@ -82,10 +84,10 @@ func (zr *Handlers) generateResponse(resp http.ResponseWriter, req *http.Request sort.Strings(sortedIDs) // check if real-debrid.com is in the traffic details - var rdTrafficUsed int64 - rdTrafficUsed = 0 + var trafficLogged int64 + trafficLogged = 0 if _, ok := trafficDetails["real-debrid.com"]; ok { - rdTrafficUsed = trafficDetails["real-debrid.com"] + trafficLogged = trafficDetails["real-debrid.com"] } userInfo.Premium = userInfo.Premium / 86400 @@ -101,7 +103,9 @@ func (zr *Handlers) generateResponse(resp http.ResponseWriter, req *http.Request Infuse: fmt.Sprintf("//%s/infuse/", req.Host), Logs: fmt.Sprintf("//%s/logs/", req.Host), UserInfo: userInfo, - RDTrafficUsed: bToGb(uint64(rdTrafficUsed)), + TrafficLogged: bToMb(uint64(trafficLogged)), + RequestedMB: bToMb(zr.downloader.RequestedBytes.Load()), + ServedMB: bToMb(zr.downloader.TotalBytes.Load()), LibrarySize: allTorrents.Count(), TorrentsToRepair: repairQueueStr, MemAlloc: bToMb(mem.Alloc), @@ -190,6 +194,12 @@ func (zr *Handlers) handleHome(resp http.ResponseWriter, req *http.Request) { response.Logs, ) + denominator := response.RequestedMB + if denominator == 0 { + denominator = 1 + } + efficiency := response.ServedMB / denominator + out += fmt.Sprintf(` Library Size @@ -216,8 +226,20 @@ func (zr *Handlers) handleHome(resp http.ResponseWriter, req *http.Request) { %d - RD Traffic Used - %d GB + Traffic Logged + %d MB + + + Traffic Requested + %d MB + + + Traffic Served + %d MB + + + Traffic Efficiency + %d%% (wasted %d MB) `, response.LibrarySize, response.MemAlloc, @@ -225,7 +247,11 @@ func (zr *Handlers) handleHome(resp http.ResponseWriter, req *http.Request) { response.Sys, response.NumGC, response.PID, - response.RDTrafficUsed, + response.TrafficLogged, + response.RequestedMB, + response.ServedMB, + efficiency, + response.RequestedMB-response.ServedMB, ) out += fmt.Sprintf(` @@ -467,7 +493,3 @@ func (zr *Handlers) handleHome(resp http.ResponseWriter, req *http.Request) { func bToMb(b uint64) uint64 { return b / 1024 / 1024 } - -func bToGb(b uint64) uint64 { - return b / 1024 / 1024 / 1024 -} diff --git a/internal/universal/downloader.go b/internal/universal/downloader.go index a3b4a2d..ea95ce2 100644 --- a/internal/universal/downloader.go +++ b/internal/universal/downloader.go @@ -2,26 +2,55 @@ package universal import ( "context" + "fmt" "io" "net/http" "path/filepath" + "strconv" "strings" + "sync/atomic" + "time" "github.com/debridmediamanager/zurg/internal/config" intTor "github.com/debridmediamanager/zurg/internal/torrent" zurghttp "github.com/debridmediamanager/zurg/pkg/http" "github.com/debridmediamanager/zurg/pkg/logutil" "github.com/debridmediamanager/zurg/pkg/realdebrid" + "github.com/panjf2000/ants/v2" ) type Downloader struct { - client *zurghttp.HTTPClient + client *zurghttp.HTTPClient + RequestedBytes atomic.Uint64 + TotalBytes atomic.Uint64 } -func NewDownloader(client *zurghttp.HTTPClient) *Downloader { - return &Downloader{ +func NewDownloader(client *zurghttp.HTTPClient, workerPool *ants.Pool) *Downloader { + dl := &Downloader{ client: client, } + + // track bandwidth usage and reset at 12AM CET + now := time.Now() + tomorrow := now.AddDate(0, 0, 1) + cetTZ, err := time.LoadLocation("CET") + if err != nil { + cetTZ = time.FixedZone("CET", 1*60*60) + } + nextMidnightInCET := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 0, 0, 0, 0, cetTZ) + duration := nextMidnightInCET.Sub(now) + timer := time.NewTimer(duration) + workerPool.Submit(func() { + <-timer.C + ticker := time.NewTicker(24 * time.Hour) + for { + dl.RequestedBytes.Store(0) + dl.TotalBytes.Store(0) + <-ticker.C + } + }) + + return dl } // DownloadFile handles a GET request for files in torrents @@ -146,7 +175,7 @@ func (dl *Downloader) streamFileToResponse( // Add the range header if it exists if req.Header.Get("Range") != "" { dlReq.Header.Add("Range", req.Header.Get("Range")) - // log.Debugf("Range request for file %s: %s", unrestrict.Download, req.Header.Get("Range")) + // log.Debugf("Serving file %s: %s", unrestrict.Filename, req.Header.Get("Range")) } // Perform the request @@ -182,9 +211,66 @@ func (dl *Downloader) streamFileToResponse( } buf := make([]byte, cfg.GetNetworkBufferSize()) - io.CopyBuffer(resp, downloadResp.Body, buf) + n, _ := io.CopyBuffer(resp, downloadResp.Body, buf) + + // Update the download statistics + reqBytes, _ := parseRangeHeader(req.Header.Get("Range")) + if reqBytes == 0 && unrestrict != nil { + reqBytes = uint64(unrestrict.Filesize) + } + dl.RequestedBytes.Add(reqBytes) + dl.TotalBytes.Add(uint64(n)) + log.Debugf("Served %d MB of the requested %d MB of file %s (range=%s)", bToMb(uint64(n)), bToMb(reqBytes), unrestrict.Filename, req.Header.Get("Range")) } func redirect(resp http.ResponseWriter, req *http.Request, url string) { http.Redirect(resp, req, url, http.StatusFound) } + +func parseRangeHeader(rangeHeader string) (uint64, error) { + if rangeHeader == "" { // Empty header means no range request + return 0, nil + } + + if !strings.HasPrefix(rangeHeader, "bytes=") { + return 0, fmt.Errorf("invalid range header format") + } + + parts := strings.SplitN(rangeHeader[6:], "-", 2) // [6:] removes "bytes=" + if len(parts) != 2 { + return 0, fmt.Errorf("invalid range specification") + } + + var start, end uint64 + var err error + + // Case 1: "bytes=100-" (from byte 100 to the end) + if parts[0] != "" { + start, err = strconv.ParseUint(parts[0], 10, 64) + if err != nil { + return 0, fmt.Errorf("invalid start value: %w", err) + } + } + + // Case 2: "bytes=-200" (last 200 bytes) + if parts[1] != "" { + end, err = strconv.ParseUint(parts[1], 10, 64) + if err != nil { + return 0, fmt.Errorf("invalid end value: %w", err) + } + } + + // Handle "bytes=500-100" (invalid range) + if start > end { + return 0, fmt.Errorf("invalid range: start cannot be greater than end") + } + + // Calculate bytes to read + bytesToRead := end - start + 1 // +1 because ranges are inclusive + + return bytesToRead, nil +} + +func bToMb(b uint64) uint64 { + return b / 1024 / 1024 +}