Add support for rar extraction
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -203,8 +203,8 @@ func (zr *Handlers) handleHome(resp http.ResponseWriter, req *http.Request) {
|
||||
<td>%d mins</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Delete Rar Files</td>
|
||||
<td>%t</td>
|
||||
<td>Action to take on RAR'ed torrents</td>
|
||||
<td>%s</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>API Timeout</td>
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user