package torrent import ( "fmt" "math" "strings" "time" ) func (t *TorrentManager) RepairAll() { _ = t.repairWorker.Submit(func() { t.log.Info("Checking for torrents to repair") t.repairAll() t.log.Info("Finished checking for torrents to repair") }) } func (t *TorrentManager) repairAll() { allTorrents, _ := t.DirectoryMap.Get(INT_ALL) var toRepair []*Torrent allTorrents.IterCb(func(_ string, torrent *Torrent) { if torrent.AnyInProgress() && torrent.ForRepair { t.log.Warnf("Skipping %s for repairs because it is in progress", torrent.AccessKey) return } else if torrent.ForRepair && !torrent.Unfixable { toRepair = append(toRepair, torrent) } }) t.log.Debugf("Found %d torrents to repair", len(toRepair)) for i := range toRepair { t.log.Infof("Repairing %s", toRepair[i].AccessKey) t.repair(toRepair[i]) } } func (t *TorrentManager) Repair(torrent *Torrent) { _ = t.repairWorker.Submit(func() { t.log.Infof("Repairing torrent %s", torrent.AccessKey) t.repair(torrent) t.log.Infof("Finished repairing torrent %s", torrent.AccessKey) var updatedPaths []string t.assignedDirectoryCb(torrent, func(directory string) { updatedPaths = append(updatedPaths, fmt.Sprintf("%s/%s", directory, torrent.AccessKey)) }) t.TriggerHookOnLibraryUpdate(updatedPaths) }) } func (t *TorrentManager) repair(torrent *Torrent) { if torrent.AnyInProgress() { t.log.Infof("Torrent %s is in progress, skipping repair until download is done", torrent.AccessKey) return } proceed := t.canCapacityHandle() // blocks for approx 45 minutes if active torrents are full if !proceed { t.log.Error("Reached the max number of active torrents, cannot continue with the repair") return } expiredLinkTolerance := 24 * time.Hour if torrent.OlderThanDuration(expiredLinkTolerance) { // first solution: reinsert with same selection if t.reinsertTorrent(torrent, "") { t.log.Infof("Successfully downloaded torrent %s to repair it", torrent.AccessKey) return } else { t.log.Warn("Failed to repair by reinserting torrent") } } else { t.log.Infof("Torrent %s is not older than %d hours to be repaired by reinsertion, skipping", torrent.AccessKey, expiredLinkTolerance.Hours()) } // second solution: add only the missing files var missingFiles []File torrent.SelectedFiles.IterCb(func(_ string, file *File) { if !strings.HasPrefix(file.Link, "http") { missingFiles = append(missingFiles, *file) } }) // if we download a single file, it will be named differently // so we need to download 1 extra file to preserve the name // this is only relevant if we enable retain_rd_torrent_name if len(missingFiles) == 1 && torrent.SelectedFiles.Count() > 1 { // add the first file link encountered with a prefix of http for _, file := range torrent.SelectedFiles.Items() { if strings.HasPrefix(file.Link, "http") { missingFiles = append(missingFiles, *file) break } } } if len(missingFiles) > 0 { t.log.Infof("Redownloading in multiple batches the %d missing files for torrent %s", len(missingFiles), torrent.AccessKey) // if not, last resort: add only the missing files but do it in 2 batches half := len(missingFiles) / 2 missingFiles1 := strings.Join(getFileIDs(missingFiles[:half]), ",") missingFiles2 := strings.Join(getFileIDs(missingFiles[half:]), ",") if missingFiles1 != "" { t.reinsertTorrent(torrent, missingFiles1) } if missingFiles2 != "" { t.reinsertTorrent(torrent, missingFiles2) } } else { t.log.Warnf("Torrent %s has no missing files to repair", torrent.AccessKey) } } func (t *TorrentManager) reinsertTorrent(torrent *Torrent, missingFiles string) bool { oldTorrentIDs := make([]string, 0) // missing files means missing links // if missingFiles is not provided if missingFiles == "" { // only replace the torrent if we are reinserting all files oldTorrentIDs = torrent.DownloadedIDs.List() tmpSelection := "" torrent.SelectedFiles.IterCb(func(_ string, file *File) { tmpSelection += fmt.Sprintf("%d,", file.ID) // select all files }) if tmpSelection == "" { return true // nothing to repair } else { missingFiles = tmpSelection[:len(tmpSelection)-1] } } // redownload torrent resp, err := t.Api.AddMagnetHash(torrent.Hash) if err != nil { t.log.Warnf("Cannot redownload torrent: %v", err) return false } time.Sleep(1 * time.Second) // select files newTorrentID := resp.ID err = t.Api.SelectTorrentFiles(newTorrentID, missingFiles) if err != nil { t.log.Warnf("Cannot start redownloading: %v", err) t.Api.DeleteTorrent(newTorrentID) return false } time.Sleep(10 * time.Second) // see if the torrent is ready info, err := t.Api.GetTorrentInfo(newTorrentID) if err != nil { t.log.Warnf("Cannot get info on redownloaded torrent id=%s : %v", newTorrentID, err) t.Api.DeleteTorrent(newTorrentID) return false } if info.Status == "magnet_error" || info.Status == "error" || info.Status == "virus" || info.Status == "dead" { t.log.Warnf("The redownloaded torrent id=%s is in error state: %s", newTorrentID, info.Status) t.Api.DeleteTorrent(newTorrentID) return false } if info.Progress != 100 { t.log.Infof("Torrent id=%s is not cached anymore so we have to wait until completion (this should fix the issue already)", info.ID) for _, id := range oldTorrentIDs { t.Api.DeleteTorrent(id) } return true } missingCount := len(strings.Split(missingFiles, ",")) if len(info.Links) != missingCount { t.log.Infof("It did not fix the issue for id=%s, only got %d files but we need %d, undoing", info.ID, len(info.Links), missingCount) t.Api.DeleteTorrent(newTorrentID) return false } t.log.Infof("Repair successful id=%s", newTorrentID) for _, id := range oldTorrentIDs { t.Api.DeleteTorrent(id) } return true } func (t *TorrentManager) canCapacityHandle() bool { // max waiting time is 45 minutes const maxRetries = 50 const baseDelay = 1 * time.Second const maxDelay = 60 * time.Second retryCount := 0 for { count, err := t.Api.GetActiveTorrentCount() if err != nil { t.log.Warnf("Cannot get active downloads count: %v", err) if retryCount >= maxRetries { t.log.Error("Max retries reached. Exiting.") return false } delay := time.Duration(math.Pow(2, float64(retryCount))) * baseDelay if delay > maxDelay { delay = maxDelay } time.Sleep(delay) retryCount++ continue } if count.DownloadingCount < count.MaxNumberOfTorrents { // t.log.Infof("We can still add a new torrent, we have capacity for %d more", count.MaxNumberOfTorrents-count.DownloadingCount) return true } delay := time.Duration(math.Pow(2, float64(retryCount))) * baseDelay if delay > maxDelay { delay = maxDelay } t.log.Infof("We have reached the max number of active torrents, waiting for %s seconds before retrying", delay) if retryCount >= maxRetries { t.log.Error("Max retries reached. Exiting.") return false } time.Sleep(delay) retryCount++ } }