Skip to content

Commit c54525c

Browse files
authored
Merge pull request #105 from contre95/feat/playlists2
Feat/playlists2
2 parents 888bae9 + 2e13609 commit c54525c

File tree

15 files changed

+1764
-56
lines changed

15 files changed

+1764
-56
lines changed

src/features/hosting/server.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/contre95/soulsolid/src/features/lyrics"
1616
"github.com/contre95/soulsolid/src/features/metadata"
1717
"github.com/contre95/soulsolid/src/features/metrics"
18+
"github.com/contre95/soulsolid/src/features/playlists"
1819
"github.com/contre95/soulsolid/src/features/syncdap"
1920
"github.com/contre95/soulsolid/src/features/ui"
2021
"github.com/contre95/soulsolid/src/music"
@@ -29,7 +30,7 @@ type Server struct {
2930
}
3031

3132
// NewServer creates a new HTTP server.
32-
func NewServer(cfg *config.Manager, importingService *importing.Service, libraryService *library.Service, syncService *syncdap.Service, downloadingService *downloading.Service, jobService *jobs.Service, tagService *metadata.Service, lyricsService *lyrics.Service, metricsService *metrics.Service, analyzeService *analyze.Service) *Server {
33+
func NewServer(cfg *config.Manager, importingService *importing.Service, libraryService *library.Service, playlistsService *playlists.Service, syncService *syncdap.Service, downloadingService *downloading.Service, jobService *jobs.Service, tagService *metadata.Service, lyricsService *lyrics.Service, metricsService *metrics.Service, analyzeService *analyze.Service) *Server {
3334
engine := html.New("./views", ".html")
3435
engine.Debug(cfg.Get().Logger.Level == "debug")
3536
// Add custom template functions
@@ -59,7 +60,7 @@ func NewServer(cfg *config.Manager, importingService *importing.Service, library
5960
}
6061
return fmt.Sprintf("%d min", minutes)
6162
})
62-
engine.AddFunc("totalDuration", func(tracks []music.Track) string {
63+
engine.AddFunc("totalDuration", func(tracks []*music.Track) string {
6364
totalSeconds := 0
6465
for _, track := range tracks {
6566
totalSeconds += track.Metadata.Duration
@@ -124,6 +125,7 @@ func NewServer(cfg *config.Manager, importingService *importing.Service, library
124125

125126
importing.RegisterRoutes(app, importingService)
126127
library.RegisterRoutes(app, libraryService)
128+
playlists.RegisterRoutes(app, playlistsService)
127129
ui.RegisterRoutes(app, uiHandler)
128130
config.RegisterRoutes(app, cfg)
129131
jobs.RegisterRoutes(app, jobService)

src/features/playlists/handlers.go

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
package playlists
2+
3+
import (
4+
"fmt"
5+
"log/slog"
6+
"strings"
7+
8+
"github.com/contre95/soulsolid/src/music"
9+
"github.com/gofiber/fiber/v2"
10+
)
11+
12+
// Handler is the handler for the playlists feature.
13+
type Handler struct {
14+
service *Service
15+
}
16+
17+
// NewHandler creates a new handler for the playlists feature.
18+
func NewHandler(service *Service) *Handler {
19+
return &Handler{service: service}
20+
}
21+
22+
// RenderPlaylistsSection renders the playlists page.
23+
func (h *Handler) RenderPlaylistsSection(c *fiber.Ctx) error {
24+
slog.Debug("RenderPlaylistsSection handler called")
25+
26+
playlists, err := h.service.GetAllPlaylists(c.Context())
27+
if err != nil {
28+
slog.Error("Error loading playlists", "error", err)
29+
playlists = []*music.Playlist{} // Continue with empty list
30+
}
31+
32+
data := fiber.Map{
33+
"Title": "Playlists",
34+
"Playlists": playlists,
35+
}
36+
if c.Get("HX-Request") != "true" {
37+
data["Section"] = "playlists"
38+
return c.Render("main", data)
39+
}
40+
return c.Render("sections/playlists", data)
41+
}
42+
43+
// GetPlaylist renders a single playlist page.
44+
func (h *Handler) GetPlaylist(c *fiber.Ctx) error {
45+
slog.Debug("GetPlaylist handler called", "id", c.Params("id"))
46+
47+
playlist, err := h.service.GetPlaylist(c.Context(), c.Params("id"))
48+
if err != nil {
49+
slog.Error("Error loading playlist", "error", err, "id", c.Params("id"))
50+
return c.Status(fiber.StatusInternalServerError).SendString("Failed to load playlist")
51+
}
52+
if playlist == nil {
53+
return c.Status(fiber.StatusNotFound).SendString("Playlist not found")
54+
}
55+
56+
data := fiber.Map{
57+
"Title": fmt.Sprintf("Playlist: %s", playlist.Name),
58+
"Playlist": playlist,
59+
}
60+
61+
if c.Get("HX-Request") != "true" {
62+
// For direct navigation to specific playlist, render main with Playlist data (no Section set)
63+
return c.Render("main", data)
64+
}
65+
return c.Render("playlists/playlist", data)
66+
}
67+
68+
// CreatePlaylist handles creating a new playlist.
69+
func (h *Handler) CreatePlaylist(c *fiber.Ctx) error {
70+
slog.Debug("CreatePlaylist handler called")
71+
72+
name := c.FormValue("name")
73+
description := c.FormValue("description")
74+
75+
if name == "" {
76+
return c.Status(fiber.StatusBadRequest).SendString("Playlist name is required")
77+
}
78+
79+
_, err := h.service.CreatePlaylist(c.Context(), name, description)
80+
if err != nil {
81+
slog.Error("Error creating playlist", "error", err)
82+
return c.Status(fiber.StatusInternalServerError).SendString("Failed to create playlist")
83+
}
84+
85+
// Trigger playlist refresh and return success toast
86+
c.Set("HX-Trigger", "refreshPlaylists")
87+
return c.Render("toast/toastOk", fiber.Map{"Msg": "Playlist created successfully"})
88+
}
89+
90+
// UpdatePlaylist handles updating a playlist.
91+
func (h *Handler) UpdatePlaylist(c *fiber.Ctx) error {
92+
slog.Debug("UpdatePlaylist handler called", "id", c.Params("id"))
93+
94+
playlistID := c.Params("id")
95+
name := c.FormValue("name")
96+
description := c.FormValue("description")
97+
98+
if name == "" {
99+
return c.Status(fiber.StatusBadRequest).SendString("Playlist name is required")
100+
}
101+
102+
playlist, err := h.service.GetPlaylist(c.Context(), playlistID)
103+
if err != nil {
104+
slog.Error("Error loading playlist for update", "error", err, "id", playlistID)
105+
return c.Status(fiber.StatusInternalServerError).SendString("Failed to load playlist")
106+
}
107+
if playlist == nil {
108+
return c.Status(fiber.StatusNotFound).SendString("Playlist not found")
109+
}
110+
111+
playlist.Name = name
112+
playlist.Description = description
113+
114+
err = h.service.UpdatePlaylist(c.Context(), playlist)
115+
if err != nil {
116+
slog.Error("Error updating playlist", "error", err, "id", playlistID)
117+
return c.Status(fiber.StatusInternalServerError).SendString("Failed to update playlist")
118+
}
119+
120+
// Return success toast
121+
return c.Render("toast/toastOk", fiber.Map{"Msg": "Playlist updated successfully"})
122+
}
123+
124+
// DeletePlaylist handles deleting a playlist.
125+
func (h *Handler) DeletePlaylist(c *fiber.Ctx) error {
126+
slog.Debug("DeletePlaylist handler called", "id", c.Params("id"))
127+
128+
err := h.service.DeletePlaylist(c.Context(), c.Params("id"))
129+
if err != nil {
130+
slog.Error("Error deleting playlist", "error", err, "id", c.Params("id"))
131+
return c.Status(fiber.StatusInternalServerError).SendString("Failed to delete playlist")
132+
}
133+
134+
// Trigger playlist refresh and return success toast
135+
c.Set("HX-Trigger", "refreshPlaylists")
136+
return c.Render("toast/toastOk", fiber.Map{"Msg": "Playlist deleted successfully"})
137+
}
138+
139+
// AddItemToPlaylist handles adding tracks, artists, or albums to a playlist.
140+
func (h *Handler) AddItemToPlaylist(c *fiber.Ctx) error {
141+
playlistID := c.FormValue("playlist_id")
142+
itemType := c.FormValue("item_type")
143+
itemID := c.FormValue("item_id")
144+
145+
slog.Debug("AddItemToPlaylist handler called", "playlistID", playlistID, "itemType", itemType, "itemID", itemID)
146+
147+
if playlistID == "" || itemType == "" || itemID == "" {
148+
slog.Error("AddItemToPlaylist: missing required parameters", "playlistID", playlistID, "itemType", itemType, "itemID", itemID)
149+
return c.Status(fiber.StatusBadRequest).SendString("Playlist ID, item type, and item ID are required")
150+
}
151+
152+
err := h.service.AddItemToPlaylist(c.Context(), playlistID, itemType, itemID)
153+
if err != nil {
154+
slog.Error("Error adding item to playlist", "error", err, "playlistID", playlistID, "itemType", itemType, "itemID", itemID)
155+
return c.Status(fiber.StatusInternalServerError).SendString("Failed to add item to playlist")
156+
}
157+
158+
// Get item name for success message
159+
var itemName string
160+
switch itemType {
161+
case "track":
162+
if track, err := h.service.library.GetTrack(c.Context(), itemID); err == nil && track != nil {
163+
itemName = track.Title
164+
}
165+
case "artist":
166+
if artist, err := h.service.library.GetArtist(c.Context(), itemID); err == nil && artist != nil {
167+
itemName = artist.Name
168+
}
169+
case "album":
170+
if album, err := h.service.library.GetAlbum(c.Context(), itemID); err == nil && album != nil {
171+
itemName = album.Title
172+
}
173+
}
174+
175+
var successMsg string
176+
switch itemType {
177+
case "track":
178+
successMsg = fmt.Sprintf("Track '%s' added to playlist", itemName)
179+
case "artist":
180+
successMsg = fmt.Sprintf("All tracks by '%s' added to playlist", itemName)
181+
case "album":
182+
successMsg = fmt.Sprintf("All tracks from '%s' added to playlist", itemName)
183+
default:
184+
successMsg = "Item added to playlist"
185+
}
186+
187+
slog.Info("Item successfully added to playlist", "playlistID", playlistID, "itemType", itemType, "itemID", itemID)
188+
189+
// Trigger playlist refresh and return success toast
190+
c.Set("HX-Trigger", "playlistUpdated")
191+
return c.Render("toast/toastOk", fiber.Map{"Msg": successMsg})
192+
}
193+
194+
// RemoveTrackFromPlaylist handles removing a track from a playlist.
195+
func (h *Handler) RemoveTrackFromPlaylist(c *fiber.Ctx) error {
196+
slog.Debug("RemoveTrackFromPlaylist handler called")
197+
198+
playlistID := c.Params("playlistId")
199+
trackID := c.Params("trackId")
200+
201+
if playlistID == "" || trackID == "" {
202+
return c.Status(fiber.StatusBadRequest).SendString("Playlist ID and Track ID are required")
203+
}
204+
205+
err := h.service.RemoveTrackFromPlaylist(c.Context(), playlistID, trackID)
206+
if err != nil {
207+
slog.Error("Error removing track from playlist", "error", err, "playlistID", playlistID, "trackID", trackID)
208+
return c.Status(fiber.StatusInternalServerError).SendString("Failed to remove track from playlist")
209+
}
210+
211+
// Trigger playlist refresh and return success toast
212+
c.Set("HX-Trigger", "playlistUpdated")
213+
return c.Render("toast/toastOk", fiber.Map{"Msg": "Track removed from playlist"})
214+
}
215+
216+
// GetPlaylistCreationModal returns the create playlist modal.
217+
func (h *Handler) GetPlaylistCreationModal(c *fiber.Ctx) error {
218+
slog.Debug("GetCreatePlaylistModal handler called")
219+
220+
return c.Render("playlists/create_playlist_modal", nil)
221+
}
222+
223+
// GetPlaylistsForItem returns playlists for adding tracks, artists, or albums.
224+
func (h *Handler) GetPlaylistsForItem(c *fiber.Ctx) error {
225+
itemType := c.Params("type")
226+
itemID := c.Params("id")
227+
228+
slog.Debug("GetPlaylistsForItem handler called", "type", itemType, "id", itemID)
229+
230+
playlists, err := h.service.GetAllPlaylists(c.Context())
231+
if err != nil {
232+
slog.Error("Error loading playlists", "error", err, "type", itemType, "id", itemID)
233+
return c.Status(fiber.StatusInternalServerError).SendString("Failed to load playlists")
234+
}
235+
236+
// Get item name for display
237+
var itemName string
238+
switch itemType {
239+
case "track":
240+
track, err := h.service.library.GetTrack(c.Context(), itemID)
241+
if err != nil || track == nil {
242+
return c.Status(fiber.StatusNotFound).SendString("Track not found")
243+
}
244+
itemName = track.Title
245+
case "artist":
246+
artist, err := h.service.library.GetArtist(c.Context(), itemID)
247+
if err != nil || artist == nil {
248+
return c.Status(fiber.StatusNotFound).SendString("Artist not found")
249+
}
250+
itemName = artist.Name
251+
case "album":
252+
album, err := h.service.library.GetAlbum(c.Context(), itemID)
253+
if err != nil || album == nil {
254+
return c.Status(fiber.StatusNotFound).SendString("Album not found")
255+
}
256+
itemName = album.Title
257+
default:
258+
return c.Status(fiber.StatusBadRequest).SendString("Invalid item type")
259+
}
260+
261+
data := fiber.Map{
262+
"Playlists": playlists,
263+
"ItemType": itemType,
264+
"ItemID": itemID,
265+
"ItemName": itemName,
266+
}
267+
268+
return c.Render("playlists/add_to_playlist_modal", data)
269+
}
270+
271+
// ExportM3U handles exporting a playlist to an M3U file.
272+
func (h *Handler) ExportM3U(c *fiber.Ctx) error {
273+
slog.Debug("ExportM3U handler called", "id", c.Params("id"))
274+
275+
playlistID := c.Params("id")
276+
277+
// Get playlist
278+
playlist, err := h.service.GetPlaylist(c.Context(), playlistID)
279+
if err != nil {
280+
slog.Error("Error loading playlist for export", "error", err, "id", playlistID)
281+
return c.Status(fiber.StatusInternalServerError).SendString("Failed to load playlist")
282+
}
283+
if playlist == nil {
284+
return c.Status(fiber.StatusNotFound).SendString("Playlist not found")
285+
}
286+
287+
// Generate M3U content
288+
var builder strings.Builder
289+
builder.WriteString("#EXTM3U\n")
290+
291+
for _, track := range playlist.Tracks {
292+
// Write extended M3U info
293+
duration := track.Metadata.Duration
294+
artists := make([]string, len(track.Artists))
295+
for i, ar := range track.Artists {
296+
if ar.Artist != nil {
297+
artists[i] = ar.Artist.Name
298+
}
299+
}
300+
artistStr := strings.Join(artists, ", ")
301+
302+
builder.WriteString(fmt.Sprintf("#EXTINF:%d,%s - %s\n", duration, artistStr, track.Title))
303+
304+
// Write file path
305+
builder.WriteString(track.Path + "\n")
306+
}
307+
308+
m3uContent := builder.String()
309+
filename := fmt.Sprintf("%s.m3u", playlist.Name)
310+
311+
// Set headers for inline display in new tab
312+
c.Set("Content-Type", "text/plain")
313+
c.Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", filename))
314+
315+
return c.SendString(m3uContent)
316+
}

src/features/playlists/routes.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package playlists
2+
3+
import (
4+
"github.com/gofiber/fiber/v2"
5+
)
6+
7+
// RegisterRoutes registers the routes for the playlists feature.
8+
func RegisterRoutes(app *fiber.App, service *Service) {
9+
handler := NewHandler(service)
10+
11+
ui := app.Group("/ui")
12+
ui.Get("/playlists", handler.RenderPlaylistsSection)
13+
ui.Get("/playlists/:id", handler.GetPlaylist)
14+
15+
playlists := app.Group("/playlists")
16+
playlists.Get("/create-modal", handler.GetPlaylistCreationModal)
17+
playlists.Post("/", handler.CreatePlaylist)
18+
playlists.Put("/:id", handler.UpdatePlaylist)
19+
playlists.Delete("/:id", handler.DeletePlaylist)
20+
playlists.Post("/items", handler.AddItemToPlaylist)
21+
playlists.Delete("/:playlistId/tracks/:trackId", handler.RemoveTrackFromPlaylist)
22+
playlists.Get("/:type/:id/playlists", handler.GetPlaylistsForItem)
23+
24+
playlists.Get("/:id/export", handler.ExportM3U)
25+
}

0 commit comments

Comments
 (0)