- Add more indepth stats like number of torrents, profile details etc
- Add torrent ingest endpoints - Add issue template
This commit is contained in:
@@ -0,0 +1,76 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: 'Report a new bug'
|
||||||
|
labels: ['Type: Bug', 'Status: Needs Triage']
|
||||||
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Is there an existing issue for this?
|
||||||
|
description: Please search to see if an open or closed issue already exists for the bug you encountered. If a bug exists and is closed note that it may only be fixed in an unstable branch.
|
||||||
|
options:
|
||||||
|
- label: I have searched the existing open and closed issues
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Current Behavior
|
||||||
|
description: A concise description of what you're experiencing.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Expected Behavior
|
||||||
|
description: A concise description of what you expected to happen.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Steps To Reproduce
|
||||||
|
description: Steps to reproduce the behavior.
|
||||||
|
placeholder: |
|
||||||
|
1. In this environment...
|
||||||
|
2. With this config...
|
||||||
|
3. Run '...'
|
||||||
|
4. See error...
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Environment
|
||||||
|
description: |
|
||||||
|
examples:
|
||||||
|
- **OS**: Ubuntu 20.04
|
||||||
|
- **Version**: v1.0.0
|
||||||
|
- **Docker Install**: Yes
|
||||||
|
- **Browser**: Firefox 90 (If UI related)
|
||||||
|
value: |
|
||||||
|
- OS:
|
||||||
|
- Version:
|
||||||
|
- Docker Install:
|
||||||
|
- Browser:
|
||||||
|
render: markdown
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: What branch are you running?
|
||||||
|
options:
|
||||||
|
- Main/Latest
|
||||||
|
- Beta
|
||||||
|
- Experimental
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Trace Logs? **Not Optional**
|
||||||
|
description: |
|
||||||
|
Trace Logs
|
||||||
|
- are **required** for bug reports
|
||||||
|
- are not optional
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Trace Logs have been provided as applicable
|
||||||
|
description: Trace logs are **generally required** and are not optional for all bug reports and contain `trace`. Info logs are invalid for bug reports and do not contain `debug` nor `trace`
|
||||||
|
options:
|
||||||
|
- label: I have read and followed the steps in the wiki link above and provided the required trace logs - the logs contain `trace` - that are relevant and show this issue.
|
||||||
|
required: true
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: 'Suggest an idea for Decypharr'
|
||||||
|
labels: ['Type: Feature Request', 'Status: Needs Triage']
|
||||||
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Is there an existing issue for this?
|
||||||
|
description: Please search to see if an open or closed issue already exists for the feature you are requesting. If a request exists and is closed note that it may only be fixed in an unstable branch.
|
||||||
|
options:
|
||||||
|
- label: I have searched the existing open and closed issues
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Is your feature request related to a problem? Please describe
|
||||||
|
description: A clear and concise description of what the problem is.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Describe the solution you'd like
|
||||||
|
description: A clear and concise description of what you want to happen.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Describe alternatives you've considered
|
||||||
|
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Anything else?
|
||||||
|
description: |
|
||||||
|
Links? References? Mockups? Anything that will give us more context about the feature you are encountering!
|
||||||
|
|
||||||
|
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
@@ -36,14 +36,9 @@ services:
|
|||||||
container_name: decypharr
|
container_name: decypharr
|
||||||
ports:
|
ports:
|
||||||
- "8282:8282" # qBittorrent
|
- "8282:8282" # qBittorrent
|
||||||
user: "1000:1000"
|
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/:/mnt
|
- /mnt/:/mnt
|
||||||
- ./configs/:/app # config.json must be in this directory
|
- ./configs/:/app # config.json must be in this directory
|
||||||
environment:
|
|
||||||
- PUID=1000
|
|
||||||
- PGID=1000
|
|
||||||
- UMASK=002
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -52,14 +52,9 @@ services:
|
|||||||
container_name: decypharr
|
container_name: decypharr
|
||||||
ports:
|
ports:
|
||||||
- "8282:8282"
|
- "8282:8282"
|
||||||
user: "1000:1000"
|
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/:/mnt # Mount your media directory
|
- /mnt/:/mnt # Mount your media directory
|
||||||
- ./config/:/app # config.json must be in this directory
|
- ./config/:/app # config.json must be in this directory
|
||||||
environment:
|
|
||||||
- PUID=1000
|
|
||||||
- PGID=1000
|
|
||||||
- UMASK=002
|
|
||||||
- QBIT_PORT=8282 # qBittorrent Port (optional)
|
- QBIT_PORT=8282 # qBittorrent Port (optional)
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
```
|
```
|
||||||
@@ -69,6 +64,13 @@ Run the Docker Compose setup:
|
|||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Notes for Docker Users
|
||||||
|
|
||||||
|
- Ensure that the `/mnt/` directory is mounted correctly to access your media files.
|
||||||
|
- The `./config/` directory should contain your `config.json` file.
|
||||||
|
- You can adjust the `PUID` and `PGID` environment variables to match your user and group IDs for proper file permissions.
|
||||||
|
- The `UMASK` environment variable can be set to control file permissions created by Decypharr.
|
||||||
|
|
||||||
|
|
||||||
## Binary Installation
|
## Binary Installation
|
||||||
If you prefer not to use Docker, you can download and run the binary directly.
|
If you prefer not to use Docker, you can download and run the binary directly.
|
||||||
@@ -107,10 +109,4 @@ You can also configure Decypharr through the web interface, but it's recommended
|
|||||||
"log_level": "info",
|
"log_level": "info",
|
||||||
"port": "8282"
|
"port": "8282"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Few Notes
|
|
||||||
|
|
||||||
- Make sure decypharr has access to the directories specified in the configuration file.
|
|
||||||
- Ensure decypharr have write permissions to the qbittorrent download folder.
|
|
||||||
- Make sure decypharr can write to the `./config/` directory.
|
|
||||||
@@ -2,7 +2,6 @@ package request
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"compress/gzip"
|
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -383,31 +382,6 @@ func JSONResponse(w http.ResponseWriter, data interface{}, code int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Gzip(body []byte) []byte {
|
|
||||||
if len(body) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the pool is nil
|
|
||||||
buf := bytes.NewBuffer(make([]byte, 0, len(body)))
|
|
||||||
|
|
||||||
gz, err := gzip.NewWriterLevel(buf, gzip.BestSpeed)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := gz.Write(body); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := gz.Close(); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
result := make([]byte, buf.Len())
|
|
||||||
copy(result, buf.Bytes())
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func Default() *Client {
|
func Default() *Client {
|
||||||
once.Do(func() {
|
once.Do(func() {
|
||||||
instance = New()
|
instance = New()
|
||||||
@@ -435,7 +409,7 @@ func isRetryableError(err error) bool {
|
|||||||
var netErr net.Error
|
var netErr net.Error
|
||||||
if errors.As(err, &netErr) {
|
if errors.As(err, &netErr) {
|
||||||
// Retry on timeout errors and temporary errors
|
// Retry on timeout errors and temporary errors
|
||||||
return netErr.Timeout() || netErr.Temporary()
|
return netErr.Timeout()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not a retryable error
|
// Not a retryable error
|
||||||
|
|||||||
@@ -31,7 +31,11 @@ type AllDebrid struct {
|
|||||||
addSamples bool
|
addSamples bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(dc config.Debrid) *AllDebrid {
|
func (ad *AllDebrid) GetProfile() (*types.Profile, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(dc config.Debrid) (*AllDebrid, error) {
|
||||||
rl := request.ParseRateLimit(dc.RateLimit)
|
rl := request.ParseRateLimit(dc.RateLimit)
|
||||||
|
|
||||||
headers := map[string]string{
|
headers := map[string]string{
|
||||||
@@ -65,7 +69,7 @@ func New(dc config.Debrid) *AllDebrid {
|
|||||||
logger: logger.New(dc.Name),
|
logger: logger.New(dc.Name),
|
||||||
checkCached: dc.CheckCached,
|
checkCached: dc.CheckCached,
|
||||||
addSamples: dc.AddSamples,
|
addSamples: dc.AddSamples,
|
||||||
}
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ad *AllDebrid) GetName() string {
|
func (ad *AllDebrid) GetName() string {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func createDebridClient(dc config.Debrid) types.Client {
|
func createDebridClient(dc config.Debrid) (types.Client, error) {
|
||||||
switch dc.Name {
|
switch dc.Name {
|
||||||
case "realdebrid":
|
case "realdebrid":
|
||||||
return realdebrid.New(dc)
|
return realdebrid.New(dc)
|
||||||
|
|||||||
@@ -52,6 +52,12 @@ func (c *downloadLinkCache) Delete(key string) {
|
|||||||
delete(c.data, key)
|
delete(c.data, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *downloadLinkCache) Len() int {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
return len(c.data)
|
||||||
|
}
|
||||||
|
|
||||||
type downloadLinkRequest struct {
|
type downloadLinkRequest struct {
|
||||||
result string
|
result string
|
||||||
err error
|
err error
|
||||||
@@ -245,3 +251,7 @@ func (c *Cache) GetDownloadByteRange(torrentName, filename string) (*[2]int64, e
|
|||||||
file := ct.Files[filename]
|
file := ct.Files[filename]
|
||||||
return file.ByteRange, nil
|
return file.ByteRange, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Cache) GetTotalActiveDownloadLinks() int {
|
||||||
|
return c.downloadLinks.Len()
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package debrid
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/sirrobot01/decypharr/internal/config"
|
"github.com/sirrobot01/decypharr/internal/config"
|
||||||
|
"github.com/sirrobot01/decypharr/internal/logger"
|
||||||
"github.com/sirrobot01/decypharr/pkg/debrid/types"
|
"github.com/sirrobot01/decypharr/pkg/debrid/types"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
@@ -10,7 +11,7 @@ type Engine struct {
|
|||||||
Clients map[string]types.Client
|
Clients map[string]types.Client
|
||||||
clientsMu sync.Mutex
|
clientsMu sync.Mutex
|
||||||
Caches map[string]*Cache
|
Caches map[string]*Cache
|
||||||
CacheMu sync.Mutex
|
cacheMu sync.Mutex
|
||||||
LastUsed string
|
LastUsed string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,16 +19,22 @@ func NewEngine() *Engine {
|
|||||||
cfg := config.Get()
|
cfg := config.Get()
|
||||||
clients := make(map[string]types.Client)
|
clients := make(map[string]types.Client)
|
||||||
|
|
||||||
|
_logger := logger.Default()
|
||||||
|
|
||||||
caches := make(map[string]*Cache)
|
caches := make(map[string]*Cache)
|
||||||
|
|
||||||
for _, dc := range cfg.Debrids {
|
for _, dc := range cfg.Debrids {
|
||||||
client := createDebridClient(dc)
|
client, err := createDebridClient(dc)
|
||||||
logger := client.GetLogger()
|
if err != nil {
|
||||||
|
_logger.Error().Err(err).Str("Debrid", dc.Name).Msg("failed to connect to debrid client")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_log := client.GetLogger()
|
||||||
if dc.UseWebDav {
|
if dc.UseWebDav {
|
||||||
caches[dc.Name] = New(dc, client)
|
caches[dc.Name] = New(dc, client)
|
||||||
logger.Info().Msg("Debrid Service started with WebDAV")
|
_log.Info().Msg("Debrid Service started with WebDAV")
|
||||||
} else {
|
} else {
|
||||||
logger.Info().Msg("Debrid Service started")
|
_log.Info().Msg("Debrid Service started")
|
||||||
}
|
}
|
||||||
clients[dc.Name] = client
|
clients[dc.Name] = client
|
||||||
}
|
}
|
||||||
@@ -51,9 +58,9 @@ func (d *Engine) Reset() {
|
|||||||
d.Clients = make(map[string]types.Client)
|
d.Clients = make(map[string]types.Client)
|
||||||
d.clientsMu.Unlock()
|
d.clientsMu.Unlock()
|
||||||
|
|
||||||
d.CacheMu.Lock()
|
d.cacheMu.Lock()
|
||||||
d.Caches = make(map[string]*Cache)
|
d.Caches = make(map[string]*Cache)
|
||||||
d.CacheMu.Unlock()
|
d.cacheMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Engine) GetDebrids() map[string]types.Client {
|
func (d *Engine) GetDebrids() map[string]types.Client {
|
||||||
|
|||||||
@@ -25,3 +25,18 @@ func mergeFiles(torrents ...CachedTorrent) map[string]types.File {
|
|||||||
}
|
}
|
||||||
return merged
|
return merged
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Cache) GetIngests() ([]types.IngestData, error) {
|
||||||
|
torrents := c.GetTorrents()
|
||||||
|
debridName := c.client.GetName()
|
||||||
|
var ingests []types.IngestData
|
||||||
|
for _, torrent := range torrents {
|
||||||
|
ingests = append(ingests, types.IngestData{
|
||||||
|
Debrid: debridName,
|
||||||
|
Name: torrent.Filename,
|
||||||
|
Hash: torrent.InfoHash,
|
||||||
|
Size: torrent.Bytes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return ingests, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,6 +31,48 @@ type DebridLink struct {
|
|||||||
addSamples bool
|
addSamples bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func New(dc config.Debrid) (*DebridLink, error) {
|
||||||
|
rl := request.ParseRateLimit(dc.RateLimit)
|
||||||
|
|
||||||
|
headers := map[string]string{
|
||||||
|
"Authorization": fmt.Sprintf("Bearer %s", dc.APIKey),
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
_log := logger.New(dc.Name)
|
||||||
|
client := request.New(
|
||||||
|
request.WithHeaders(headers),
|
||||||
|
request.WithLogger(_log),
|
||||||
|
request.WithRateLimiter(rl),
|
||||||
|
request.WithProxy(dc.Proxy),
|
||||||
|
)
|
||||||
|
|
||||||
|
accounts := make(map[string]types.Account)
|
||||||
|
for idx, key := range dc.DownloadAPIKeys {
|
||||||
|
id := strconv.Itoa(idx)
|
||||||
|
accounts[id] = types.Account{
|
||||||
|
Name: key,
|
||||||
|
ID: id,
|
||||||
|
Token: key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &DebridLink{
|
||||||
|
Name: "debridlink",
|
||||||
|
Host: "https://debrid-link.com/api/v2",
|
||||||
|
APIKey: dc.APIKey,
|
||||||
|
accounts: accounts,
|
||||||
|
DownloadUncached: dc.DownloadUncached,
|
||||||
|
client: client,
|
||||||
|
MountPath: dc.Folder,
|
||||||
|
logger: logger.New(dc.Name),
|
||||||
|
checkCached: dc.CheckCached,
|
||||||
|
addSamples: dc.AddSamples,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dl *DebridLink) GetProfile() (*types.Profile, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (dl *DebridLink) GetName() string {
|
func (dl *DebridLink) GetName() string {
|
||||||
return dl.Name
|
return dl.Name
|
||||||
}
|
}
|
||||||
@@ -335,44 +377,6 @@ func (dl *DebridLink) GetDownloadUncached() bool {
|
|||||||
return dl.DownloadUncached
|
return dl.DownloadUncached
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(dc config.Debrid) *DebridLink {
|
|
||||||
rl := request.ParseRateLimit(dc.RateLimit)
|
|
||||||
|
|
||||||
headers := map[string]string{
|
|
||||||
"Authorization": fmt.Sprintf("Bearer %s", dc.APIKey),
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
_log := logger.New(dc.Name)
|
|
||||||
client := request.New(
|
|
||||||
request.WithHeaders(headers),
|
|
||||||
request.WithLogger(_log),
|
|
||||||
request.WithRateLimiter(rl),
|
|
||||||
request.WithProxy(dc.Proxy),
|
|
||||||
)
|
|
||||||
|
|
||||||
accounts := make(map[string]types.Account)
|
|
||||||
for idx, key := range dc.DownloadAPIKeys {
|
|
||||||
id := strconv.Itoa(idx)
|
|
||||||
accounts[id] = types.Account{
|
|
||||||
Name: key,
|
|
||||||
ID: id,
|
|
||||||
Token: key,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &DebridLink{
|
|
||||||
Name: "debridlink",
|
|
||||||
Host: "https://debrid-link.com/api/v2",
|
|
||||||
APIKey: dc.APIKey,
|
|
||||||
accounts: accounts,
|
|
||||||
DownloadUncached: dc.DownloadUncached,
|
|
||||||
client: client,
|
|
||||||
MountPath: dc.Folder,
|
|
||||||
logger: logger.New(dc.Name),
|
|
||||||
checkCached: dc.CheckCached,
|
|
||||||
addSamples: dc.AddSamples,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dl *DebridLink) GetTorrents() ([]*types.Torrent, error) {
|
func (dl *DebridLink) GetTorrents() ([]*types.Torrent, error) {
|
||||||
page := 0
|
page := 0
|
||||||
perPage := 100
|
perPage := 100
|
||||||
|
|||||||
@@ -45,9 +45,10 @@ type RealDebrid struct {
|
|||||||
rarSemaphore chan struct{}
|
rarSemaphore chan struct{}
|
||||||
checkCached bool
|
checkCached bool
|
||||||
addSamples bool
|
addSamples bool
|
||||||
|
Profile *types.Profile
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(dc config.Debrid) *RealDebrid {
|
func New(dc config.Debrid) (*RealDebrid, error) {
|
||||||
rl := request.ParseRateLimit(dc.RateLimit)
|
rl := request.ParseRateLimit(dc.RateLimit)
|
||||||
|
|
||||||
headers := map[string]string{
|
headers := map[string]string{
|
||||||
@@ -70,7 +71,7 @@ func New(dc config.Debrid) *RealDebrid {
|
|||||||
"Authorization": fmt.Sprintf("Bearer %s", currentDownloadKey),
|
"Authorization": fmt.Sprintf("Bearer %s", currentDownloadKey),
|
||||||
}
|
}
|
||||||
|
|
||||||
return &RealDebrid{
|
r := &RealDebrid{
|
||||||
Name: "realdebrid",
|
Name: "realdebrid",
|
||||||
Host: "https://api.real-debrid.com/rest/1.0",
|
Host: "https://api.real-debrid.com/rest/1.0",
|
||||||
APIKey: dc.APIKey,
|
APIKey: dc.APIKey,
|
||||||
@@ -99,6 +100,12 @@ func New(dc config.Debrid) *RealDebrid {
|
|||||||
checkCached: dc.CheckCached,
|
checkCached: dc.CheckCached,
|
||||||
addSamples: dc.AddSamples,
|
addSamples: dc.AddSamples,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, err := r.GetProfile(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RealDebrid) GetName() string {
|
func (r *RealDebrid) GetName() string {
|
||||||
@@ -908,3 +915,30 @@ func (r *RealDebrid) DeleteDownloadLink(linkId string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RealDebrid) GetProfile() (*types.Profile, error) {
|
||||||
|
if r.Profile != nil {
|
||||||
|
return r.Profile, nil
|
||||||
|
}
|
||||||
|
url := fmt.Sprintf("%s/user", r.Host)
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||||
|
|
||||||
|
resp, err := r.client.MakeRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var data profileResponse
|
||||||
|
if json.Unmarshal(resp, &data) != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
profile := &types.Profile{
|
||||||
|
Id: data.Id,
|
||||||
|
Username: data.Username,
|
||||||
|
Email: data.Email,
|
||||||
|
Points: data.Points,
|
||||||
|
Premium: data.Premium,
|
||||||
|
Expiration: data.Expiration,
|
||||||
|
Type: data.Type,
|
||||||
|
}
|
||||||
|
return profile, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -139,3 +139,15 @@ type ErrorResponse struct {
|
|||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
ErrorCode int `json:"error_code"`
|
ErrorCode int `json:"error_code"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type profileResponse struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Points int64 `json:"points"`
|
||||||
|
Locale string `json:"locale"`
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Premium int `json:"premium"`
|
||||||
|
Expiration time.Time `json:"expiration"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,7 +37,11 @@ type Torbox struct {
|
|||||||
addSamples bool
|
addSamples bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(dc config.Debrid) *Torbox {
|
func (tb *Torbox) GetProfile() (*types.Profile, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(dc config.Debrid) (*Torbox, error) {
|
||||||
rl := request.ParseRateLimit(dc.RateLimit)
|
rl := request.ParseRateLimit(dc.RateLimit)
|
||||||
|
|
||||||
headers := map[string]string{
|
headers := map[string]string{
|
||||||
@@ -73,7 +77,7 @@ func New(dc config.Debrid) *Torbox {
|
|||||||
logger: _log,
|
logger: _log,
|
||||||
checkCached: dc.CheckCached,
|
checkCached: dc.CheckCached,
|
||||||
addSamples: dc.AddSamples,
|
addSamples: dc.AddSamples,
|
||||||
}
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tb *Torbox) GetName() string {
|
func (tb *Torbox) GetName() string {
|
||||||
|
|||||||
@@ -25,4 +25,5 @@ type Client interface {
|
|||||||
DisableAccount(string)
|
DisableAccount(string)
|
||||||
ResetActiveDownloadKeys()
|
ResetActiveDownloadKeys()
|
||||||
DeleteDownloadLink(linkId string) error
|
DeleteDownloadLink(linkId string) error
|
||||||
|
GetProfile() (*Profile, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,3 +125,25 @@ type Account struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IngestData struct {
|
||||||
|
Debrid string `json:"debrid"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Profile struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Points int64 `json:"points"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Premium int `json:"premium"`
|
||||||
|
Expiration time.Time `json:"expiration"`
|
||||||
|
|
||||||
|
LibrarySize int `json:"library_size"`
|
||||||
|
BadTorrents int `json:"bad_torrents"`
|
||||||
|
ActiveLinks int `json:"active_links"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -309,70 +309,6 @@ func (q *QBit) createSymlinksWebdav(debridTorrent *debrid.Torrent, rclonePath, t
|
|||||||
return symlinkPath, nil
|
return symlinkPath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QBit) createSymlinks(debridTorrent *debrid.Torrent, rclonePath, torrentFolder string) (string, error) {
|
|
||||||
files := debridTorrent.Files
|
|
||||||
symlinkPath := filepath.Join(q.DownloadFolder, debridTorrent.Arr.Name, torrentFolder) // /mnt/symlinks/{category}/MyTVShow/
|
|
||||||
err := os.MkdirAll(symlinkPath, os.ModePerm)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to create directory: %s: %v", symlinkPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
remainingFiles := make(map[string]debrid.File)
|
|
||||||
for _, file := range files {
|
|
||||||
remainingFiles[file.Path] = file
|
|
||||||
}
|
|
||||||
|
|
||||||
ticker := time.NewTicker(100 * time.Millisecond)
|
|
||||||
defer ticker.Stop()
|
|
||||||
timeout := time.After(30 * time.Minute)
|
|
||||||
filePaths := make([]string, 0, len(files))
|
|
||||||
|
|
||||||
for len(remainingFiles) > 0 {
|
|
||||||
select {
|
|
||||||
case <-ticker.C:
|
|
||||||
entries, err := os.ReadDir(rclonePath)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check which files exist in this batch
|
|
||||||
for _, entry := range entries {
|
|
||||||
filename := entry.Name()
|
|
||||||
if file, exists := remainingFiles[filename]; exists {
|
|
||||||
fullFilePath := filepath.Join(rclonePath, filename)
|
|
||||||
fileSymlinkPath := filepath.Join(symlinkPath, file.Name)
|
|
||||||
|
|
||||||
if err := os.Symlink(fullFilePath, fileSymlinkPath); err != nil && !os.IsExist(err) {
|
|
||||||
q.logger.Debug().Msgf("Failed to create symlink: %s: %v", fileSymlinkPath, err)
|
|
||||||
} else {
|
|
||||||
filePaths = append(filePaths, fileSymlinkPath)
|
|
||||||
delete(remainingFiles, filename)
|
|
||||||
q.logger.Info().Msgf("File is ready: %s", file.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case <-timeout:
|
|
||||||
q.logger.Warn().Msgf("Timeout waiting for files, %d files still pending", len(remainingFiles))
|
|
||||||
return symlinkPath, fmt.Errorf("timeout waiting for files")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if q.SkipPreCache {
|
|
||||||
return symlinkPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
|
|
||||||
if err := q.preCacheFile(debridTorrent.Name, filePaths); err != nil {
|
|
||||||
q.logger.Error().Msgf("Failed to pre-cache file: %s", err)
|
|
||||||
} else {
|
|
||||||
q.logger.Trace().Msgf("Pre-cached %d files", len(filePaths))
|
|
||||||
}
|
|
||||||
}() // Pre-cache the files in the background
|
|
||||||
return symlinkPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *QBit) getTorrentPath(rclonePath string, debridTorrent *debrid.Torrent) (string, error) {
|
func (q *QBit) getTorrentPath(rclonePath string, debridTorrent *debrid.Torrent) (string, error) {
|
||||||
for {
|
for {
|
||||||
torrentPath, err := debridTorrent.GetMountFolder(rclonePath)
|
torrentPath, err := debridTorrent.GetMountFolder(rclonePath)
|
||||||
|
|||||||
@@ -1,159 +0,0 @@
|
|||||||
package repair
|
|
||||||
|
|
||||||
//func (r *Repair) clean(job *Job) error {
|
|
||||||
// // Create a new error group
|
|
||||||
// g, ctx := errgroup.WithContext(context.Background())
|
|
||||||
//
|
|
||||||
// uniqueItems := make(map[string]string)
|
|
||||||
// mu := sync.Mutex{}
|
|
||||||
//
|
|
||||||
// // Limit concurrent goroutines
|
|
||||||
// g.SetLimit(10)
|
|
||||||
//
|
|
||||||
// for _, a := range job.Arrs {
|
|
||||||
// a := a // Capture range variable
|
|
||||||
// g.Go(func() error {
|
|
||||||
// // Check if context was canceled
|
|
||||||
// select {
|
|
||||||
// case <-ctx.Done():
|
|
||||||
// return ctx.Err()
|
|
||||||
// default:
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// items, err := r.cleanArr(job, a, "")
|
|
||||||
// if err != nil {
|
|
||||||
// r.logger.Error().Err(err).Msgf("Error cleaning %s", a)
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Safely append the found items to the shared slice
|
|
||||||
// if len(items) > 0 {
|
|
||||||
// mu.Lock()
|
|
||||||
// for k, v := range items {
|
|
||||||
// uniqueItems[k] = v
|
|
||||||
// }
|
|
||||||
// mu.Unlock()
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return nil
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if err := g.Wait(); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if len(uniqueItems) == 0 {
|
|
||||||
// job.CompletedAt = time.Now()
|
|
||||||
// job.Status = JobCompleted
|
|
||||||
//
|
|
||||||
// go func() {
|
|
||||||
// if err := request.SendDiscordMessage("repair_clean_complete", "success", job.discordContext()); err != nil {
|
|
||||||
// r.logger.Error().Msgf("Error sending discord message: %v", err)
|
|
||||||
// }
|
|
||||||
// }()
|
|
||||||
//
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// cache := r.deb.Caches["realdebrid"]
|
|
||||||
// if cache == nil {
|
|
||||||
// return fmt.Errorf("cache not found")
|
|
||||||
// }
|
|
||||||
// torrents := cache.GetTorrents()
|
|
||||||
//
|
|
||||||
// dangling := make([]string, 0)
|
|
||||||
// for _, t := range torrents {
|
|
||||||
// if _, ok := uniqueItems[t.Name]; !ok {
|
|
||||||
// dangling = append(dangling, t.Id)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// r.logger.Info().Msgf("Found %d delapitated items", len(dangling))
|
|
||||||
//
|
|
||||||
// if len(dangling) == 0 {
|
|
||||||
// job.CompletedAt = time.Now()
|
|
||||||
// job.Status = JobCompleted
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// client := r.deb.Clients["realdebrid"]
|
|
||||||
// if client == nil {
|
|
||||||
// return fmt.Errorf("client not found")
|
|
||||||
// }
|
|
||||||
// for _, id := range dangling {
|
|
||||||
// err := client.DeleteTorrent(id)
|
|
||||||
// if err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return nil
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//func (r *Repair) cleanArr(j *Job, _arr string, tmdbId string) (map[string]string, error) {
|
|
||||||
// uniqueItems := make(map[string]string)
|
|
||||||
// a := r.arrs.Get(_arr)
|
|
||||||
//
|
|
||||||
// r.logger.Info().Msgf("Starting repair for %s", a.Name)
|
|
||||||
// media, err := a.GetMedia(tmdbId)
|
|
||||||
// if err != nil {
|
|
||||||
// r.logger.Info().Msgf("Failed to get %s media: %v", a.Name, err)
|
|
||||||
// return uniqueItems, err
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Create a new error group
|
|
||||||
// g, ctx := errgroup.WithContext(context.Background())
|
|
||||||
//
|
|
||||||
// mu := sync.Mutex{}
|
|
||||||
//
|
|
||||||
// // Limit concurrent goroutines
|
|
||||||
// g.SetLimit(runtime.NumCPU() * 4)
|
|
||||||
//
|
|
||||||
// for _, m := range media {
|
|
||||||
// m := m // Create a new variable scoped to the loop iteration
|
|
||||||
// g.Go(func() error {
|
|
||||||
// // Check if context was canceled
|
|
||||||
// select {
|
|
||||||
// case <-ctx.Done():
|
|
||||||
// return ctx.Err()
|
|
||||||
// default:
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// u := r.getUniquePaths(m)
|
|
||||||
// for k, v := range u {
|
|
||||||
// mu.Lock()
|
|
||||||
// uniqueItems[k] = v
|
|
||||||
// mu.Unlock()
|
|
||||||
// }
|
|
||||||
// return nil
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if err := g.Wait(); err != nil {
|
|
||||||
// return uniqueItems, err
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// r.logger.Info().Msgf("Repair completed for %s. %d unique items", a.Name, len(uniqueItems))
|
|
||||||
// return uniqueItems, nil
|
|
||||||
//}
|
|
||||||
|
|
||||||
//func (r *Repair) getUniquePaths(media arr.Content) map[string]string {
|
|
||||||
// // Use zurg setup to check file availability with zurg
|
|
||||||
// // This reduces bandwidth usage significantly
|
|
||||||
//
|
|
||||||
// uniqueParents := make(map[string]string)
|
|
||||||
// files := media.Files
|
|
||||||
// for _, file := range files {
|
|
||||||
// target := getSymlinkTarget(file.Path)
|
|
||||||
// if target != "" {
|
|
||||||
// file.IsSymlink = true
|
|
||||||
// dir, f := filepath.Split(target)
|
|
||||||
// parent := filepath.Base(filepath.Clean(dir))
|
|
||||||
// // Set target path folder/file.mkv
|
|
||||||
// file.TargetPath = f
|
|
||||||
// uniqueParents[parent] = target
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return uniqueParents
|
|
||||||
//}
|
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/sirrobot01/decypharr/internal/request"
|
||||||
|
debridTypes "github.com/sirrobot01/decypharr/pkg/debrid/types"
|
||||||
|
"github.com/sirrobot01/decypharr/pkg/service"
|
||||||
|
"net/http"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) handleIngests(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ingests := make([]debridTypes.IngestData, 0)
|
||||||
|
svc := service.GetService()
|
||||||
|
if svc.Debrid == nil {
|
||||||
|
http.Error(w, "Debrid service is not enabled", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, cache := range svc.Debrid.Caches {
|
||||||
|
if cache == nil {
|
||||||
|
s.logger.Error().Msg("Debrid cache is nil, skipping")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data, err := cache.GetIngests()
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error().Err(err).Msg("Failed to get ingests from debrid cache")
|
||||||
|
http.Error(w, "Failed to get ingests: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ingests = append(ingests, data...)
|
||||||
|
}
|
||||||
|
|
||||||
|
request.JSONResponse(w, ingests, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleIngestsByDebrid(w http.ResponseWriter, r *http.Request) {
|
||||||
|
debridName := chi.URLParam(r, "debrid")
|
||||||
|
if debridName == "" {
|
||||||
|
http.Error(w, "Debrid name is required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
svc := service.GetService()
|
||||||
|
if svc.Debrid == nil {
|
||||||
|
http.Error(w, "Debrid service is not enabled", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cache, exists := svc.Debrid.Caches[debridName]
|
||||||
|
if !exists {
|
||||||
|
http.Error(w, "Debrid cache not found: "+debridName, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := cache.GetIngests()
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error().Err(err).Msg("Failed to get ingests from debrid cache")
|
||||||
|
http.Error(w, "Failed to get ingests: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
request.JSONResponse(w, data, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var memStats runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&memStats)
|
||||||
|
|
||||||
|
stats := map[string]any{
|
||||||
|
// Memory stats
|
||||||
|
"heap_alloc_mb": fmt.Sprintf("%.2fMB", float64(memStats.HeapAlloc)/1024/1024),
|
||||||
|
"total_alloc_mb": fmt.Sprintf("%.2fMB", float64(memStats.TotalAlloc)/1024/1024),
|
||||||
|
"memory_used": fmt.Sprintf("%.2fMB", float64(memStats.Sys)/1024/1024),
|
||||||
|
|
||||||
|
// GC stats
|
||||||
|
"gc_cycles": memStats.NumGC,
|
||||||
|
// Goroutine stats
|
||||||
|
"goroutines": runtime.NumGoroutine(),
|
||||||
|
|
||||||
|
// System info
|
||||||
|
"num_cpu": runtime.NumCPU(),
|
||||||
|
|
||||||
|
// OS info
|
||||||
|
"os": runtime.GOOS,
|
||||||
|
"arch": runtime.GOARCH,
|
||||||
|
"go_version": runtime.Version(),
|
||||||
|
}
|
||||||
|
|
||||||
|
svc := service.GetService()
|
||||||
|
if svc.Debrid == nil {
|
||||||
|
request.JSONResponse(w, stats, http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clients := svc.Debrid.GetDebrids()
|
||||||
|
profiles := make([]*debridTypes.Profile, 0)
|
||||||
|
for debridName, client := range clients {
|
||||||
|
profile, err := client.GetProfile()
|
||||||
|
profile.Name = debridName
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error().Err(err).Msg("Failed to get debrid profile")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cache, ok := svc.Debrid.Caches[debridName]
|
||||||
|
if ok {
|
||||||
|
// Get torrent data
|
||||||
|
profile.LibrarySize = len(cache.GetTorrents())
|
||||||
|
profile.BadTorrents = len(cache.GetListing("__bad__"))
|
||||||
|
profile.ActiveLinks = cache.GetTotalActiveDownloadLinks()
|
||||||
|
|
||||||
|
}
|
||||||
|
profiles = append(profiles, profile)
|
||||||
|
}
|
||||||
|
stats["debrids"] = profiles
|
||||||
|
request.JSONResponse(w, stats, http.StatusOK)
|
||||||
|
}
|
||||||
+6
-30
@@ -9,12 +9,10 @@ import (
|
|||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/sirrobot01/decypharr/internal/config"
|
"github.com/sirrobot01/decypharr/internal/config"
|
||||||
"github.com/sirrobot01/decypharr/internal/logger"
|
"github.com/sirrobot01/decypharr/internal/logger"
|
||||||
"github.com/sirrobot01/decypharr/internal/request"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
@@ -45,8 +43,12 @@ func New(handlers map[string]http.Handler) *Server {
|
|||||||
//logs
|
//logs
|
||||||
r.Get("/logs", s.getLogs)
|
r.Get("/logs", s.getLogs)
|
||||||
|
|
||||||
//stats
|
//debugs
|
||||||
r.Get("/stats", s.getStats)
|
r.Route("/debug", func(r chi.Router) {
|
||||||
|
r.Get("/stats", s.handleStats)
|
||||||
|
r.Get("/ingests", s.handleIngests)
|
||||||
|
r.Get("/ingests/{debrid}", s.handleIngestsByDebrid)
|
||||||
|
})
|
||||||
|
|
||||||
//webhooks
|
//webhooks
|
||||||
r.Post("/webhooks/tautulli", s.handleTautulli)
|
r.Post("/webhooks/tautulli", s.handleTautulli)
|
||||||
@@ -108,29 +110,3 @@ func (s *Server) getLogs(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) getStats(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var memStats runtime.MemStats
|
|
||||||
runtime.ReadMemStats(&memStats)
|
|
||||||
|
|
||||||
stats := map[string]interface{}{
|
|
||||||
// Memory stats
|
|
||||||
"heap_alloc_mb": fmt.Sprintf("%.2fMB", float64(memStats.HeapAlloc)/1024/1024),
|
|
||||||
"total_alloc_mb": fmt.Sprintf("%.2fMB", float64(memStats.TotalAlloc)/1024/1024),
|
|
||||||
"memory_used": fmt.Sprintf("%.2fMB", float64(memStats.Sys)/1024/1024),
|
|
||||||
|
|
||||||
// GC stats
|
|
||||||
"gc_cycles": memStats.NumGC,
|
|
||||||
// Goroutine stats
|
|
||||||
"goroutines": runtime.NumGoroutine(),
|
|
||||||
|
|
||||||
// System info
|
|
||||||
"num_cpu": runtime.NumCPU(),
|
|
||||||
|
|
||||||
// OS info
|
|
||||||
"os": runtime.GOOS,
|
|
||||||
"arch": runtime.GOARCH,
|
|
||||||
"go_version": runtime.Version(),
|
|
||||||
}
|
|
||||||
request.JSONResponse(w, stats, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -37,12 +37,6 @@ func Reset() {
|
|||||||
if instance.Debrid != nil {
|
if instance.Debrid != nil {
|
||||||
instance.Debrid.Reset()
|
instance.Debrid.Reset()
|
||||||
}
|
}
|
||||||
if instance.Arr != nil {
|
|
||||||
//instance.Arr.Reset()
|
|
||||||
}
|
|
||||||
if instance.Repair != nil {
|
|
||||||
//instance.Repair.Reset()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
once = sync.Once{}
|
once = sync.Once{}
|
||||||
instance = nil
|
instance = nil
|
||||||
|
|||||||
@@ -169,7 +169,7 @@
|
|||||||
<i class="bi bi-sun-fill" id="lightIcon"></i>
|
<i class="bi bi-sun-fill" id="lightIcon"></i>
|
||||||
<i class="bi bi-moon-fill d-none" id="darkIcon"></i>
|
<i class="bi bi-moon-fill d-none" id="darkIcon"></i>
|
||||||
</div>
|
</div>
|
||||||
<a href="{{.URLBase}}stats" class="me-2">
|
<a href="{{.URLBase}}debug/stats" class="me-2">
|
||||||
<i class="bi bi-bar-chart-line me-1"></i>Stats
|
<i class="bi bi-bar-chart-line me-1"></i>Stats
|
||||||
</a>
|
</a>
|
||||||
<span class="badge bg-primary" id="version-badge">Loading...</span>
|
<span class="badge bg-primary" id="version-badge">Loading...</span>
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ type File struct {
|
|||||||
cache *debrid.Cache
|
cache *debrid.Cache
|
||||||
fileId string
|
fileId string
|
||||||
torrentName string
|
torrentName string
|
||||||
torrentId string
|
|
||||||
|
|
||||||
modTime time.Time
|
modTime time.Time
|
||||||
|
|
||||||
@@ -47,7 +46,6 @@ type File struct {
|
|||||||
|
|
||||||
downloadLink string
|
downloadLink string
|
||||||
link string
|
link string
|
||||||
canDelete bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// File interface implementations for File
|
// File interface implementations for File
|
||||||
|
|||||||
@@ -326,7 +326,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
handler.ServeHTTP(w, r)
|
handler.ServeHTTP(w, r)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getContentType(fileName string) string {
|
func getContentType(fileName string) string {
|
||||||
|
|||||||
@@ -11,16 +11,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// getName: Returns the torrent name and filename from the path
|
|
||||||
func getName(rootDir, path string) (string, string) {
|
|
||||||
path = strings.TrimPrefix(path, rootDir)
|
|
||||||
parts := strings.Split(strings.TrimPrefix(path, string(os.PathSeparator)), string(os.PathSeparator))
|
|
||||||
if len(parts) < 2 {
|
|
||||||
return "", ""
|
|
||||||
}
|
|
||||||
return parts[1], strings.Join(parts[2:], string(os.PathSeparator)) // Note the change from [0] to [1]
|
|
||||||
}
|
|
||||||
|
|
||||||
func isValidURL(str string) bool {
|
func isValidURL(str string) bool {
|
||||||
u, err := url.Parse(str)
|
u, err := url.Parse(str)
|
||||||
// A valid URL should parse without error, and have a non-empty scheme and host.
|
// A valid URL should parse without error, and have a non-empty scheme and host.
|
||||||
|
|||||||
Reference in New Issue
Block a user