218 lines
5.6 KiB
Go
218 lines
5.6 KiB
Go
package torrent
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
|
|
pb "homelab.lan/music-agregator/gen/music_agregator/torrent/v1"
|
|
"homelab.lan/music-agregator/internal/config"
|
|
)
|
|
|
|
type TorrentService struct {
|
|
client TorrentClient
|
|
token string
|
|
}
|
|
|
|
func NewTorrentService(cfg config.Config) (*TorrentService, error) {
|
|
var client TorrentClient
|
|
|
|
switch cfg.Torrent.ClientType {
|
|
case config.TorrentClientQbittorrent:
|
|
client = NewQbittorrentClient(cfg.Torrent.Url)
|
|
default:
|
|
return nil, fmt.Errorf("unknown torrent client type: %s", cfg.Torrent.ClientType)
|
|
}
|
|
|
|
token, err := client.Login(cfg.Torrent.Username, cfg.Torrent.Password)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("torrent client login failed: %w", err)
|
|
}
|
|
|
|
log.Info().Str("client", string(cfg.Torrent.ClientType)).Msg("torrent client connected")
|
|
|
|
return &TorrentService{
|
|
client: client,
|
|
token: token,
|
|
}, nil
|
|
}
|
|
|
|
func (service *TorrentService) List(req *pb.ListRequest) (*pb.ListResponse, error) {
|
|
torrents, err := service.client.List()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &pb.ListResponse{Items: toProtoItems(torrents)}, nil
|
|
}
|
|
|
|
func (service *TorrentService) Add(req *pb.AddRequest) (*pb.AddResponse, error) {
|
|
var file TorrentFile
|
|
|
|
if len(req.GetTorrentData()) > 0 {
|
|
file = TorrentFile{
|
|
Filename: req.GetFilename(),
|
|
Data: req.GetTorrentData(),
|
|
}
|
|
} else if req.GetDownloadUrl() != "" {
|
|
downloaded, err := downloadTorrentFile(req.GetDownloadUrl())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
file = *downloaded
|
|
} else {
|
|
return nil, fmt.Errorf("either torrent_data or download_url must be provided")
|
|
}
|
|
|
|
if err := service.client.AddTorrent(file); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
searchName := strings.TrimSuffix(file.Filename, ".torrent")
|
|
torrents, err := service.client.Find(FindOptions{Name: searchName})
|
|
if err != nil {
|
|
log.Warn().Err(err).Msg("torrent added but failed to find it afterwards")
|
|
return &pb.AddResponse{}, nil
|
|
}
|
|
|
|
if len(torrents) == 0 {
|
|
log.Warn().Str("filename", file.Filename).Msg("torrent added but not found in client")
|
|
return &pb.AddResponse{}, nil
|
|
}
|
|
|
|
return &pb.AddResponse{Item: toProtoItem(torrents[0])}, nil
|
|
}
|
|
|
|
func toProtoItems(torrents []TorrentInfo) []*pb.ListItem {
|
|
items := make([]*pb.ListItem, len(torrents))
|
|
for i, t := range torrents {
|
|
items[i] = toProtoItem(t)
|
|
}
|
|
return items
|
|
}
|
|
|
|
func toProtoItem(t TorrentInfo) *pb.ListItem {
|
|
return &pb.ListItem{
|
|
Hash: t.Hash,
|
|
Name: t.Name,
|
|
Size: t.Size,
|
|
Progress: t.Progress,
|
|
Dlspeed: t.DlSpeed,
|
|
Upspeed: t.UpSpeed,
|
|
NumSeeds: t.NumSeeds,
|
|
NumLeechs: t.NumLeechs,
|
|
State: t.State,
|
|
Eta: t.ETA,
|
|
Ratio: t.Ratio,
|
|
Category: t.Category,
|
|
Tags: t.Tags,
|
|
AddedOn: t.AddedOn,
|
|
CompletionOn: t.CompletionOn,
|
|
SavePath: t.SavePath,
|
|
ContentPath: t.ContentPath,
|
|
Downloaded: t.Downloaded,
|
|
Uploaded: t.Uploaded,
|
|
Tracker: t.Tracker,
|
|
SeedingTime: t.SeedingTime,
|
|
AmountLeft: t.AmountLeft,
|
|
Availability: t.Availability,
|
|
|
|
SizeFormatted: formatBytes(t.Size),
|
|
ProgressFormatted: fmt.Sprintf("%.1f%%", t.Progress*100),
|
|
DlspeedFormatted: formatSpeed(t.DlSpeed),
|
|
UpspeedFormatted: formatSpeed(t.UpSpeed),
|
|
AddedOnFormatted: formatTimestamp(t.AddedOn),
|
|
CompletionOnFormatted: formatTimestamp(t.CompletionOn),
|
|
DownloadedFormatted: formatBytes(t.Downloaded),
|
|
UploadedFormatted: formatBytes(t.Uploaded),
|
|
AmountLeftFormatted: formatBytes(t.AmountLeft),
|
|
AvailabilityFormatted: fmt.Sprintf("%.2f", t.Availability),
|
|
EtaFormatted: formatETA(t.ETA),
|
|
}
|
|
}
|
|
|
|
func formatBytes(b int64) string {
|
|
switch {
|
|
case b >= 1<<30:
|
|
return fmt.Sprintf("%.2f GB", float64(b)/float64(1<<30))
|
|
case b >= 1<<20:
|
|
return fmt.Sprintf("%.1f MB", float64(b)/float64(1<<20))
|
|
case b >= 1<<10:
|
|
return fmt.Sprintf("%.0f KB", float64(b)/float64(1<<10))
|
|
default:
|
|
return fmt.Sprintf("%d B", b)
|
|
}
|
|
}
|
|
|
|
func formatSpeed(bytesPerSec int64) string {
|
|
if bytesPerSec == 0 {
|
|
return "0 B/s"
|
|
}
|
|
return formatBytes(bytesPerSec) + "/s"
|
|
}
|
|
|
|
func formatTimestamp(ts int64) string {
|
|
if ts <= 0 {
|
|
return ""
|
|
}
|
|
return time.Unix(ts, 0).Format("2006-01-02 15:04:05")
|
|
}
|
|
|
|
func formatETA(seconds int64) string {
|
|
if seconds <= 0 || seconds >= 8640000 {
|
|
return "∞"
|
|
}
|
|
d := time.Duration(seconds) * time.Second
|
|
h := int(d.Hours())
|
|
m := int(d.Minutes()) % 60
|
|
s := int(d.Seconds()) % 60
|
|
if h > 0 {
|
|
return fmt.Sprintf("%dh %dm %ds", h, m, s)
|
|
}
|
|
if m > 0 {
|
|
return fmt.Sprintf("%dm %ds", m, s)
|
|
}
|
|
return fmt.Sprintf("%ds", s)
|
|
}
|
|
|
|
func downloadTorrentFile(url string) (*TorrentFile, error) {
|
|
log.Trace().Str("url", url).Msg("downloading torrent file")
|
|
|
|
client := &http.Client{Timeout: 30 * time.Second}
|
|
resp, err := client.Get(url)
|
|
if err != nil {
|
|
log.Error().Err(err).Str("url", url).Msg("downloading torrent file failed")
|
|
return nil, fmt.Errorf("downloading torrent file: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
log.Error().Int("status", resp.StatusCode).Str("url", url).Msg("torrent download returned non-OK status")
|
|
return nil, fmt.Errorf("torrent download returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
data, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading torrent file: %w", err)
|
|
}
|
|
|
|
filename := path.Base(resp.Request.URL.Path)
|
|
if !strings.HasSuffix(strings.ToLower(filename), ".torrent") {
|
|
filename += ".torrent"
|
|
}
|
|
|
|
log.Debug().Str("filename", filename).Int("size", len(data)).Msg("torrent file downloaded")
|
|
|
|
return &TorrentFile{
|
|
Filename: filename,
|
|
Data: data,
|
|
}, nil
|
|
}
|