Add support for rar extraction

This commit is contained in:
Ben Adrian Sarmiento
2024-06-05 09:20:40 +02:00
parent f977abc052
commit 62e6143e06
9 changed files with 112 additions and 46 deletions

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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"`
}

View File

@@ -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(),

View File

@@ -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
}

View File

@@ -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"
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
}