Add support for rar extraction
This commit is contained in:
@@ -28,7 +28,7 @@ type ConfigInterface interface {
|
|||||||
GetDownloadTimeoutSecs() int
|
GetDownloadTimeoutSecs() int
|
||||||
GetRetriesUntilFailed() int
|
GetRetriesUntilFailed() int
|
||||||
GetRateLimitSleepSecs() int
|
GetRateLimitSleepSecs() int
|
||||||
ShouldDeleteRarFiles() bool
|
GetRarAction() string
|
||||||
GetDownloadsEveryMins() int
|
GetDownloadsEveryMins() int
|
||||||
GetDumpTorrentsEveryMins() int
|
GetDumpTorrentsEveryMins() int
|
||||||
GetPlayableExtensions() []string
|
GetPlayableExtensions() []string
|
||||||
@@ -41,7 +41,6 @@ type ZurgConfig struct {
|
|||||||
|
|
||||||
ApiTimeoutSecs int `yaml:"api_timeout_secs" json:"api_timeout_secs"`
|
ApiTimeoutSecs int `yaml:"api_timeout_secs" json:"api_timeout_secs"`
|
||||||
CanRepair bool `yaml:"enable_repair" json:"enable_repair"`
|
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"`
|
DownloadsEveryMins int `yaml:"downloads_every_mins" json:"downloads_every_mins"`
|
||||||
DownloadsLimit int `yaml:"downloads_limit" json:"downloads_limit"`
|
DownloadsLimit int `yaml:"downloads_limit" json:"downloads_limit"`
|
||||||
DumpTorrentsEveryMins int `yaml:"dump_torrents_every_mins" json:"dump_torrents_every_mins"`
|
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"`
|
ServeFromRclone bool `yaml:"serve_from_rclone" json:"serve_from_rclone"`
|
||||||
TorrentsCount int `yaml:"get_torrents_count" json:"get_torrents_count"`
|
TorrentsCount int `yaml:"get_torrents_count" json:"get_torrents_count"`
|
||||||
Username string `yaml:"username" json:"username"`
|
Username string `yaml:"username" json:"username"`
|
||||||
|
RarAction string `yaml:"rar_action" json:"rar_action"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (z *ZurgConfig) GetConfig() ZurgConfig {
|
func (z *ZurgConfig) GetConfig() ZurgConfig {
|
||||||
@@ -205,8 +205,15 @@ func (z *ZurgConfig) GetRateLimitSleepSecs() int {
|
|||||||
return z.RateLimitSleepSecs
|
return z.RateLimitSleepSecs
|
||||||
}
|
}
|
||||||
|
|
||||||
func (z *ZurgConfig) ShouldDeleteRarFiles() bool {
|
// GetRarAction returns the action to take when a rar'ed torrent is found
|
||||||
return z.DeleteRarFiles
|
// 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 {
|
func (z *ZurgConfig) GetPlayableExtensions() []string {
|
||||||
|
|||||||
@@ -267,6 +267,17 @@ func (z *ZurgConfigV1) matchFilter(torrentName string, torrentSize int64, torren
|
|||||||
}
|
}
|
||||||
return checkArithmeticSequenceInFilenames(fileNames)
|
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 {
|
if filter.FileInsideSizeGreaterThanOrEqual > 0 {
|
||||||
for _, fileSize := range fileSizes {
|
for _, fileSize := range fileSizes {
|
||||||
if fileSize >= filter.FileInsideSizeGreaterThanOrEqual {
|
if fileSize >= filter.FileInsideSizeGreaterThanOrEqual {
|
||||||
|
|||||||
@@ -41,4 +41,5 @@ type FilterConditionsV1 struct {
|
|||||||
FileInsideSizeLessThanOrEqual int64 `yaml:"any_file_inside_size_lte"`
|
FileInsideSizeLessThanOrEqual int64 `yaml:"any_file_inside_size_lte"`
|
||||||
|
|
||||||
HasEpisodes bool `yaml:"has_episodes"`
|
HasEpisodes bool `yaml:"has_episodes"`
|
||||||
|
IsMusic bool `yaml:"is_music"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,8 +203,8 @@ func (zr *Handlers) handleHome(resp http.ResponseWriter, req *http.Request) {
|
|||||||
<td>%d mins</td>
|
<td>%d mins</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Delete Rar Files</td>
|
<td>Action to take on RAR'ed torrents</td>
|
||||||
<td>%t</td>
|
<td>%s</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>API Timeout</td>
|
<td>API Timeout</td>
|
||||||
@@ -327,7 +327,7 @@ func (zr *Handlers) handleHome(resp http.ResponseWriter, req *http.Request) {
|
|||||||
response.Config.EnableRetainFolderNameExtension(),
|
response.Config.EnableRetainFolderNameExtension(),
|
||||||
response.Config.EnableRepair(),
|
response.Config.EnableRepair(),
|
||||||
response.Config.GetRepairEveryMins(),
|
response.Config.GetRepairEveryMins(),
|
||||||
response.Config.ShouldDeleteRarFiles(),
|
response.Config.GetRarAction(),
|
||||||
response.Config.GetApiTimeoutSecs(),
|
response.Config.GetApiTimeoutSecs(),
|
||||||
response.Config.GetTorrentsCount(),
|
response.Config.GetTorrentsCount(),
|
||||||
response.Config.GetDownloadTimeoutSecs(),
|
response.Config.GetDownloadTimeoutSecs(),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/debridmediamanager/zurg/internal/config"
|
"github.com/debridmediamanager/zurg/internal/config"
|
||||||
"github.com/debridmediamanager/zurg/pkg/realdebrid"
|
"github.com/debridmediamanager/zurg/pkg/realdebrid"
|
||||||
|
"github.com/debridmediamanager/zurg/pkg/utils"
|
||||||
mapset "github.com/deckarep/golang-set/v2"
|
mapset "github.com/deckarep/golang-set/v2"
|
||||||
cmap "github.com/orcaman/concurrent-map/v2"
|
cmap "github.com/orcaman/concurrent-map/v2"
|
||||||
)
|
)
|
||||||
@@ -179,6 +180,25 @@ func (t *TorrentManager) repair(torrent *Torrent) {
|
|||||||
|
|
||||||
brokenFiles, allBroken := getBrokenFiles(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
|
// 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())
|
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)
|
t.writeTorrentToFile(torrent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is a rar'ed torrent, nothing we can do
|
|
||||||
if assignedCount == 0 && rarCount == 1 {
|
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.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)
|
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 {
|
} else {
|
||||||
t.repairLog.Warnf("Torrent %s is rar'ed and we cannot repair it", t.GetKey(torrent))
|
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.markAsUnfixable(torrent, "rar'ed by RD")
|
||||||
t.markAsUnplayable(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 false // end repair
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true // continue repair
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TorrentManager) redownloadTorrent(torrent *Torrent, selection []string) (*realdebrid.TorrentInfo, error) {
|
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))
|
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
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,16 @@ func getContentMimeType(filePath string) string {
|
|||||||
return "application/zip"
|
return "application/zip"
|
||||||
case ".txt":
|
case ".txt":
|
||||||
return "text/plain"
|
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:
|
default:
|
||||||
return "application/octet-stream"
|
return "application/octet-stream"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -328,20 +328,10 @@ func (r *HTTPClient) CanFetchFirstByte(url string) bool {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
|
resp, _ := r.Do(req)
|
||||||
resp, err := r.Do(req)
|
if resp != nil {
|
||||||
if err != nil {
|
defer resp.Body.Close()
|
||||||
return false
|
return resp.StatusCode == 200 || resp.StatusCode == 206
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ func (rd *RealDebrid) GetTorrents(onlyOne bool) ([]Torrent, int, error) {
|
|||||||
page := 1
|
page := 1
|
||||||
// compute ceiling of totalCount / limit
|
// compute ceiling of totalCount / limit
|
||||||
maxPages := (totalCount + rd.cfg.GetTorrentsCount() - 1) / rd.cfg.GetTorrentsCount()
|
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
|
maxParallelThreads := 4
|
||||||
if maxPages < maxParallelThreads {
|
if maxPages < maxParallelThreads {
|
||||||
maxParallelThreads = maxPages
|
maxParallelThreads = maxPages
|
||||||
@@ -370,7 +370,7 @@ func (rd *RealDebrid) GetDownloads() []Download {
|
|||||||
|
|
||||||
// compute ceiling of totalCount / limit
|
// compute ceiling of totalCount / limit
|
||||||
maxPages := (totalCount + limit - 1) / 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
|
maxParallelThreads := 8
|
||||||
if maxPages < maxParallelThreads {
|
if maxPages < maxParallelThreads {
|
||||||
maxParallelThreads = maxPages
|
maxParallelThreads = maxPages
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ func IsPlayable(filePath string) bool {
|
|||||||
return strings.HasSuffix(filePath, ".avi") ||
|
return strings.HasSuffix(filePath, ".avi") ||
|
||||||
strings.HasSuffix(filePath, ".m2ts") ||
|
strings.HasSuffix(filePath, ".m2ts") ||
|
||||||
strings.HasSuffix(filePath, ".m4v") ||
|
strings.HasSuffix(filePath, ".m4v") ||
|
||||||
strings.HasSuffix(filePath, ".mkv") ||
|
strings.HasSuffix(filePath, ".mkv") || // confirmed working
|
||||||
strings.HasSuffix(filePath, ".mp4") ||
|
strings.HasSuffix(filePath, ".mp4") || // confirmed working
|
||||||
strings.HasSuffix(filePath, ".mpg") ||
|
strings.HasSuffix(filePath, ".mpg") ||
|
||||||
strings.HasSuffix(filePath, ".ts") ||
|
strings.HasSuffix(filePath, ".ts") ||
|
||||||
strings.HasSuffix(filePath, ".wmv")
|
strings.HasSuffix(filePath, ".wmv") ||
|
||||||
|
strings.HasSuffix(filePath, ".m4b") // confirmed working
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user