WIP
This commit is contained in:
+160
-16
@@ -1,13 +1,20 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/anacrolix/torrent/metainfo"
|
||||
"github.com/fujin/music-agregator/internal/database"
|
||||
"github.com/fujin/music-agregator/internal/indexer"
|
||||
"github.com/fujin/music-agregator/internal/metadata"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -36,6 +43,7 @@ type AlbumSyncResult struct {
|
||||
DownloadStatus *DownloadStatus `json:"download_status,omitempty"`
|
||||
TorrentHash *string `json:"torrent_hash,omitempty"`
|
||||
Indexer *string `json:"indexer,omitempty"`
|
||||
JobID *string `json:"job_id,omitempty"`
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
@@ -53,6 +61,18 @@ type downloadResult struct {
|
||||
torrentHash *string
|
||||
indexer *string
|
||||
err *string
|
||||
queueID *string
|
||||
}
|
||||
|
||||
type downloadContext struct {
|
||||
artistName string
|
||||
albumTitle string
|
||||
year *uint32
|
||||
artistID *uuid.UUID
|
||||
albumID *uuid.UUID
|
||||
indexerService *IndexerService
|
||||
torrentService *TorrentService
|
||||
db *database.DB
|
||||
}
|
||||
|
||||
func Sync(
|
||||
@@ -153,7 +173,7 @@ func Sync(
|
||||
}
|
||||
|
||||
var downloadStatus *DownloadStatus
|
||||
var torrentHash, indexerName, dlError *string
|
||||
var torrentHash, indexerName, dlError, jobID *string
|
||||
|
||||
if options.Download {
|
||||
var year *uint32
|
||||
@@ -167,11 +187,38 @@ func Sync(
|
||||
}
|
||||
}
|
||||
|
||||
dlResult := downloadAlbum(ctx, artist.Name, album.Title, year, indexerService, torrentService)
|
||||
var artistUUID, albumUUID *uuid.UUID
|
||||
if artistMetadataID != nil {
|
||||
if id, err := uuid.Parse(*artistMetadataID); err == nil {
|
||||
artistUUID = &id
|
||||
if artistRow, err := db.GetArtistByForeignID(ctx, artist.Id); err == nil {
|
||||
artistUUID = &artistRow.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
if albumID, err := uuid.Parse(album.Id); err == nil {
|
||||
if albumRow, err := db.GetAlbumByID(ctx, albumID); err == nil {
|
||||
albumUUID = &albumRow.ID
|
||||
}
|
||||
}
|
||||
|
||||
dlCtx := &downloadContext{
|
||||
artistName: artist.Name,
|
||||
albumTitle: album.Title,
|
||||
year: year,
|
||||
artistID: artistUUID,
|
||||
albumID: albumUUID,
|
||||
indexerService: indexerService,
|
||||
torrentService: torrentService,
|
||||
db: db,
|
||||
}
|
||||
|
||||
dlResult := downloadAlbum(ctx, dlCtx)
|
||||
downloadStatus = &dlResult.status
|
||||
torrentHash = dlResult.torrentHash
|
||||
indexerName = dlResult.indexer
|
||||
dlError = dlResult.err
|
||||
jobID = dlResult.queueID
|
||||
|
||||
switch dlResult.status {
|
||||
case DownloadStatusAdded:
|
||||
@@ -190,6 +237,7 @@ func Sync(
|
||||
DownloadStatus: downloadStatus,
|
||||
TorrentHash: torrentHash,
|
||||
Indexer: indexerName,
|
||||
JobID: jobID,
|
||||
Error: dlError,
|
||||
})
|
||||
}
|
||||
@@ -206,39 +254,63 @@ func Sync(
|
||||
}, nil
|
||||
}
|
||||
|
||||
func downloadAlbum(
|
||||
ctx context.Context,
|
||||
artistName, albumTitle string,
|
||||
year *uint32,
|
||||
indexerService *IndexerService,
|
||||
torrentService *TorrentService,
|
||||
) downloadResult {
|
||||
albumStr := albumTitle
|
||||
func downloadAlbum(ctx context.Context, dlCtx *downloadContext) downloadResult {
|
||||
albumStr := dlCtx.albumTitle
|
||||
criteria := &indexer.MusicSearchCriteria{
|
||||
Artist: artistName,
|
||||
Artist: dlCtx.artistName,
|
||||
Album: &albumStr,
|
||||
Year: year,
|
||||
Year: dlCtx.year,
|
||||
Limit: 20,
|
||||
Offset: 0,
|
||||
}
|
||||
|
||||
searchResults, err := indexerService.Search(ctx, criteria, nil)
|
||||
log.Info().
|
||||
Str("artist", dlCtx.artistName).
|
||||
Str("album", dlCtx.albumTitle).
|
||||
Interface("year", dlCtx.year).
|
||||
Msg("[DOWNLOAD] searching indexers")
|
||||
|
||||
searchResults, err := dlCtx.indexerService.Search(ctx, criteria, nil)
|
||||
if err != nil {
|
||||
errStr := "indexer search failed: " + err.Error()
|
||||
log.Error().Err(err).Str("artist", dlCtx.artistName).Str("album", dlCtx.albumTitle).Msg("[DOWNLOAD] indexer search failed")
|
||||
return downloadResult{
|
||||
status: DownloadStatusFailed,
|
||||
err: &errStr,
|
||||
}
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Int("results", len(searchResults)).
|
||||
Str("artist", dlCtx.artistName).
|
||||
Str("album", dlCtx.albumTitle).
|
||||
Msg("[DOWNLOAD] search completed")
|
||||
|
||||
if len(searchResults) == 0 {
|
||||
log.Warn().Str("artist", dlCtx.artistName).Str("album", dlCtx.albumTitle).Msg("[DOWNLOAD] no results found")
|
||||
return downloadResult{status: DownloadStatusNoResults}
|
||||
}
|
||||
|
||||
best := selectBestResult(searchResults)
|
||||
|
||||
if err := torrentService.AddTorrentURL(ctx, best.DownloadURL, nil); err != nil {
|
||||
errStr := "failed to add torrent: " + err.Error()
|
||||
seeders := 0
|
||||
if best.Seeders != nil {
|
||||
seeders = *best.Seeders
|
||||
}
|
||||
log.Info().
|
||||
Str("title", best.Title).
|
||||
Str("indexer", best.Indexer).
|
||||
Int("seeders", seeders).
|
||||
Uint64("size_bytes", best.Size).
|
||||
Interface("infohash", best.Infohash).
|
||||
Msg("[DOWNLOAD] selected best result")
|
||||
|
||||
log.Info().Str("url", best.DownloadURL).Msg("[DOWNLOAD] fetching torrent file")
|
||||
|
||||
torrent, err := fetchTorrentFile(ctx, best.DownloadURL)
|
||||
if err != nil {
|
||||
errStr := "failed to fetch torrent file: " + err.Error()
|
||||
log.Error().Err(err).Str("url", best.DownloadURL).Msg("[DOWNLOAD] failed to fetch torrent file")
|
||||
return downloadResult{
|
||||
status: DownloadStatusFailed,
|
||||
indexer: &best.Indexer,
|
||||
@@ -246,10 +318,41 @@ func downloadAlbum(
|
||||
}
|
||||
}
|
||||
|
||||
log.Info().Int("size_bytes", len(torrent.Data)).Str("infohash", torrent.InfoHash).Msg("[DOWNLOAD] adding torrent file to client")
|
||||
|
||||
if err := dlCtx.torrentService.AddTorrentFile(ctx, torrent.Data, nil); err != nil {
|
||||
errStr := "failed to add torrent: " + err.Error()
|
||||
log.Error().Err(err).Msg("[DOWNLOAD] failed to add torrent")
|
||||
return downloadResult{
|
||||
status: DownloadStatusFailed,
|
||||
indexer: &best.Indexer,
|
||||
err: &errStr,
|
||||
}
|
||||
}
|
||||
|
||||
log.Info().Str("indexer", best.Indexer).Str("hash", torrent.InfoHash).Msg("[DOWNLOAD] torrent added successfully")
|
||||
|
||||
infoHash := torrent.InfoHash
|
||||
|
||||
var queueIDStr *string
|
||||
if dlCtx.db != nil {
|
||||
title := dlCtx.artistName + " - " + dlCtx.albumTitle
|
||||
size := int64(best.Size)
|
||||
queueID, err := dlCtx.db.AddToDownloadQueue(ctx, title, size, &infoHash, &best.Indexer, dlCtx.albumID, dlCtx.artistID)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("title", title).Msg("[DOWNLOAD] failed to add to download queue")
|
||||
} else {
|
||||
idStr := queueID.String()
|
||||
queueIDStr = &idStr
|
||||
log.Info().Str("queue_id", idStr).Str("title", title).Str("hash", infoHash).Msg("[DOWNLOAD] added to download queue")
|
||||
}
|
||||
}
|
||||
|
||||
return downloadResult{
|
||||
status: DownloadStatusAdded,
|
||||
torrentHash: best.Infohash,
|
||||
torrentHash: &infoHash,
|
||||
indexer: &best.Indexer,
|
||||
queueID: queueIDStr,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,6 +380,47 @@ func selectBestResult(results []indexer.SearchResult) *indexer.SearchResult {
|
||||
return best
|
||||
}
|
||||
|
||||
type torrentFile struct {
|
||||
Data []byte
|
||||
InfoHash string
|
||||
}
|
||||
|
||||
// fetchTorrentFile downloads a .torrent file from the given URL and extracts infohash.
|
||||
// This is necessary because the torrent client may be on a different network
|
||||
// (e.g., behind VPN) and cannot access the indexer directly.
|
||||
func fetchTorrentFile(ctx context.Context, url string) (*torrentFile, error) {
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetch torrent: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read response: %w", err)
|
||||
}
|
||||
|
||||
mi, err := metainfo.Load(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse torrent: %w", err)
|
||||
}
|
||||
|
||||
hash := mi.HashInfoBytes().HexString()
|
||||
|
||||
return &torrentFile{Data: data, InfoHash: hash}, nil
|
||||
}
|
||||
|
||||
func parseUUID(s string) ([16]byte, error) {
|
||||
var id [16]byte
|
||||
s = strings.ReplaceAll(s, "-", "")
|
||||
|
||||
Reference in New Issue
Block a user