Add album/track releases with audio analysis, AnalyzeAlbumRelease RPC, Docker path auto-resolution, release parsing decision tree

This commit is contained in:
Alexander
2026-05-09 23:16:59 +02:00
parent 1e8506f146
commit 2740585261
25 changed files with 1841 additions and 125 deletions
+3 -2
View File
@@ -43,6 +43,7 @@ type TorrentClient interface {
Login(username string, password string) (string, error)
List() ([]TorrentInfo, error)
Find(opts FindOptions) ([]TorrentInfo, error)
AddTorrent(file TorrentFile) error
AddMagnet(magnetURI string) error
AddTorrent(file TorrentFile, savePath string) error
AddMagnet(magnetURI string, savePath string) error
DefaultSavePath() (string, error)
}
+88
View File
@@ -0,0 +1,88 @@
package torrent
import (
"context"
"fmt"
"strings"
"time"
dockerclient "github.com/docker/docker/client"
"github.com/rs/zerolog/log"
)
type PathMapper struct {
containerPath string
hostPath string
}
func NewPathMapper(containerName string, torrentClient TorrentClient) (*PathMapper, error) {
if containerName == "" {
savePath, err := torrentClient.DefaultSavePath()
if err != nil {
return nil, fmt.Errorf("getting default save path: %w", err)
}
log.Info().Str("path", savePath).Msg("no container configured, using direct path")
return &PathMapper{containerPath: savePath, hostPath: savePath}, nil
}
savePath, err := torrentClient.DefaultSavePath()
if err != nil {
return nil, fmt.Errorf("getting default save path: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cli, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation())
if err != nil {
return nil, fmt.Errorf("creating docker client: %w", err)
}
defer cli.Close()
inspect, err := cli.ContainerInspect(ctx, containerName)
if err != nil {
return nil, fmt.Errorf("inspecting container %s: %w", containerName, err)
}
hostPath := ""
for _, mount := range inspect.Mounts {
if mount.Destination == savePath {
hostPath = mount.Source
break
}
}
if hostPath == "" {
return nil, fmt.Errorf("no mount found for %s in container %s", savePath, containerName)
}
log.Info().
Str("container", containerName).
Str("container_path", savePath).
Str("host_path", hostPath).
Msg("resolved download path mapping")
return &PathMapper{containerPath: savePath, hostPath: hostPath}, nil
}
func (m *PathMapper) ToHost(containerPath string) string {
if m.containerPath == m.hostPath {
return containerPath
}
return strings.Replace(containerPath, m.containerPath, m.hostPath, 1)
}
func (m *PathMapper) ToContainer(hostPath string) string {
if m.containerPath == m.hostPath {
return hostPath
}
return strings.Replace(hostPath, m.hostPath, m.containerPath, 1)
}
func (m *PathMapper) HostDownloadPath() string {
return m.hostPath
}
func (m *PathMapper) ContainerDownloadPath() string {
return m.containerPath
}
+38 -4
View File
@@ -173,8 +173,8 @@ func filterLocally(torrents []TorrentInfo, opts FindOptions) []TorrentInfo {
return result
}
func (c *QbittorrentClient) AddTorrent(file TorrentFile) error {
log.Trace().Str("filename", file.Filename).Int("size", len(file.Data)).Msg("qbittorrent adding torrent file")
func (c *QbittorrentClient) AddTorrent(file TorrentFile, savePath string) error {
log.Trace().Str("filename", file.Filename).Int("size", len(file.Data)).Str("save_path", savePath).Msg("qbittorrent adding torrent file")
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
@@ -190,6 +190,12 @@ func (c *QbittorrentClient) AddTorrent(file TorrentFile) error {
return fmt.Errorf("writing torrent data: %w", err)
}
if savePath != "" {
if err := writer.WriteField("savepath", savePath); err != nil {
return fmt.Errorf("writing savepath field: %w", err)
}
}
if err := writer.Close(); err != nil {
return fmt.Errorf("closing multipart writer: %w", err)
}
@@ -205,14 +211,17 @@ func (c *QbittorrentClient) AddTorrent(file TorrentFile) error {
return c.doAdd(req, file.Filename)
}
func (c *QbittorrentClient) AddMagnet(magnetURI string) error {
func (c *QbittorrentClient) AddMagnet(magnetURI string, savePath string) error {
truncated := magnetURI
if len(truncated) > 80 {
truncated = truncated[:80] + "..."
}
log.Trace().Str("magnet", truncated).Msg("qbittorrent adding magnet")
log.Trace().Str("magnet", truncated).Str("save_path", savePath).Msg("qbittorrent adding magnet")
data := url.Values{"urls": {magnetURI}}
if savePath != "" {
data.Set("savepath", savePath)
}
req, err := http.NewRequest("POST", c.baseURL+"/api/v2/torrents/add", strings.NewReader(data.Encode()))
if err != nil {
log.Error().Err(err).Msg("qbittorrent creating magnet add request failed")
@@ -303,3 +312,28 @@ func (t *QbittorrentListItem) toTorrentInfo() TorrentInfo {
Availability: t.Availability,
}
}
func (c *QbittorrentClient) DefaultSavePath() (string, error) {
req, err := http.NewRequest("GET", c.baseURL+"/api/v2/app/defaultSavePath", nil)
if err != nil {
return "", fmt.Errorf("creating request: %w", err)
}
req.AddCookie(&http.Cookie{Name: "SID", Value: c.sid})
resp, err := c.client.Do(req)
if err != nil {
return "", fmt.Errorf("requesting default save path: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("default save path returned status %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("reading response: %w", err)
}
return strings.TrimSpace(string(body)), nil
}
+1 -1
View File
@@ -51,7 +51,7 @@ func (service *TorrentService) Add(req *pb.AddRequest) (*pb.AddResponse, error)
return nil, fmt.Errorf("either torrent_data or download_url must be provided")
}
if err := service.client.AddTorrent(file); err != nil {
if err := service.client.AddTorrent(file, ""); err != nil {
return nil, err
}