mirror of
https://github.com/navidrome/navidrome.git
synced 2026-11-06 18:28:39 +01:00
Backend - Add podcast data model (PodcastChannel, PodcastEpisode) with migrations - Implement Subsonic API endpoints: getPodcasts, getNewestPodcasts, createPodcastChannel, refreshPodcasts, deletePodcastChannel, deletePodcastEpisode, downloadPodcastEpisode, getPodcastEpisode - Add native REST API endpoints: GET/DELETE /api/podcast, GET /api/podcast/preview (feed info without creating channel) - Inject events.Broker into podcast service for SSE support - Emit PodcastEpisodeProgress SSE events during download (every 512 KB) and on completion/error with status field - Use HTTP Content-Length as fallback when RSS feed omits enclosure size - Write ID3 tags (title, album, genre=Podcast) to downloaded files via ffmpeg so the library scanner reads correct metadata - Set MediaFile fields (Title, Album, AlbumID=channelId, AlbumArtist, Genre) on episode registration - Add duplicate URL check in AddChannel - Add ExistsByURL to PodcastChannelRepository Frontend - Podcast list - Add grid/list view toggle (Redux podcastViewReducer) matching album list - New PodcastGridView component with responsive column count (2-6 cols) - Cover image 100px in table view - Remove Feed URL column; add inline copy-to-clipboard button Frontend - Podcast show (episode list) - Real-time download progress (%) in Status column via SSE, no polling - Spinner only before first SSE event; N% once data arrives - Size column removed; Downloading badge replaced with progress - Completed episodes play on row click; separate play button removed - Play / Shuffle / Play Next / Add to Queue buttons above episode list (only shown when completed episodes exist) - On download completion, reload episodes to obtain streamId for immediate playback without page refresh - Clicking album name in AudioTitle navigates to podcast channel page Frontend - Podcast create - Full-width URL input with Fetch Feed Info button and Enter key support - Preview card (cover image, title, episode count, description) before committing channel creation - Add Channel button appears only after preview; shows already-registered message if channel URL exists Frontend - Playlist - Album link navigates to podcast channel page for podcast tracks (identified by genre=Podcast) - Artist column shows '-' for podcast tracks with empty artist field Closes #5420 Signed-off-by: ji-ho lee <search5@gmail.com>
99 lines
2.2 KiB
Go
99 lines
2.2 KiB
Go
package events
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
)
|
|
|
|
type eventCtxKey string
|
|
|
|
const broadcastToAllKey eventCtxKey = "broadcastToAll"
|
|
|
|
// broadcastToAll is a context key that can be used to broadcast an event to all clients
|
|
func broadcastToAll(ctx context.Context) context.Context {
|
|
return context.WithValue(ctx, broadcastToAllKey, true)
|
|
}
|
|
|
|
type Event interface {
|
|
Name(Event) string
|
|
Data(Event) string
|
|
}
|
|
|
|
type baseEvent struct{}
|
|
|
|
func (e *baseEvent) Name(evt Event) string {
|
|
str := strings.TrimPrefix(reflect.TypeOf(evt).String(), "*events.")
|
|
return str[:0] + string(unicode.ToLower(rune(str[0]))) + str[1:]
|
|
}
|
|
|
|
func (e *baseEvent) Data(evt Event) string {
|
|
data, _ := json.Marshal(evt)
|
|
return string(data)
|
|
}
|
|
|
|
type ScanStatus struct {
|
|
baseEvent
|
|
Scanning bool `json:"scanning"`
|
|
Count int64 `json:"count"`
|
|
FolderCount int64 `json:"folderCount"`
|
|
Error string `json:"error"`
|
|
ScanType string `json:"scanType"`
|
|
ElapsedTime time.Duration `json:"elapsedTime"`
|
|
}
|
|
|
|
type KeepAlive struct {
|
|
baseEvent
|
|
TS int64 `json:"ts"`
|
|
}
|
|
|
|
type ServerStart struct {
|
|
baseEvent
|
|
StartTime time.Time `json:"startTime"`
|
|
Version string `json:"version"`
|
|
}
|
|
|
|
const Any = "*"
|
|
|
|
type RefreshResource struct {
|
|
baseEvent
|
|
resources map[string][]string
|
|
}
|
|
|
|
type NowPlayingCount struct {
|
|
baseEvent
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
type PodcastEpisodeProgress struct {
|
|
baseEvent
|
|
EpisodeID string `json:"episodeId"`
|
|
ChannelID string `json:"channelId"`
|
|
DownloadedBytes int64 `json:"downloadedBytes"`
|
|
Size int64 `json:"size"`
|
|
Duration int `json:"duration,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
}
|
|
|
|
func (rr *RefreshResource) With(resource string, ids ...string) *RefreshResource {
|
|
if rr.resources == nil {
|
|
rr.resources = make(map[string][]string)
|
|
}
|
|
if len(ids) == 0 {
|
|
rr.resources[resource] = append(rr.resources[resource], Any)
|
|
}
|
|
rr.resources[resource] = append(rr.resources[resource], ids...)
|
|
return rr
|
|
}
|
|
|
|
func (rr *RefreshResource) Data(evt Event) string {
|
|
if rr.resources == nil {
|
|
return `{"*":"*"}`
|
|
}
|
|
r := evt.(*RefreshResource)
|
|
data, _ := json.Marshal(r.resources)
|
|
return string(data)
|
|
}
|