diff --git a/internal/config/types.go b/internal/config/types.go
index 04856fc..c3bce18 100644
--- a/internal/config/types.go
+++ b/internal/config/types.go
@@ -28,7 +28,7 @@ type ConfigInterface interface {
GetDownloadTimeoutSecs() int
GetRetriesUntilFailed() int
GetRateLimitSleepSecs() int
- ShouldDeleteRarFiles() bool
+ GetRarAction() string
GetDownloadsEveryMins() int
GetDumpTorrentsEveryMins() int
GetPlayableExtensions() []string
@@ -41,7 +41,6 @@ type ZurgConfig struct {
ApiTimeoutSecs int `yaml:"api_timeout_secs" json:"api_timeout_secs"`
CanRepair bool `yaml:"enable_repair" json:"enable_repair"`
- DeleteRarFiles bool `yaml:"auto_delete_rar_torrents" json:"auto_delete_rar_torrents"`
DownloadsEveryMins int `yaml:"downloads_every_mins" json:"downloads_every_mins"`
DownloadsLimit int `yaml:"downloads_limit" json:"downloads_limit"`
DumpTorrentsEveryMins int `yaml:"dump_torrents_every_mins" json:"dump_torrents_every_mins"`
@@ -65,6 +64,7 @@ type ZurgConfig struct {
ServeFromRclone bool `yaml:"serve_from_rclone" json:"serve_from_rclone"`
TorrentsCount int `yaml:"get_torrents_count" json:"get_torrents_count"`
Username string `yaml:"username" json:"username"`
+ RarAction string `yaml:"rar_action" json:"rar_action"`
}
func (z *ZurgConfig) GetConfig() ZurgConfig {
@@ -205,8 +205,15 @@ func (z *ZurgConfig) GetRateLimitSleepSecs() int {
return z.RateLimitSleepSecs
}
-func (z *ZurgConfig) ShouldDeleteRarFiles() bool {
- return z.DeleteRarFiles
+// GetRarAction returns the action to take when a rar'ed torrent is found
+// none: do nothing (mark as unrepairable)
+// extract: extract the rar'ed torrent
+// delete: delete the rar'ed torrent
+func (z *ZurgConfig) GetRarAction() string {
+ if z.RarAction == "" {
+ return "none"
+ }
+ return z.RarAction
}
func (z *ZurgConfig) GetPlayableExtensions() []string {
diff --git a/internal/config/v1.go b/internal/config/v1.go
index 48db77c..a308032 100644
--- a/internal/config/v1.go
+++ b/internal/config/v1.go
@@ -267,6 +267,17 @@ func (z *ZurgConfigV1) matchFilter(torrentName string, torrentSize int64, torren
}
return checkArithmeticSequenceInFilenames(fileNames)
}
+ if filter.IsMusic {
+ musicExts := []string{".m3u", ".mp3", ".flac"}
+ for _, filename := range fileNames {
+ for _, ext := range musicExts {
+ if strings.HasSuffix(strings.ToLower(filename), ext) {
+ return true
+ }
+ }
+ }
+ return false
+ }
if filter.FileInsideSizeGreaterThanOrEqual > 0 {
for _, fileSize := range fileSizes {
if fileSize >= filter.FileInsideSizeGreaterThanOrEqual {
diff --git a/internal/config/v1types.go b/internal/config/v1types.go
index 65987c1..b05287d 100644
--- a/internal/config/v1types.go
+++ b/internal/config/v1types.go
@@ -41,4 +41,5 @@ type FilterConditionsV1 struct {
FileInsideSizeLessThanOrEqual int64 `yaml:"any_file_inside_size_lte"`
HasEpisodes bool `yaml:"has_episodes"`
+ IsMusic bool `yaml:"is_music"`
}
diff --git a/internal/handlers/home.go b/internal/handlers/home.go
index a86b8fb..f6b54e4 100644
--- a/internal/handlers/home.go
+++ b/internal/handlers/home.go
@@ -203,8 +203,8 @@ func (zr *Handlers) handleHome(resp http.ResponseWriter, req *http.Request) {
%d mins |
- | Delete Rar Files |
- %t |
+ Action to take on RAR'ed torrents |
+ %s |
| API Timeout |
@@ -327,7 +327,7 @@ func (zr *Handlers) handleHome(resp http.ResponseWriter, req *http.Request) {
response.Config.EnableRetainFolderNameExtension(),
response.Config.EnableRepair(),
response.Config.GetRepairEveryMins(),
- response.Config.ShouldDeleteRarFiles(),
+ response.Config.GetRarAction(),
response.Config.GetApiTimeoutSecs(),
response.Config.GetTorrentsCount(),
response.Config.GetDownloadTimeoutSecs(),
diff --git a/internal/torrent/repair.go b/internal/torrent/repair.go
index 1552082..e6f0052 100644
--- a/internal/torrent/repair.go
+++ b/internal/torrent/repair.go
@@ -10,6 +10,7 @@ import (
"github.com/debridmediamanager/zurg/internal/config"
"github.com/debridmediamanager/zurg/pkg/realdebrid"
+ "github.com/debridmediamanager/zurg/pkg/utils"
mapset "github.com/deckarep/golang-set/v2"
cmap "github.com/orcaman/concurrent-map/v2"
)
@@ -179,6 +180,25 @@ func (t *TorrentManager) repair(torrent *Torrent) {
brokenFiles, allBroken := getBrokenFiles(torrent)
+ // check if broken files are playable
+ allPlayable := true
+ for _, file := range brokenFiles {
+ if utils.IsPlayable(file.Path) {
+ continue
+ }
+
+ allPlayable = false
+ if t.Config.GetRarAction() == "extract" {
+ info, _ := t.redownloadTorrent(torrent, []string{fmt.Sprintf("%d", file.ID)})
+ if info != nil {
+ t.setToBinOnceDone(info.ID)
+ }
+ }
+ }
+ if !allPlayable {
+ return
+ }
+
// first step: redownload the whole torrent
t.repairLog.Debugf("Torrent %s has %d broken files (out of %d), repairing by redownloading whole torrent", t.GetKey(torrent), len(brokenFiles), torrent.SelectedFiles.Count())
@@ -331,36 +351,62 @@ func (t *TorrentManager) assignLinks(torrent *Torrent) bool {
t.writeTorrentToFile(torrent)
}
- // this is a rar'ed torrent, nothing we can do
if assignedCount == 0 && rarCount == 1 {
- if t.Config.ShouldDeleteRarFiles() {
+ action := t.Config.GetRarAction()
+
+ if action == "delete" {
t.repairLog.Warnf("Torrent %s is rar'ed and we cannot repair it, deleting it as configured", t.GetKey(torrent))
t.Delete(t.GetKey(torrent), true)
+ return false
+ }
+
+ newUnassignedLinks.IterCb(func(_ string, unassigned *realdebrid.Download) {
+ newFile := &File{
+ File: realdebrid.File{
+ ID: 0,
+ Path: unassigned.Filename,
+ Bytes: unassigned.Filesize,
+ Selected: 0,
+ },
+ Ended: torrent.Added,
+ Link: unassigned.Link,
+ State: NewFileState("ok_file"),
+ }
+ torrent.SelectedFiles.Set(unassigned.Filename, newFile)
+ })
+
+ if action == "extract" {
+ videoFiles := []string{}
+ torrent.SelectedFiles.IterCb(func(_ string, file *File) {
+ if utils.IsPlayable(file.Path) {
+ videoFiles = append(videoFiles, fmt.Sprintf("%d", file.ID))
+ } else {
+ info, _ := t.redownloadTorrent(torrent, []string{fmt.Sprintf("%d", file.ID)})
+ if info != nil {
+ t.setToBinOnceDone(info.ID)
+ }
+ }
+ })
+ if len(videoFiles) > 0 {
+ info, _ := t.redownloadTorrent(torrent, videoFiles)
+ if info != nil {
+ t.setToBinOnceDone(info.ID)
+ }
+ }
} else {
t.repairLog.Warnf("Torrent %s is rar'ed and we cannot repair it", t.GetKey(torrent))
- newUnassignedLinks.IterCb(func(_ string, unassigned *realdebrid.Download) {
- newFile := &File{
- File: realdebrid.File{
- ID: 0,
- Path: unassigned.Filename,
- Bytes: unassigned.Filesize,
- Selected: 0,
- },
- Ended: torrent.Added,
- Link: unassigned.Link,
- State: NewFileState("ok_file"),
- }
- torrent.SelectedFiles.Set(unassigned.Filename, newFile)
- })
- torrent.UnassignedLinks = mapset.NewSet[string]()
t.markAsUnfixable(torrent, "rar'ed by RD")
t.markAsUnplayable(torrent, "rar'ed by RD")
- torrent.State.Event(context.Background(), "mark_as_repaired")
}
+
+ torrent.UnassignedLinks = mapset.NewSet[string]()
+ torrent.State.Event(context.Background(), "mark_as_repaired")
+ t.writeTorrentToFile(torrent)
+
return false // end repair
}
- return true
+ return true // continue repair
}
func (t *TorrentManager) redownloadTorrent(torrent *Torrent, selection []string) (*realdebrid.TorrentInfo, error) {
@@ -454,7 +500,7 @@ func (t *TorrentManager) redownloadTorrent(torrent *Torrent, selection []string)
return nil, fmt.Errorf("only got %d links but we need %d", len(info.Links), len(selection))
}
- t.repairLog.Infof("Redownloading torrent %s successful (id=%s, progress=%d)", t.GetKey(torrent), info.ID, info.Progress)
+ t.repairLog.Infof("Redownloading %d file(s) of torrent %s successful (id=%s, progress=%d)", len(selection), t.GetKey(torrent), info.ID, info.Progress)
return info, nil
}
diff --git a/internal/universal/check.go b/internal/universal/check.go
index 71ad47c..2f87a2c 100644
--- a/internal/universal/check.go
+++ b/internal/universal/check.go
@@ -81,6 +81,16 @@ func getContentMimeType(filePath string) string {
return "application/zip"
case ".txt":
return "text/plain"
+ case ".srt":
+ return "text/srt"
+ case ".jpg":
+ return "image/jpeg"
+ case ".png":
+ return "image/png"
+ case ".gif":
+ return "image/gif"
+ case ".webp":
+ return "image/webp"
default:
return "application/octet-stream"
}
diff --git a/pkg/http/client.go b/pkg/http/client.go
index aa6d852..4a000ea 100644
--- a/pkg/http/client.go
+++ b/pkg/http/client.go
@@ -328,20 +328,10 @@ func (r *HTTPClient) CanFetchFirstByte(url string) bool {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
req = req.WithContext(ctx)
-
- resp, err := r.Do(req)
- if err != nil {
- return false
- }
- defer resp.Body.Close()
-
- // If server supports partial content
- if resp.StatusCode/100 == 2 {
- buffer := make([]byte, 1)
- _, err = resp.Body.Read(buffer)
- if err == nil {
- return true
- }
+ resp, _ := r.Do(req)
+ if resp != nil {
+ defer resp.Body.Close()
+ return resp.StatusCode == 200 || resp.StatusCode == 206
}
return false
}
diff --git a/pkg/realdebrid/api.go b/pkg/realdebrid/api.go
index 31584a4..1cbae48 100644
--- a/pkg/realdebrid/api.go
+++ b/pkg/realdebrid/api.go
@@ -174,7 +174,7 @@ func (rd *RealDebrid) GetTorrents(onlyOne bool) ([]Torrent, int, error) {
page := 1
// compute ceiling of totalCount / limit
maxPages := (totalCount + rd.cfg.GetTorrentsCount() - 1) / rd.cfg.GetTorrentsCount()
- rd.log.Debugf("Torrents total count is %d, max pages is %d", totalCount, maxPages)
+ rd.log.Debugf("Torrents total count is %d, total page count is %d", totalCount, maxPages)
maxParallelThreads := 4
if maxPages < maxParallelThreads {
maxParallelThreads = maxPages
@@ -370,7 +370,7 @@ func (rd *RealDebrid) GetDownloads() []Download {
// compute ceiling of totalCount / limit
maxPages := (totalCount + limit - 1) / limit
- rd.log.Debugf("Total downloads count is %d, max pages is %d", totalCount, maxPages)
+ rd.log.Debugf("Total downloads count is %d, total page count is %d", totalCount, maxPages)
maxParallelThreads := 8
if maxPages < maxParallelThreads {
maxParallelThreads = maxPages
diff --git a/pkg/utils/playable.go b/pkg/utils/playable.go
index 7ec6350..3ce6cb9 100644
--- a/pkg/utils/playable.go
+++ b/pkg/utils/playable.go
@@ -7,9 +7,10 @@ func IsPlayable(filePath string) bool {
return strings.HasSuffix(filePath, ".avi") ||
strings.HasSuffix(filePath, ".m2ts") ||
strings.HasSuffix(filePath, ".m4v") ||
- strings.HasSuffix(filePath, ".mkv") ||
- strings.HasSuffix(filePath, ".mp4") ||
+ strings.HasSuffix(filePath, ".mkv") || // confirmed working
+ strings.HasSuffix(filePath, ".mp4") || // confirmed working
strings.HasSuffix(filePath, ".mpg") ||
strings.HasSuffix(filePath, ".ts") ||
- strings.HasSuffix(filePath, ".wmv")
+ strings.HasSuffix(filePath, ".wmv") ||
+ strings.HasSuffix(filePath, ".m4b") // confirmed working
}