add workflow to build

This commit is contained in:
roman.nikolsky
2026-07-04 01:56:10 +03:00
commit af098e6ec4
14 changed files with 1701 additions and 0 deletions
+142
View File
@@ -0,0 +1,142 @@
package preview
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/user/ytdlp-navidrome/internal/config"
"github.com/user/ytdlp-navidrome/internal/ytdlp"
)
// State represents the per-user dialog state.
type State int
const (
StateIdle State = iota
StateSearchResults
StatePreviewSent
)
// UserSession holds the current dialog state for a Matrix user.
type UserSession struct {
State State
Query string
Results []ytdlp.SearchResult
VideoID string
PreviewPath string
Timestamp time.Time
}
// Manager handles preview lifecycle and user sessions.
type Manager struct {
mu sync.RWMutex
sessions map[string]*UserSession
cfg *config.Config
}
func NewManager(cfg *config.Config) *Manager {
return &Manager{
sessions: make(map[string]*UserSession),
cfg: cfg,
}
}
// GetSession returns the current session for a user.
func (m *Manager) GetSession(userID string) *UserSession {
m.mu.RLock()
defer m.mu.RUnlock()
return m.sessions[userID]
}
// SetSearchResults stores search results for a user.
func (m *Manager) SetSearchResults(userID, query string, results []ytdlp.SearchResult) {
m.mu.Lock()
defer m.mu.Unlock()
m.sessions[userID] = &UserSession{
State: StateSearchResults,
Query: query,
Results: results,
Timestamp: time.Now(),
}
}
// SetPreviewSent records that a preview was sent for a user.
func (m *Manager) SetPreviewSent(userID, videoID, previewPath string) {
m.mu.Lock()
defer m.mu.Unlock()
m.sessions[userID] = &UserSession{
State: StatePreviewSent,
VideoID: videoID,
PreviewPath: previewPath,
Timestamp: time.Now(),
}
}
// ResetSession clears a user session (e.g., on cancel or completion).
func (m *Manager) ResetSession(userID string) {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.sessions, userID)
}
// IsExpired checks if a session has exceeded the preview TTL.
func (s *UserSession) IsExpired(ttl time.Duration) bool {
return time.Since(s.Timestamp) > ttl
}
// SanitizeFilename replaces forbidden characters with underscores.
func SanitizeFilename(name string) string {
forbidden := []string{"/", "\\", ":", "*", "?", "\"", "<", ">", "|"}
result := name
for _, ch := range forbidden {
result = strings.ReplaceAll(result, ch, "_")
}
return result
}
// BuildFinalFilename constructs the final filename: "Artist - Title.ext"
func BuildFinalFilename(result *ytdlp.SearchResult, ext string) string {
artist := result.Channel
if artist == "" {
artist = "unknown"
}
title := result.Title
if title == "" {
title = result.ID
}
return SanitizeFilename(fmt.Sprintf("%s - %s.%s", artist, title, ext))
}
// CleanupOldPreviews removes expired preview files from tmp dir.
func (m *Manager) CleanupOldPreviews(ctx context.Context) {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
m.mu.Lock()
for userID, session := range m.sessions {
if session.State == StatePreviewSent && session.IsExpired(m.cfg.PreviewTTL) {
if session.PreviewPath != "" {
os.Remove(session.PreviewPath)
}
delete(m.sessions, userID)
}
}
m.mu.Unlock()
}
}
}
// PreviewFilePath returns the expected preview file path for a video ID.
func PreviewFilePath(tmpDir, videoID, ext string) string {
return filepath.Join(tmpDir, fmt.Sprintf("%s.%s", videoID, ext))
}