handlers.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. package web
  2. import (
  3. "net/http"
  4. "github.com/pocketbase/pocketbase/core"
  5. "code.osinet.fr/fgm/jamtrack/internal/query"
  6. "code.osinet.fr/fgm/jamtrack/views"
  7. )
  8. // Dashboard renders the practice-list ranking page.
  9. func Dashboard(e *core.RequestEvent) error {
  10. locationID := e.Request.URL.Query().Get("location")
  11. ranks, err := query.FetchRanking(e.App, locationID)
  12. if err != nil {
  13. return err
  14. }
  15. return views.Dashboard(ranks, locationID).Render(e.Request.Context(), e.Response)
  16. }
  17. // JamList renders the list of all jams.
  18. func JamList(e *core.RequestEvent) error {
  19. jams, err := e.App.FindRecordsByFilter("jams", "", "-date", -1, 0)
  20. if err != nil {
  21. return err
  22. }
  23. locs, err := e.App.FindRecordsByFilter("locations", "", "name", -1, 0)
  24. if err != nil {
  25. return err
  26. }
  27. locationNames := make(map[string]string, len(locs))
  28. for _, l := range locs {
  29. locationNames[l.Id] = l.GetString("name")
  30. }
  31. type row struct {
  32. JamID string `db:"jam"`
  33. Total int `db:"total"`
  34. Played int `db:"played"`
  35. }
  36. var counts []row
  37. if err := e.App.DB().
  38. NewQuery("SELECT jam, COUNT(*) AS total, SUM(played) AS played FROM setlist GROUP BY jam").
  39. All(&counts); err != nil {
  40. return err
  41. }
  42. songCounts := make(map[string]views.SongCount, len(counts))
  43. for _, r := range counts {
  44. songCounts[r.JamID] = views.SongCount{Played: r.Played, Total: r.Total}
  45. }
  46. return views.JamList(jams, locationNames, songCounts).Render(e.Request.Context(), e.Response)
  47. }
  48. // JamNew renders the new-jam form.
  49. func JamNew(e *core.RequestEvent) error {
  50. locations, err := e.App.FindRecordsByFilter("locations", "", "name", -1, 0)
  51. if err != nil {
  52. return err
  53. }
  54. return views.JamNew(locations).Render(e.Request.Context(), e.Response)
  55. }
  56. // JamCreate handles the new-jam form submission.
  57. func JamCreate(e *core.RequestEvent) error {
  58. collection, err := e.App.FindCollectionByNameOrId("jams")
  59. if err != nil {
  60. return err
  61. }
  62. record := core.NewRecord(collection)
  63. record.Set("date", e.Request.FormValue("date"))
  64. record.Set("location", e.Request.FormValue("location"))
  65. record.Set("notes", e.Request.FormValue("notes"))
  66. if err := e.App.Save(record); err != nil {
  67. return err
  68. }
  69. return e.Redirect(http.StatusFound, "/jams/"+record.Id)
  70. }
  71. // JamDetail renders the detail view for a single jam and its setlist.
  72. func JamDetail(e *core.RequestEvent) error {
  73. id := e.Request.PathValue("id")
  74. jam, err := e.App.FindRecordById("jams", id)
  75. if err != nil {
  76. return err
  77. }
  78. location, err := e.App.FindRecordById("locations", jam.GetString("location"))
  79. if err != nil {
  80. return err
  81. }
  82. setlistRecords, err := e.App.FindRecordsByFilter(
  83. "setlist", "jam={:jam}", "id", -1, 0,
  84. map[string]any{"jam": id},
  85. )
  86. if err != nil {
  87. return err
  88. }
  89. rows := make([]views.SetlistRow, 0, len(setlistRecords))
  90. for _, sl := range setlistRecords {
  91. song, err := e.App.FindRecordById("songs", sl.GetString("song"))
  92. if err != nil {
  93. return err
  94. }
  95. rows = append(rows, views.SetlistRow{
  96. ID: sl.Id,
  97. Artist: song.GetString("artist"),
  98. Title: song.GetString("title"),
  99. Played: sl.GetBool("played"),
  100. })
  101. }
  102. return views.JamDetail(jam, location, rows).Render(e.Request.Context(), e.Response)
  103. }
  104. // LocationList renders the locations management page.
  105. func LocationList(e *core.RequestEvent) error {
  106. locations, err := e.App.FindRecordsByFilter("locations", "", "name", -1, 0)
  107. if err != nil {
  108. return err
  109. }
  110. return views.LocationList(locations).Render(e.Request.Context(), e.Response)
  111. }
  112. // LocationCreate handles adding a new location.
  113. func LocationCreate(e *core.RequestEvent) error {
  114. collection, err := e.App.FindCollectionByNameOrId("locations")
  115. if err != nil {
  116. return err
  117. }
  118. record := core.NewRecord(collection)
  119. record.Set("name", e.Request.FormValue("name"))
  120. record.Set("notes", e.Request.FormValue("notes"))
  121. if err := e.App.Save(record); err != nil {
  122. return err
  123. }
  124. return e.Redirect(http.StatusFound, "/locations")
  125. }
  126. // LocationDetail renders per-location jam history and practice ranking.
  127. func LocationDetail(e *core.RequestEvent) error {
  128. id := e.Request.PathValue("id")
  129. loc, err := e.App.FindRecordById("locations", id)
  130. if err != nil {
  131. return err
  132. }
  133. jams, err := e.App.FindRecordsByFilter(
  134. "jams", "location={:loc}", "-date", -1, 0,
  135. map[string]any{"loc": id},
  136. )
  137. if err != nil {
  138. return err
  139. }
  140. ranks, err := query.FetchRanking(e.App, id)
  141. if err != nil {
  142. return err
  143. }
  144. return views.LocationDetail(loc, jams, ranks).Render(e.Request.Context(), e.Response)
  145. }
  146. // SongSearch is the HTMX typeahead endpoint. It dispatches on the "field"
  147. // query param: "title" returns title suggestions (filtered by artist when
  148. // provided); anything else returns distinct artist suggestions.
  149. func SongSearch(e *core.RequestEvent) error {
  150. if e.Request.URL.Query().Get("field") == "title" {
  151. return songTitleSearch(e)
  152. }
  153. return songArtistSearch(e)
  154. }
  155. func songArtistSearch(e *core.RequestEvent) error {
  156. q := e.Request.URL.Query()
  157. artistQ := "%" + q.Get("artist") + "%"
  158. title := q.Get("title")
  159. filter := "artist~{:artistQ}"
  160. params := map[string]any{"artistQ": artistQ}
  161. if title != "" {
  162. filter += " && title={:title}"
  163. params["title"] = title
  164. }
  165. records, err := e.App.FindRecordsByFilter("songs", filter, "artist", 20, 0, params)
  166. if err != nil {
  167. return err
  168. }
  169. seen := map[string]bool{}
  170. artists := make([]string, 0, len(records))
  171. for _, r := range records {
  172. a := r.GetString("artist")
  173. if !seen[a] {
  174. seen[a] = true
  175. artists = append(artists, a)
  176. }
  177. }
  178. return views.SongOptions(artists).Render(e.Request.Context(), e.Response)
  179. }
  180. func songTitleSearch(e *core.RequestEvent) error {
  181. q := e.Request.URL.Query()
  182. artist := q.Get("artist")
  183. titleQ := "%" + q.Get("title") + "%"
  184. filter := "title~{:titleQ}"
  185. params := map[string]any{"titleQ": titleQ}
  186. if artist != "" {
  187. filter = "artist={:artist} && " + filter
  188. params["artist"] = artist
  189. }
  190. records, err := e.App.FindRecordsByFilter("songs", filter, "title", 20, 0, params)
  191. if err != nil {
  192. return err
  193. }
  194. seen := map[string]bool{}
  195. titles := make([]string, 0, len(records))
  196. for _, r := range records {
  197. t := r.GetString("title")
  198. if !seen[t] {
  199. seen[t] = true
  200. titles = append(titles, t)
  201. }
  202. }
  203. return views.SongOptions(titles).Render(e.Request.Context(), e.Response)
  204. }
  205. // findOrCreateSong returns an existing song matching artist+title exactly, or
  206. // creates and saves a new one.
  207. func findOrCreateSong(app core.App, artist, title string) (*core.Record, error) {
  208. existing, err := app.FindRecordsByFilter(
  209. "songs", "artist={:artist} && title={:title}", "", 1, 0,
  210. map[string]any{"artist": artist, "title": title},
  211. )
  212. if err == nil && len(existing) > 0 {
  213. return existing[0], nil
  214. }
  215. col, err := app.FindCollectionByNameOrId("songs")
  216. if err != nil {
  217. return nil, err
  218. }
  219. song := core.NewRecord(col)
  220. song.Set("artist", artist)
  221. song.Set("title", title)
  222. if err := app.Save(song); err != nil {
  223. return nil, err
  224. }
  225. return song, nil
  226. }
  227. // SetlistAdd adds a song to a jam's setlist (creating the song if it doesn't
  228. // exist) and returns the new setlist row fragment.
  229. func SetlistAdd(e *core.RequestEvent) error {
  230. jamID := e.Request.PathValue("id")
  231. artist := e.Request.FormValue("artist")
  232. title := e.Request.FormValue("title")
  233. song, err := findOrCreateSong(e.App, artist, title)
  234. if err != nil {
  235. return err
  236. }
  237. played := e.Request.FormValue("played") == "on"
  238. // Create the setlist entry.
  239. slCol, err := e.App.FindCollectionByNameOrId("setlist")
  240. if err != nil {
  241. return err
  242. }
  243. sl := core.NewRecord(slCol)
  244. sl.Set("jam", jamID)
  245. sl.Set("song", song.Id)
  246. sl.Set("played", played)
  247. if err := e.App.Save(sl); err != nil {
  248. return err
  249. }
  250. row := views.SetlistRow{
  251. ID: sl.Id,
  252. Artist: artist,
  253. Title: title,
  254. Played: played,
  255. }
  256. return views.SetlistRowFrag(jamID, row).Render(e.Request.Context(), e.Response)
  257. }
  258. // SetlistView returns the display row fragment for a setlist entry (used by
  259. // the Cancel button in the inline edit form).
  260. func SetlistView(e *core.RequestEvent) error {
  261. id := e.Request.PathValue("id")
  262. sl, err := e.App.FindRecordById("setlist", id)
  263. if err != nil {
  264. return err
  265. }
  266. song, err := e.App.FindRecordById("songs", sl.GetString("song"))
  267. if err != nil {
  268. return err
  269. }
  270. row := views.SetlistRow{
  271. ID: sl.Id,
  272. Artist: song.GetString("artist"),
  273. Title: song.GetString("title"),
  274. Played: sl.GetBool("played"),
  275. }
  276. return views.SetlistRowFrag(sl.GetString("jam"), row).Render(e.Request.Context(), e.Response)
  277. }
  278. // SetlistEditForm returns the inline edit form fragment for a setlist entry.
  279. func SetlistEditForm(e *core.RequestEvent) error {
  280. id := e.Request.PathValue("id")
  281. sl, err := e.App.FindRecordById("setlist", id)
  282. if err != nil {
  283. return err
  284. }
  285. song, err := e.App.FindRecordById("songs", sl.GetString("song"))
  286. if err != nil {
  287. return err
  288. }
  289. row := views.SetlistRow{
  290. ID: sl.Id,
  291. Artist: song.GetString("artist"),
  292. Title: song.GetString("title"),
  293. Played: sl.GetBool("played"),
  294. }
  295. return views.SetlistEditFrag(row).Render(e.Request.Context(), e.Response)
  296. }
  297. // SetlistUpdate updates the artist, title, and played status of a setlist
  298. // entry and returns the updated row fragment.
  299. func SetlistUpdate(e *core.RequestEvent) error {
  300. id := e.Request.PathValue("id")
  301. artist := e.Request.FormValue("artist")
  302. title := e.Request.FormValue("title")
  303. played := e.Request.FormValue("played") == "on"
  304. sl, err := e.App.FindRecordById("setlist", id)
  305. if err != nil {
  306. return err
  307. }
  308. song, err := findOrCreateSong(e.App, artist, title)
  309. if err != nil {
  310. return err
  311. }
  312. sl.Set("song", song.Id)
  313. sl.Set("played", played)
  314. if err := e.App.Save(sl); err != nil {
  315. return err
  316. }
  317. row := views.SetlistRow{
  318. ID: sl.Id,
  319. Artist: artist,
  320. Title: title,
  321. Played: played,
  322. }
  323. return views.SetlistRowFrag(sl.GetString("jam"), row).Render(e.Request.Context(), e.Response)
  324. }
  325. // SetlistToggle toggles the played bool on a setlist entry and returns the
  326. // updated row fragment.
  327. func SetlistToggle(e *core.RequestEvent) error {
  328. id := e.Request.PathValue("id")
  329. sl, err := e.App.FindRecordById("setlist", id)
  330. if err != nil {
  331. return err
  332. }
  333. sl.Set("played", !sl.GetBool("played"))
  334. if err := e.App.Save(sl); err != nil {
  335. return err
  336. }
  337. song, err := e.App.FindRecordById("songs", sl.GetString("song"))
  338. if err != nil {
  339. return err
  340. }
  341. row := views.SetlistRow{
  342. ID: sl.Id,
  343. Artist: song.GetString("artist"),
  344. Title: song.GetString("title"),
  345. Played: sl.GetBool("played"),
  346. }
  347. return views.SetlistRowFrag(sl.GetString("jam"), row).Render(e.Request.Context(), e.Response)
  348. }
  349. // SetlistDelete removes a song from a jam's setlist.
  350. func SetlistDelete(e *core.RequestEvent) error {
  351. id := e.Request.PathValue("id")
  352. sl, err := e.App.FindRecordById("setlist", id)
  353. if err != nil {
  354. return err
  355. }
  356. return e.App.Delete(sl)
  357. }
