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 }