mirror of
https://github.com/Dvorinka/SEEN.git
synced 2026-06-05 13:03:01 +00:00
small fix, don't worry about it
This commit is contained in:
@@ -0,0 +1,339 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Status string
|
||||
|
||||
const (
|
||||
StatusQueued Status = "queued"
|
||||
StatusPreparing Status = "preparing"
|
||||
StatusDownloading Status = "downloading"
|
||||
StatusStalled Status = "stalled"
|
||||
StatusRetrying Status = "retrying"
|
||||
StatusCompleted Status = "completed"
|
||||
StatusFailed Status = "failed"
|
||||
StatusCancelled Status = "cancelled"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidInput = errors.New("invalid input")
|
||||
ErrNotFound = errors.New("download job not found")
|
||||
)
|
||||
|
||||
type Job struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"userId,omitempty"`
|
||||
SourceType string `json:"sourceType"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Title string `json:"title"`
|
||||
Status string `json:"status"`
|
||||
QueuePosition int `json:"queuePosition,omitempty"`
|
||||
ProgressPercent int `json:"progressPercent"`
|
||||
BytesTotal int64 `json:"bytesTotal"`
|
||||
BytesDownloaded int64 `json:"bytesDownloaded"`
|
||||
DownloadSpeedMbps float64 `json:"downloadSpeedMbps"`
|
||||
EtaSeconds int `json:"etaSeconds,omitempty"`
|
||||
ErrorMessage string `json:"errorMessage,omitempty"`
|
||||
RetryCount int `json:"retryCount"`
|
||||
CreatedAt time.Time `json:"createdAt,omitempty"`
|
||||
UpdatedAt time.Time `json:"updatedAt,omitempty"`
|
||||
StartedAt time.Time `json:"startedAt,omitempty"`
|
||||
CompletedAt time.Time `json:"completedAt,omitempty"`
|
||||
CancelledAt time.Time `json:"cancelledAt,omitempty"`
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
ID int64 `json:"id"`
|
||||
JobID string `json:"jobId"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
ProgressPercent int `json:"progressPercent"`
|
||||
Payload string `json:"payload"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type CreateInput struct {
|
||||
SourceType string
|
||||
Source string
|
||||
Title string
|
||||
}
|
||||
|
||||
type ListParams struct {
|
||||
Status string
|
||||
Limit int
|
||||
Offset int
|
||||
}
|
||||
|
||||
type EventParams struct {
|
||||
Limit int
|
||||
After string
|
||||
}
|
||||
|
||||
type UpdateInput struct {
|
||||
Status string
|
||||
ProgressPercent int
|
||||
BytesTotal int64
|
||||
BytesDownloaded int64
|
||||
DownloadSpeedMbps float64
|
||||
EtaSeconds int
|
||||
ErrorMessage string
|
||||
RetryCount int
|
||||
}
|
||||
|
||||
type Repository interface {
|
||||
CreateJob(ctx context.Context, userID uuid.UUID, input CreateInput) (Job, error)
|
||||
ListJobs(ctx context.Context, userID uuid.UUID, params ListParams) ([]Job, error)
|
||||
GetJobByID(ctx context.Context, userID uuid.UUID, jobID string) (Job, error)
|
||||
CancelJob(ctx context.Context, userID uuid.UUID, jobID string) (Job, error)
|
||||
ListEvents(ctx context.Context, userID uuid.UUID, jobID string, params EventParams) ([]Event, error)
|
||||
AppendEvent(ctx context.Context, jobID string, event Event) error
|
||||
UpdateJob(ctx context.Context, userID uuid.UUID, jobID string, input UpdateInput) (Job, error)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
repo Repository
|
||||
}
|
||||
|
||||
func NewService(repo Repository) *Service {
|
||||
return &Service{repo: repo}
|
||||
}
|
||||
|
||||
func (s *Service) Create(ctx context.Context, userID uuid.UUID, input CreateInput) (Job, error) {
|
||||
if userID == uuid.Nil {
|
||||
return Job{}, ErrInvalidInput
|
||||
}
|
||||
|
||||
sourceType := strings.TrimSpace(strings.ToLower(input.SourceType))
|
||||
source := strings.TrimSpace(input.Source)
|
||||
title := strings.TrimSpace(input.Title)
|
||||
|
||||
if sourceType == "" || source == "" {
|
||||
return Job{}, ErrInvalidInput
|
||||
}
|
||||
|
||||
if !isAllowedSourceType(sourceType) {
|
||||
return Job{}, ErrInvalidInput
|
||||
}
|
||||
|
||||
return s.repo.CreateJob(ctx, userID, CreateInput{
|
||||
SourceType: sourceType,
|
||||
Source: source,
|
||||
Title: title,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) List(ctx context.Context, userID uuid.UUID, params ListParams) ([]Job, error) {
|
||||
if userID == uuid.Nil {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
|
||||
limit := params.Limit
|
||||
if limit < 1 || limit > 100 {
|
||||
limit = 20
|
||||
}
|
||||
|
||||
offset := params.Offset
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
status := strings.TrimSpace(strings.ToLower(params.Status))
|
||||
if status != "" && !isAllowedStatus(status) {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
|
||||
return s.repo.ListJobs(ctx, userID, ListParams{
|
||||
Status: status,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) Cancel(ctx context.Context, userID uuid.UUID, jobID string) (Job, error) {
|
||||
if userID == uuid.Nil || strings.TrimSpace(jobID) == "" {
|
||||
return Job{}, ErrInvalidInput
|
||||
}
|
||||
|
||||
job, err := s.repo.GetJobByID(ctx, userID, jobID)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
return Job{}, ErrNotFound
|
||||
}
|
||||
return Job{}, err
|
||||
}
|
||||
|
||||
if isTerminalStatus(job.Status) {
|
||||
return job, nil
|
||||
}
|
||||
|
||||
updated, err := s.repo.CancelJob(ctx, userID, jobID)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
return Job{}, ErrNotFound
|
||||
}
|
||||
return Job{}, err
|
||||
}
|
||||
|
||||
_ = s.repo.AppendEvent(ctx, updated.ID, Event{
|
||||
Status: StatusCancelled.String(),
|
||||
Message: "job cancelled by user",
|
||||
ProgressPercent: updated.ProgressPercent,
|
||||
Payload: "{}",
|
||||
})
|
||||
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (s *Service) Events(ctx context.Context, userID uuid.UUID, jobID string, params EventParams) ([]Event, error) {
|
||||
if userID == uuid.Nil || strings.TrimSpace(jobID) == "" {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
|
||||
limit := params.Limit
|
||||
if limit < 1 || limit > 200 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
_, err := s.repo.GetJobByID(ctx, userID, jobID)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.repo.ListEvents(ctx, userID, jobID, EventParams{
|
||||
Limit: limit,
|
||||
After: strings.TrimSpace(params.After),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) Transition(ctx context.Context, userID uuid.UUID, jobID string, input UpdateInput) (Job, error) {
|
||||
if userID == uuid.Nil || strings.TrimSpace(jobID) == "" {
|
||||
return Job{}, ErrInvalidInput
|
||||
}
|
||||
|
||||
nextStatus := strings.TrimSpace(strings.ToLower(input.Status))
|
||||
if !isAllowedStatus(nextStatus) {
|
||||
return Job{}, ErrInvalidInput
|
||||
}
|
||||
|
||||
current, err := s.repo.GetJobByID(ctx, userID, jobID)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
return Job{}, ErrNotFound
|
||||
}
|
||||
return Job{}, err
|
||||
}
|
||||
|
||||
if !canTransition(current.Status, nextStatus) {
|
||||
return Job{}, ErrInvalidInput
|
||||
}
|
||||
|
||||
if input.ProgressPercent < current.ProgressPercent && !isTerminalStatus(nextStatus) {
|
||||
return Job{}, ErrInvalidInput
|
||||
}
|
||||
|
||||
if nextStatus == StatusCompleted.String() {
|
||||
input.ProgressPercent = 100
|
||||
}
|
||||
|
||||
updated, err := s.repo.UpdateJob(ctx, userID, jobID, input)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
return Job{}, ErrNotFound
|
||||
}
|
||||
return Job{}, err
|
||||
}
|
||||
|
||||
_ = s.repo.AppendEvent(ctx, updated.ID, Event{
|
||||
Status: updated.Status,
|
||||
Message: eventMessageForStatus(updated.Status),
|
||||
ProgressPercent: updated.ProgressPercent,
|
||||
Payload: "{}",
|
||||
})
|
||||
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func isAllowedSourceType(value string) bool {
|
||||
return slices.Contains([]string{"magnet", "torrent", "direct", "http"}, value)
|
||||
}
|
||||
|
||||
func isAllowedStatus(value string) bool {
|
||||
return slices.Contains([]string{
|
||||
StatusQueued.String(),
|
||||
StatusPreparing.String(),
|
||||
StatusDownloading.String(),
|
||||
StatusStalled.String(),
|
||||
StatusRetrying.String(),
|
||||
StatusCompleted.String(),
|
||||
StatusFailed.String(),
|
||||
StatusCancelled.String(),
|
||||
}, value)
|
||||
}
|
||||
|
||||
func isTerminalStatus(value string) bool {
|
||||
return slices.Contains([]string{
|
||||
StatusCompleted.String(),
|
||||
StatusFailed.String(),
|
||||
StatusCancelled.String(),
|
||||
}, strings.TrimSpace(strings.ToLower(value)))
|
||||
}
|
||||
|
||||
func canTransition(from string, to string) bool {
|
||||
current := strings.TrimSpace(strings.ToLower(from))
|
||||
next := strings.TrimSpace(strings.ToLower(to))
|
||||
|
||||
if current == next {
|
||||
return true
|
||||
}
|
||||
|
||||
switch current {
|
||||
case StatusQueued.String():
|
||||
return next == StatusPreparing.String() || next == StatusCancelled.String()
|
||||
case StatusPreparing.String():
|
||||
return next == StatusDownloading.String() || next == StatusCancelled.String()
|
||||
case StatusDownloading.String():
|
||||
return next == StatusStalled.String() || next == StatusCompleted.String() || next == StatusFailed.String() || next == StatusCancelled.String()
|
||||
case StatusStalled.String():
|
||||
return next == StatusRetrying.String() || next == StatusCancelled.String()
|
||||
case StatusRetrying.String():
|
||||
return next == StatusPreparing.String() || next == StatusCancelled.String()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func eventMessageForStatus(status string) string {
|
||||
switch strings.TrimSpace(strings.ToLower(status)) {
|
||||
case StatusPreparing.String():
|
||||
return "job preparing"
|
||||
case StatusDownloading.String():
|
||||
return "download started"
|
||||
case StatusStalled.String():
|
||||
return "download stalled"
|
||||
case StatusRetrying.String():
|
||||
return "retrying stalled download"
|
||||
case StatusCompleted.String():
|
||||
return "download completed"
|
||||
case StatusFailed.String():
|
||||
return "download failed"
|
||||
case StatusCancelled.String():
|
||||
return "download cancelled"
|
||||
default:
|
||||
return "download updated"
|
||||
}
|
||||
}
|
||||
|
||||
func (s Status) String() string {
|
||||
return string(s)
|
||||
}
|
||||
Reference in New Issue
Block a user