PANIC: session(release): write data/sessions/d/2/d214c365717b01e1: no space left on device

PANIC

session(release): write data/sessions/d/2/d214c365717b01e1: no space left on device
/my/cache/.heroku/go/go-path/pkg/mod/github.com/go-macaron/session@v1.0.3/session.go:204 (0xb13e07)
/my/cache/.heroku/go/go-path/pkg/mod/gopkg.in/macaron.v1@v1.5.1/context.go:80 (0x967b75)
/my/cache/.heroku/go/go-path/pkg/mod/github.com/go-macaron/inject@v0.0.0-20200308113650-138e5925c53b/inject.go:157 (0x9512ee)
/my/cache/.heroku/go/go-path/pkg/mod/github.com/go-macaron/inject@v0.0.0-20200308113650-138e5925c53b/inject.go:135 (0x951205)
/my/cache/.heroku/go/go-path/pkg/mod/gopkg.in/macaron.v1@v1.5.1/context.go:124 (0x967cc4)
/my/cache/.heroku/go/go-path/pkg/mod/gopkg.in/macaron.v1@v1.5.1/context.go:114 (0x967bf6)
/my/cache/.heroku/go/go-path/pkg/mod/gopkg.in/macaron.v1@v1.5.1/recovery.go:161 (0x15baec4)
/my/cache/.heroku/go/go-path/pkg/mod/gopkg.in/macaron.v1@v1.5.1/logger.go:40 (0x96b257)
/my/cache/.heroku/go/go-path/pkg/mod/github.com/go-macaron/inject@v0.0.0-20200308113650-138e5925c53b/inject.go:157 (0x9512ee)
/my/cache/.heroku/go/go-path/pkg/mod/github.com/go-macaron/inject@v0.0.0-20200308113650-138e5925c53b/inject.go:135 (0x951205)
/my/cache/.heroku/go/go-path/pkg/mod/gopkg.in/macaron.v1@v1.5.1/context.go:124 (0x967cc4)
/my/cache/.heroku/go/go-path/pkg/mod/gopkg.in/macaron.v1@v1.5.1/router.go:187 (0x972959)
/my/cache/.heroku/go/go-path/pkg/mod/gopkg.in/macaron.v1@v1.5.1/router.go:304 (0x973a01)
/my/cache/.heroku/go/go-path/pkg/mod/gopkg.in/macaron.v1@v1.5.1/macaron.go:218 (0x96c572)
/my/cache/.heroku/go/go1.26.3/go/src/net/http/server.go:3311 (0x85a5cd)
/my/cache/.heroku/go/go1.26.3/go/src/net/http/server.go:2073 (0x837f6f)
/my/cache/.heroku/go/go1.26.3/go/src/runtime/asm_amd64.s:1771 (0x493380)