Files
music-agregator/internal/database/db.go
T

762 lines
23 KiB
Go

package database
import (
"context"
"encoding/json"
"regexp"
"strings"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
type DB struct {
pool *pgxpool.Pool
}
func New(ctx context.Context, databaseURL string) (*DB, error) {
pool, err := pgxpool.New(ctx, databaseURL)
if err != nil {
return nil, err
}
if err := pool.Ping(ctx); err != nil {
return nil, err
}
return &DB{pool: pool}, nil
}
func (db *DB) Close() {
db.pool.Close()
}
type Artist struct {
ID string
Name string
SortName string
ArtistType string
Description string
Genres []Genre
ExternalIDs []ExternalID
}
type Album struct {
ID string
Title string
AlbumType string
ReleaseDate string
Genres []Genre
}
type Genre struct {
ID string `json:"id"`
Name string `json:"name"`
}
type ExternalID struct {
Source string `json:"source"`
SourceID string `json:"source_id"`
URL string `json:"url"`
}
type ArtistMetadataRow struct {
ID uuid.UUID `json:"id"`
ForeignArtistID *string `json:"foreign_artist_id"`
Name string `json:"name"`
SortName *string `json:"sort_name"`
ArtistType *string `json:"artist_type"`
Genres json.RawMessage `json:"genres"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type AlbumRow struct {
ID uuid.UUID `json:"id"`
ArtistMetadataID uuid.UUID `json:"artist_metadata_id"`
ForeignAlbumID *string `json:"foreign_album_id"`
Title string `json:"title"`
AlbumType *string `json:"album_type"`
ReleaseDate *time.Time `json:"release_date"`
Monitored bool `json:"monitored"`
AddedAt time.Time `json:"added_at"`
}
type AlbumWithArtistRow struct {
ID uuid.UUID `json:"id"`
ForeignAlbumID *string `json:"foreign_album_id"`
Title string `json:"title"`
AlbumType *string `json:"album_type"`
ReleaseDate *time.Time `json:"release_date"`
Monitored bool `json:"monitored"`
AddedAt time.Time `json:"added_at"`
ArtistID uuid.UUID `json:"artist_id"`
ArtistName string `json:"artist_name"`
}
func (db *DB) UpsertArtistMetadata(ctx context.Context, artist *Artist) (uuid.UUID, error) {
id, err := uuid.Parse(artist.ID)
if err != nil {
id = uuid.New()
}
genres, _ := json.Marshal(artist.Genres)
links, _ := json.Marshal(artist.ExternalIDs)
var resultID uuid.UUID
err = db.pool.QueryRow(ctx, `
INSERT INTO artist_metadata (
id, foreign_artist_id, name, sort_name, disambiguation,
artist_type, status, overview, genres, links, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW())
ON CONFLICT (foreign_artist_id) DO UPDATE SET
name = EXCLUDED.name,
sort_name = EXCLUDED.sort_name,
artist_type = EXCLUDED.artist_type,
overview = EXCLUDED.overview,
genres = EXCLUDED.genres,
links = EXCLUDED.links,
updated_at = NOW()
RETURNING id
`, id, artist.ID, artist.Name, artist.SortName, artist.Description,
artist.ArtistType, "active", artist.Description, genres, links).Scan(&resultID)
return resultID, err
}
var cleanTitleRegex = regexp.MustCompile(`[^a-z0-9]`)
func (db *DB) UpsertAlbum(ctx context.Context, album *Album, artistMetadataID uuid.UUID) (uuid.UUID, error) {
id, err := uuid.Parse(album.ID)
if err != nil {
id = uuid.New()
}
genres, _ := json.Marshal(album.Genres)
images, _ := json.Marshal([]any{})
var releaseDate *time.Time
if album.ReleaseDate != "" {
if t, err := time.Parse("2006-01-02", album.ReleaseDate); err == nil {
releaseDate = &t
}
}
cleanTitle := cleanTitleRegex.ReplaceAllString(strings.ToLower(album.Title), "")
var resultID uuid.UUID
err = db.pool.QueryRow(ctx, `
INSERT INTO albums (
id, artist_metadata_id, foreign_album_id, title, clean_title,
overview, album_type, release_date, images, genres
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
ON CONFLICT (foreign_album_id) DO UPDATE SET
title = EXCLUDED.title,
album_type = EXCLUDED.album_type,
release_date = EXCLUDED.release_date,
genres = EXCLUDED.genres
RETURNING id
`, id, artistMetadataID, album.ID, album.Title, cleanTitle,
"", album.AlbumType, releaseDate, images, genres).Scan(&resultID)
return resultID, err
}
func (db *DB) ListArtists(ctx context.Context) ([]ArtistMetadataRow, error) {
rows, err := db.pool.Query(ctx, `
SELECT id, foreign_artist_id, name, sort_name, artist_type, genres, created_at, updated_at
FROM artist_metadata
ORDER BY name
`)
if err != nil {
return nil, err
}
defer rows.Close()
var artists []ArtistMetadataRow
for rows.Next() {
var a ArtistMetadataRow
err := rows.Scan(&a.ID, &a.ForeignArtistID, &a.Name, &a.SortName, &a.ArtistType, &a.Genres, &a.CreatedAt, &a.UpdatedAt)
if err != nil {
return nil, err
}
artists = append(artists, a)
}
return artists, nil
}
func (db *DB) ListAlbumsByArtist(ctx context.Context, artistMetadataID uuid.UUID) ([]AlbumRow, error) {
rows, err := db.pool.Query(ctx, `
SELECT id, artist_metadata_id, foreign_album_id, title, album_type, release_date, monitored, added_at
FROM albums
WHERE artist_metadata_id = $1
ORDER BY release_date DESC NULLS LAST
`, artistMetadataID)
if err != nil {
return nil, err
}
defer rows.Close()
var albums []AlbumRow
for rows.Next() {
var a AlbumRow
err := rows.Scan(&a.ID, &a.ArtistMetadataID, &a.ForeignAlbumID, &a.Title, &a.AlbumType, &a.ReleaseDate, &a.Monitored, &a.AddedAt)
if err != nil {
return nil, err
}
albums = append(albums, a)
}
return albums, nil
}
func (db *DB) ListAllAlbums(ctx context.Context) ([]AlbumWithArtistRow, error) {
rows, err := db.pool.Query(ctx, `
SELECT
a.id, a.foreign_album_id, a.title, a.album_type, a.release_date, a.monitored, a.added_at,
am.id as artist_id, am.name as artist_name
FROM albums a
JOIN artist_metadata am ON a.artist_metadata_id = am.id
ORDER BY a.added_at DESC
`)
if err != nil {
return nil, err
}
defer rows.Close()
var albums []AlbumWithArtistRow
for rows.Next() {
var a AlbumWithArtistRow
err := rows.Scan(&a.ID, &a.ForeignAlbumID, &a.Title, &a.AlbumType, &a.ReleaseDate, &a.Monitored, &a.AddedAt, &a.ArtistID, &a.ArtistName)
if err != nil {
return nil, err
}
albums = append(albums, a)
}
return albums, nil
}
func (db *DB) CountArtists(ctx context.Context) (int64, error) {
var count int64
err := db.pool.QueryRow(ctx, "SELECT COUNT(*) FROM artist_metadata").Scan(&count)
return count, err
}
func (db *DB) CountAlbums(ctx context.Context) (int64, error) {
var count int64
err := db.pool.QueryRow(ctx, "SELECT COUNT(*) FROM albums").Scan(&count)
return count, err
}
func (db *DB) CountAlbumsByArtist(ctx context.Context, artistMetadataID uuid.UUID) (int64, error) {
var count int64
err := db.pool.QueryRow(ctx, `
SELECT COUNT(*) FROM albums WHERE artist_metadata_id = $1
`, artistMetadataID).Scan(&count)
return count, err
}
func (db *DB) TouchArtistUpdatedAt(ctx context.Context, artistMetadataID uuid.UUID) error {
_, err := db.pool.Exec(ctx, `
UPDATE artist_metadata SET updated_at = NOW() WHERE id = $1
`, artistMetadataID)
return err
}
func (db *DB) DeleteArtistByForeignID(ctx context.Context, foreignArtistID string) (bool, error) {
result, err := db.pool.Exec(ctx, `
DELETE FROM artist_metadata WHERE foreign_artist_id = $1
`, foreignArtistID)
if err != nil {
return false, err
}
return result.RowsAffected() > 0, nil
}
type ArtistRow struct {
ID uuid.UUID `json:"id"`
MetadataID uuid.UUID `json:"metadata_id"`
ForeignArtistID string `json:"foreign_artist_id"`
Name string `json:"name"`
QualityProfileID *uuid.UUID `json:"quality_profile_id"`
MetadataProfileID *uuid.UUID `json:"metadata_profile_id"`
RootFolderID *uuid.UUID `json:"root_folder_id"`
Path *string `json:"path"`
Monitored bool `json:"monitored"`
MonitorNewItems string `json:"monitor_new_items"`
}
func (db *DB) UpsertArtist(ctx context.Context, metadataID uuid.UUID) (uuid.UUID, error) {
var existingID uuid.UUID
err := db.pool.QueryRow(ctx, `
SELECT id FROM artists WHERE metadata_id = $1
`, metadataID).Scan(&existingID)
if err == nil {
return existingID, nil
}
var resultID uuid.UUID
err = db.pool.QueryRow(ctx, `
INSERT INTO artists (metadata_id, monitored, monitor_new_items)
VALUES ($1, true, 'all')
RETURNING id
`, metadataID).Scan(&resultID)
return resultID, err
}
func (db *DB) GetArtistByForeignID(ctx context.Context, foreignArtistID string) (*ArtistRow, error) {
var a ArtistRow
err := db.pool.QueryRow(ctx, `
SELECT a.id, a.metadata_id, am.foreign_artist_id, am.name,
a.quality_profile_id, a.metadata_profile_id, a.root_folder_id,
a.path, a.monitored, a.monitor_new_items
FROM artists a
JOIN artist_metadata am ON a.metadata_id = am.id
WHERE am.foreign_artist_id = $1
`, foreignArtistID).Scan(
&a.ID, &a.MetadataID, &a.ForeignArtistID, &a.Name,
&a.QualityProfileID, &a.MetadataProfileID, &a.RootFolderID,
&a.Path, &a.Monitored, &a.MonitorNewItems,
)
if err != nil {
return nil, err
}
return &a, nil
}
type ArtistUpdate struct {
QualityProfileID *string `json:"quality_profile_id"`
MetadataProfileID *string `json:"metadata_profile_id"`
RootFolderID *string `json:"root_folder_id"`
Path *string `json:"path"`
Monitored *bool `json:"monitored"`
MonitorNewItems *string `json:"monitor_new_items"`
}
func (db *DB) UpdateArtistByForeignID(ctx context.Context, foreignArtistID string, update ArtistUpdate) (*ArtistRow, error) {
metadataRow, err := db.GetArtistMetadataByForeignID(ctx, foreignArtistID)
if err != nil {
return nil, err
}
if update.Monitored != nil {
_, err = db.pool.Exec(ctx, `
UPDATE artists SET monitored = $1 WHERE metadata_id = $2
`, *update.Monitored, metadataRow.ID)
if err != nil {
return nil, err
}
}
if update.Path != nil {
_, err = db.pool.Exec(ctx, `
UPDATE artists SET path = $1 WHERE metadata_id = $2
`, *update.Path, metadataRow.ID)
if err != nil {
return nil, err
}
}
if update.QualityProfileID != nil {
var qpID *uuid.UUID
if *update.QualityProfileID != "" {
parsed, err := uuid.Parse(*update.QualityProfileID)
if err == nil {
qpID = &parsed
}
}
_, err = db.pool.Exec(ctx, `
UPDATE artists SET quality_profile_id = $1 WHERE metadata_id = $2
`, qpID, metadataRow.ID)
if err != nil {
return nil, err
}
}
if update.RootFolderID != nil {
var rfID *uuid.UUID
if *update.RootFolderID != "" {
parsed, err := uuid.Parse(*update.RootFolderID)
if err == nil {
rfID = &parsed
}
}
_, err = db.pool.Exec(ctx, `
UPDATE artists SET root_folder_id = $1 WHERE metadata_id = $2
`, rfID, metadataRow.ID)
if err != nil {
return nil, err
}
}
if update.MonitorNewItems != nil {
_, err = db.pool.Exec(ctx, `
UPDATE artists SET monitor_new_items = $1 WHERE metadata_id = $2
`, *update.MonitorNewItems, metadataRow.ID)
if err != nil {
return nil, err
}
}
return db.GetArtistByForeignID(ctx, foreignArtistID)
}
func (db *DB) GetArtistMetadataByForeignID(ctx context.Context, foreignArtistID string) (*ArtistMetadataRow, error) {
var a ArtistMetadataRow
err := db.pool.QueryRow(ctx, `
SELECT id, foreign_artist_id, name, sort_name, artist_type, genres, created_at, updated_at
FROM artist_metadata
WHERE foreign_artist_id = $1
`, foreignArtistID).Scan(&a.ID, &a.ForeignArtistID, &a.Name, &a.SortName, &a.ArtistType, &a.Genres, &a.CreatedAt, &a.UpdatedAt)
if err != nil {
return nil, err
}
return &a, nil
}
func (db *DB) GetAlbumByID(ctx context.Context, albumID uuid.UUID) (*AlbumRow, error) {
var a AlbumRow
err := db.pool.QueryRow(ctx, `
SELECT id, artist_metadata_id, foreign_album_id, title, album_type, release_date, monitored, added_at
FROM albums WHERE id = $1
`, albumID).Scan(&a.ID, &a.ArtistMetadataID, &a.ForeignAlbumID, &a.Title, &a.AlbumType, &a.ReleaseDate, &a.Monitored, &a.AddedAt)
if err != nil {
return nil, err
}
return &a, nil
}
type AlbumDetailRow struct {
ID uuid.UUID `json:"id"`
ArtistMetadataID uuid.UUID `json:"artist_metadata_id"`
ForeignAlbumID *string `json:"foreign_album_id"`
Title string `json:"title"`
AlbumType *string `json:"album_type"`
ReleaseDate *time.Time `json:"release_date"`
Monitored bool `json:"monitored"`
AddedAt time.Time `json:"added_at"`
ArtistName string `json:"artist_name"`
ForeignArtistID *string `json:"foreign_artist_id"`
}
func (db *DB) GetAlbumDetailByID(ctx context.Context, albumID uuid.UUID) (*AlbumDetailRow, error) {
var a AlbumDetailRow
err := db.pool.QueryRow(ctx, `
SELECT a.id, a.artist_metadata_id, a.foreign_album_id, a.title, a.album_type,
a.release_date, a.monitored, a.added_at, am.name, am.foreign_artist_id
FROM albums a
JOIN artist_metadata am ON a.artist_metadata_id = am.id
WHERE a.id = $1
`, albumID).Scan(&a.ID, &a.ArtistMetadataID, &a.ForeignAlbumID, &a.Title, &a.AlbumType,
&a.ReleaseDate, &a.Monitored, &a.AddedAt, &a.ArtistName, &a.ForeignArtistID)
if err != nil {
return nil, err
}
return &a, nil
}
func (db *DB) UpdateAlbumMonitored(ctx context.Context, albumID uuid.UUID, monitored bool) error {
_, err := db.pool.Exec(ctx, `
UPDATE albums SET monitored = $1 WHERE id = $2
`, monitored, albumID)
return err
}
func (db *DB) BulkUpdateAlbumsMonitored(ctx context.Context, artistMetadataID uuid.UUID, monitored bool) (int64, error) {
result, err := db.pool.Exec(ctx, `
UPDATE albums SET monitored = $1 WHERE artist_metadata_id = $2
`, monitored, artistMetadataID)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
func (db *DB) GetMonitoredAlbumsByArtist(ctx context.Context, artistMetadataID uuid.UUID) ([]AlbumRow, error) {
rows, err := db.pool.Query(ctx, `
SELECT id, artist_metadata_id, foreign_album_id, title, album_type, release_date, monitored, added_at
FROM albums
WHERE artist_metadata_id = $1 AND monitored = true
ORDER BY release_date DESC NULLS LAST
`, artistMetadataID)
if err != nil {
return nil, err
}
defer rows.Close()
var albums []AlbumRow
for rows.Next() {
var a AlbumRow
err := rows.Scan(&a.ID, &a.ArtistMetadataID, &a.ForeignAlbumID, &a.Title, &a.AlbumType, &a.ReleaseDate, &a.Monitored, &a.AddedAt)
if err != nil {
return nil, err
}
albums = append(albums, a)
}
return albums, nil
}
type WantedAlbumRow struct {
ID uuid.UUID `json:"id"`
AlbumID uuid.UUID `json:"album_id"`
Priority int `json:"priority"`
SearchCount int `json:"search_count"`
LastSearchedAt *time.Time `json:"last_searched_at"`
AddedAt time.Time `json:"added_at"`
}
func (db *DB) AddToWantedAlbums(ctx context.Context, albumID uuid.UUID) error {
_, err := db.pool.Exec(ctx, `
INSERT INTO wanted_albums (album_id)
VALUES ($1)
ON CONFLICT (album_id) DO NOTHING
`, albumID)
return err
}
func (db *DB) RemoveFromWantedAlbums(ctx context.Context, albumID uuid.UUID) error {
_, err := db.pool.Exec(ctx, `
DELETE FROM wanted_albums WHERE album_id = $1
`, albumID)
return err
}
func (db *DB) IsAlbumWanted(ctx context.Context, albumID uuid.UUID) (bool, error) {
var count int64
err := db.pool.QueryRow(ctx, `
SELECT COUNT(*) FROM wanted_albums WHERE album_id = $1
`, albumID).Scan(&count)
return count > 0, err
}
func (db *DB) HasTrackFiles(ctx context.Context, albumID uuid.UUID) (bool, error) {
var count int64
err := db.pool.QueryRow(ctx, `
SELECT COUNT(*) FROM track_files WHERE album_id = $1
`, albumID).Scan(&count)
return count > 0, err
}
type BlocklistEntry struct {
ID uuid.UUID `json:"id"`
ArtistID uuid.UUID `json:"artist_id"`
AlbumID uuid.UUID `json:"album_id"`
SourceTitle string `json:"source_title"`
TorrentHash *string `json:"torrent_hash"`
Indexer *string `json:"indexer"`
Message *string `json:"message"`
}
func (db *DB) AddToBlocklist(ctx context.Context, artistID, albumID uuid.UUID, sourceTitle string, torrentHash, indexer *string) error {
_, err := db.pool.Exec(ctx, `
INSERT INTO blocklist (artist_id, album_id, source_title, torrent_hash, indexer)
VALUES ($1, $2, $3, $4, $5)
`, artistID, albumID, sourceTitle, torrentHash, indexer)
return err
}
func (db *DB) IsBlocklisted(ctx context.Context, sourceTitle string, torrentHash *string) (bool, error) {
var count int64
if torrentHash != nil && *torrentHash != "" {
err := db.pool.QueryRow(ctx, `
SELECT COUNT(*) FROM blocklist WHERE source_title = $1 OR torrent_hash = $2
`, sourceTitle, *torrentHash).Scan(&count)
return count > 0, err
}
err := db.pool.QueryRow(ctx, `
SELECT COUNT(*) FROM blocklist WHERE source_title = $1
`, sourceTitle).Scan(&count)
return count > 0, err
}
func (db *DB) ListBlocklist(ctx context.Context) ([]BlocklistEntry, error) {
rows, err := db.pool.Query(ctx, `
SELECT id, artist_id, album_id, source_title, torrent_hash, indexer, message
FROM blocklist ORDER BY date DESC
`)
if err != nil {
return nil, err
}
defer rows.Close()
var entries []BlocklistEntry
for rows.Next() {
var e BlocklistEntry
err := rows.Scan(&e.ID, &e.ArtistID, &e.AlbumID, &e.SourceTitle, &e.TorrentHash, &e.Indexer, &e.Message)
if err != nil {
return nil, err
}
entries = append(entries, e)
}
return entries, nil
}
func (db *DB) GetArtistIDByAlbum(ctx context.Context, albumID uuid.UUID) (*uuid.UUID, error) {
var artistID uuid.UUID
err := db.pool.QueryRow(ctx, `
SELECT ar.id FROM artists ar
JOIN artist_metadata am ON ar.metadata_id = am.id
JOIN albums a ON a.artist_metadata_id = am.id
WHERE a.id = $1
`, albumID).Scan(&artistID)
if err != nil {
return nil, err
}
return &artistID, nil
}
type DownloadQueueRow struct {
ID uuid.UUID `json:"id"`
ArtistID *uuid.UUID `json:"artist_id"`
AlbumID *uuid.UUID `json:"album_id"`
DownloadID *string `json:"download_id"`
Title string `json:"title"`
Size int64 `json:"size"`
SizeLeft int64 `json:"size_left"`
Status string `json:"status"`
Progress float32 `json:"progress"`
ErrorMessage *string `json:"error_message"`
Protocol string `json:"protocol"`
Indexer *string `json:"indexer"`
DownloadClient *string `json:"download_client"`
TorrentHash *string `json:"torrent_hash"`
OutputPath *string `json:"output_path"`
AddedAt time.Time `json:"added_at"`
CompletedAt *time.Time `json:"completed_at"`
}
func (db *DB) AddToDownloadQueue(ctx context.Context, title string, size int64, torrentHash, indexer *string, albumID, artistID *uuid.UUID) (uuid.UUID, error) {
var id uuid.UUID
err := db.pool.QueryRow(ctx, `
INSERT INTO download_queue (title, size, torrent_hash, indexer, album_id, artist_id, status)
VALUES ($1, $2, $3, $4, $5, $6, 'queued')
RETURNING id
`, title, size, torrentHash, indexer, albumID, artistID).Scan(&id)
return id, err
}
func (db *DB) GetDownloadQueueItem(ctx context.Context, id uuid.UUID) (*DownloadQueueRow, error) {
var row DownloadQueueRow
err := db.pool.QueryRow(ctx, `
SELECT id, artist_id, album_id, download_id, title, size, size_left, status, progress,
error_message, protocol, indexer, download_client, torrent_hash, output_path, added_at, completed_at
FROM download_queue WHERE id = $1
`, id).Scan(&row.ID, &row.ArtistID, &row.AlbumID, &row.DownloadID, &row.Title, &row.Size,
&row.SizeLeft, &row.Status, &row.Progress, &row.ErrorMessage, &row.Protocol, &row.Indexer,
&row.DownloadClient, &row.TorrentHash, &row.OutputPath, &row.AddedAt, &row.CompletedAt)
if err != nil {
return nil, err
}
return &row, nil
}
func (db *DB) ListDownloadQueue(ctx context.Context, status *string) ([]DownloadQueueRow, error) {
var rows []DownloadQueueRow
var query string
var args []any
if status != nil {
query = `
SELECT id, artist_id, album_id, download_id, title, size, size_left, status, progress,
error_message, protocol, indexer, download_client, torrent_hash, output_path, added_at, completed_at
FROM download_queue WHERE status = $1 ORDER BY added_at DESC
`
args = []any{*status}
} else {
query = `
SELECT id, artist_id, album_id, download_id, title, size, size_left, status, progress,
error_message, protocol, indexer, download_client, torrent_hash, output_path, added_at, completed_at
FROM download_queue ORDER BY added_at DESC
`
}
dbRows, err := db.pool.Query(ctx, query, args...)
if err != nil {
return nil, err
}
defer dbRows.Close()
for dbRows.Next() {
var row DownloadQueueRow
err := dbRows.Scan(&row.ID, &row.ArtistID, &row.AlbumID, &row.DownloadID, &row.Title, &row.Size,
&row.SizeLeft, &row.Status, &row.Progress, &row.ErrorMessage, &row.Protocol, &row.Indexer,
&row.DownloadClient, &row.TorrentHash, &row.OutputPath, &row.AddedAt, &row.CompletedAt)
if err != nil {
return nil, err
}
rows = append(rows, row)
}
return rows, nil
}
func (db *DB) UpdateDownloadQueueStatus(ctx context.Context, id uuid.UUID, status string, errorMessage *string) error {
if status == "completed" {
_, err := db.pool.Exec(ctx, `
UPDATE download_queue SET status = $1, completed_at = NOW() WHERE id = $2
`, status, id)
return err
}
if errorMessage != nil {
_, err := db.pool.Exec(ctx, `
UPDATE download_queue SET status = $1, error_message = $2 WHERE id = $3
`, status, *errorMessage, id)
return err
}
_, err := db.pool.Exec(ctx, `
UPDATE download_queue SET status = $1 WHERE id = $2
`, status, id)
return err
}
func (db *DB) UpdateDownloadQueueProgress(ctx context.Context, id uuid.UUID, progress float32, sizeLeft int64, status string) error {
_, err := db.pool.Exec(ctx, `
UPDATE download_queue SET progress = $1, size_left = $2, status = $3 WHERE id = $4
`, progress, sizeLeft, status, id)
return err
}
func (db *DB) DeleteDownloadQueueItem(ctx context.Context, id uuid.UUID) error {
_, err := db.pool.Exec(ctx, `DELETE FROM download_queue WHERE id = $1`, id)
return err
}
func (db *DB) GetDownloadQueueByTorrentHash(ctx context.Context, hash string) (*DownloadQueueRow, error) {
var row DownloadQueueRow
err := db.pool.QueryRow(ctx, `
SELECT id, artist_id, album_id, download_id, title, size, size_left, status, progress,
error_message, protocol, indexer, download_client, torrent_hash, output_path, added_at, completed_at
FROM download_queue WHERE torrent_hash = $1
`, hash).Scan(&row.ID, &row.ArtistID, &row.AlbumID, &row.DownloadID, &row.Title, &row.Size,
&row.SizeLeft, &row.Status, &row.Progress, &row.ErrorMessage, &row.Protocol, &row.Indexer,
&row.DownloadClient, &row.TorrentHash, &row.OutputPath, &row.AddedAt, &row.CompletedAt)
if err != nil {
return nil, err
}
return &row, nil
}
type DownloadQueueStats struct {
Total int64 `json:"total"`
Downloading int64 `json:"downloading"`
Queued int64 `json:"queued"`
Completed int64 `json:"completed"`
Failed int64 `json:"failed"`
}
func (db *DB) GetDownloadQueueStats(ctx context.Context) (*DownloadQueueStats, error) {
var stats DownloadQueueStats
err := db.pool.QueryRow(ctx, `
SELECT
COUNT(*) as total,
COUNT(*) FILTER (WHERE status = 'downloading') as downloading,
COUNT(*) FILTER (WHERE status = 'queued') as queued,
COUNT(*) FILTER (WHERE status = 'completed') as completed,
COUNT(*) FILTER (WHERE status = 'failed') as failed
FROM download_queue
`).Scan(&stats.Total, &stats.Downloading, &stats.Queued, &stats.Completed, &stats.Failed)
return &stats, err
}