Files
unified-media-manager/internal/service/blocklist.go
2026-04-24 10:45:19 -07:00

183 lines
5.4 KiB
Go

package service
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"log/slog"
"time"
"github.com/TopherMayor/unified-media-manager/internal/db"
)
type BlocklistItem struct {
ID int64 `json:"id"`
ReleaseTitle string `json:"release_title"`
SourceTitle *string `json:"source_title,omitempty"`
Quality json.RawMessage `json:"quality"`
Indexer *string `json:"indexer,omitempty"`
Protocol string `json:"protocol"`
TorrentHash *string `json:"torrent_hash,omitempty"`
Size *int64 `json:"size,omitempty"`
Message *string `json:"message,omitempty"`
MediaID *int64 `json:"media_id,omitempty"`
BlockReason string `json:"block_reason"`
AutoExpiresAt *time.Time `json:"auto_expires_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
type BlocklistFilters struct {
Page int
PageSize int
}
type AddBlocklistRequest struct {
ReleaseTitle string `json:"release_title"`
SourceTitle *string `json:"source_title,omitempty"`
Quality json.RawMessage `json:"quality,omitempty"`
Indexer *string `json:"indexer,omitempty"`
Protocol string `json:"protocol,omitempty"`
TorrentHash *string `json:"torrent_hash,omitempty"`
Size *int64 `json:"size,omitempty"`
Message *string `json:"message,omitempty"`
MediaID *int64 `json:"media_id,omitempty"`
BlockReason *string `json:"block_reason,omitempty"`
AutoExpiresAt *time.Time `json:"auto_expires_at,omitempty"`
}
const blocklistColumns = `id, release_title, source_title, quality, indexer, protocol,
torrent_hash, size, message, media_id, block_reason, auto_expires_at, created_at`
type BlocklistService struct {
db *db.DB
}
func NewBlocklistService(database *db.DB) *BlocklistService {
return &BlocklistService{db: database}
}
func scanBlocklistItem(scanner interface{ Scan(...interface{}) error }) (*BlocklistItem, error) {
var item BlocklistItem
var sourceTitle, indexer, torrentHash, message sql.NullString
var size, mediaID sql.NullInt64
var autoExpiresAt sql.NullTime
var quality []byte
err := scanner.Scan(&item.ID, &item.ReleaseTitle, &sourceTitle, &quality, &indexer,
&item.Protocol, &torrentHash, &size, &message, &mediaID, &item.BlockReason,
&autoExpiresAt, &item.CreatedAt)
if err != nil {
return nil, err
}
if sourceTitle.Valid {
item.SourceTitle = &sourceTitle.String
}
if indexer.Valid {
item.Indexer = &indexer.String
}
if torrentHash.Valid {
item.TorrentHash = &torrentHash.String
}
if message.Valid {
item.Message = &message.String
}
if size.Valid {
item.Size = &size.Int64
}
if mediaID.Valid {
item.MediaID = &mediaID.Int64
}
if autoExpiresAt.Valid {
item.AutoExpiresAt = &autoExpiresAt.Time
}
if quality != nil {
item.Quality = json.RawMessage(quality)
}
return &item, nil
}
func (s *BlocklistService) List(ctx context.Context, filters BlocklistFilters) ([]BlocklistItem, int, error) {
var total int
if err := s.db.Pool.QueryRow(ctx, "SELECT COUNT(*) FROM blocklist").Scan(&total); err != nil {
return nil, 0, fmt.Errorf("count blocklist: %w", err)
}
rows, err := s.db.Pool.Query(ctx,
fmt.Sprintf("SELECT %s FROM blocklist ORDER BY created_at DESC LIMIT $1 OFFSET $2", blocklistColumns),
filters.PageSize, (filters.Page-1)*filters.PageSize)
if err != nil {
return nil, 0, fmt.Errorf("list blocklist: %w", err)
}
defer rows.Close()
var items []BlocklistItem
for rows.Next() {
item, err := scanBlocklistItem(rows)
if err != nil {
slog.Error("failed to scan blocklist item", "error", err)
continue
}
items = append(items, *item)
}
return items, total, nil
}
func (s *BlocklistService) Add(ctx context.Context, req AddBlocklistRequest) (int64, error) {
protocol := req.Protocol
if protocol == "" {
protocol = "torrent"
}
blockReason := "manual"
if req.BlockReason != nil {
blockReason = *req.BlockReason
}
quality := req.Quality
if quality == nil {
quality = json.RawMessage("{}")
}
var id int64
err := s.db.Pool.QueryRow(ctx,
`INSERT INTO blocklist (release_title, source_title, quality, indexer, protocol,
torrent_hash, size, message, media_id, block_reason, auto_expires_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id`,
req.ReleaseTitle, req.SourceTitle, quality, req.Indexer, protocol,
req.TorrentHash, req.Size, req.Message, req.MediaID, blockReason, req.AutoExpiresAt).Scan(&id)
if err != nil {
return 0, fmt.Errorf("create blocklist entry: %w", err)
}
return id, nil
}
func (s *BlocklistService) Delete(ctx context.Context, id int64) error {
tag, err := s.db.Pool.Exec(ctx, "DELETE FROM blocklist WHERE id = $1", id)
if err != nil {
return fmt.Errorf("delete blocklist item: %w", err)
}
if tag.RowsAffected() == 0 {
return fmt.Errorf("blocklist item not found")
}
return nil
}
func (s *BlocklistService) Clear(ctx context.Context) (int64, error) {
tag, err := s.db.Pool.Exec(ctx, "DELETE FROM blocklist")
if err != nil {
return 0, fmt.Errorf("clear blocklist: %w", err)
}
return tag.RowsAffected(), nil
}
func (s *BlocklistService) ClearExpired(ctx context.Context) (int64, error) {
tag, err := s.db.Pool.Exec(ctx,
"DELETE FROM blocklist WHERE auto_expires_at IS NOT NULL AND auto_expires_at < NOW()")
if err != nil {
return 0, fmt.Errorf("clear expired blocklist: %w", err)
}
return tag.RowsAffected(), nil
}