From ea5a5f1f8e360d1dcc22e6ec18992a6ca1bcdf64 Mon Sep 17 00:00:00 2001 From: Baraa Al-Masri Date: Thu, 23 May 2024 03:43:20 +0300 Subject: [PATCH 1/4] feat(history): add model --- cmd/migrator/migrator.go | 1 + db/allowed_models.go | 3 ++- models/history.go | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 models/history.go diff --git a/cmd/migrator/migrator.go b/cmd/migrator/migrator.go index 0013da62..cd1ac10f 100644 --- a/cmd/migrator/migrator.go +++ b/cmd/migrator/migrator.go @@ -19,6 +19,7 @@ func Migrate() error { new(models.Playlist), new(models.PlaylistSong), new(models.PlaylistOwner), + new(models.History), ) if err != nil { return err diff --git a/db/allowed_models.go b/db/allowed_models.go index 7e714e64..6f47782f 100644 --- a/db/allowed_models.go +++ b/db/allowed_models.go @@ -4,6 +4,7 @@ import "dankmuzikk/models" type AllowedModels interface { models.Account | models.Profile | models.EmailVerificationCode | - models.Song | models.Playlist | models.PlaylistSong | models.PlaylistOwner + models.Song | models.Playlist | models.PlaylistSong | models.PlaylistOwner | + models.History GetId() uint } diff --git a/models/history.go b/models/history.go new file mode 100644 index 00000000..2d6645c8 --- /dev/null +++ b/models/history.go @@ -0,0 +1,15 @@ +package models + +import "time" + +type History struct { + SongId uint `gorm:"primaryKey"` + ProfileId uint `gorm:"primaryKey"` + + CreatedAt time.Time + UpdatedAt time.Time +} + +func (h History) GetId() uint { + return h.ProfileId | h.SongId +} From ae673035d90b73b7b2cd5820ee2b4256f17eddb6 Mon Sep 17 00:00:00 2001 From: Baraa Al-Masri Date: Thu, 23 May 2024 04:01:39 +0300 Subject: [PATCH 2/4] chore(history): add song to history --- cmd/server/server.go | 7 +++++-- handlers/apis/songs.go | 29 +++++++++++++++++++++++++---- handlers/handler.go | 14 ++++++++++++++ models/history.go | 9 ++++----- services/history/history.go | 36 ++++++++++++++++++++++++++++++++++++ 5 files changed, 84 insertions(+), 11 deletions(-) create mode 100644 services/history/history.go diff --git a/cmd/server/server.go b/cmd/server/server.go index f980a560..479fbbd1 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -8,6 +8,7 @@ import ( "dankmuzikk/handlers/pages" "dankmuzikk/log" "dankmuzikk/models" + "dankmuzikk/services/history" "dankmuzikk/services/jwt" "dankmuzikk/services/login" "dankmuzikk/services/playlists" @@ -40,10 +41,12 @@ func StartServer(staticFS embed.FS) error { playlistRepo := db.NewBaseDB[models.Playlist](dbConn) playlistOwnersRepo := db.NewBaseDB[models.PlaylistOwner](dbConn) playlistSongsRepo := db.NewBaseDB[models.PlaylistSong](dbConn) + historyRepo := db.NewBaseDB[models.History](dbConn) downloadService := download.New(songRepo) playlistsService := playlists.New(playlistRepo, playlistOwnersRepo, playlistSongsRepo) songsService := songs.New(playlistSongsRepo, playlistOwnersRepo, songRepo, playlistRepo, downloadService) + historyService := history.New(historyRepo, songRepo) jwtUtil := jwt.NewJWTImpl() @@ -82,7 +85,7 @@ func StartServer(staticFS embed.FS) error { emailLoginApi := apis.NewEmailLoginApi(login.NewEmailLoginService(accountRepo, profileRepo, otpRepo, jwtUtil)) googleLoginApi := apis.NewGoogleLoginApi(login.NewGoogleLoginService(accountRepo, profileRepo, otpRepo, jwtUtil)) - songDownloadApi := apis.NewDownloadHandler(downloadService, songsService) + songDownloadApi := apis.NewDownloadHandler(downloadService, songsService, historyService) playlistsApi := apis.NewPlaylistApi(playlistsService, songsService) apisHandler := http.NewServeMux() @@ -94,7 +97,7 @@ func StartServer(staticFS embed.FS) error { apisHandler.HandleFunc("/login/google/callback", googleLoginApi.HandleGoogleOAuthLoginCallback) apisHandler.HandleFunc("GET /logout", apis.HandleLogout) apisHandler.HandleFunc("GET /search-suggestion", apis.HandleSearchSuggestions) - apisHandler.HandleFunc("GET /song", songDownloadApi.HandlePlaySong) + apisHandler.HandleFunc("GET /song", gHandler.OptionalAuthApi(songDownloadApi.HandlePlaySong)) apisHandler.HandleFunc("POST /playlist", gHandler.AuthApi(playlistsApi.HandleCreatePlaylist)) apisHandler.HandleFunc("PUT /toggle-song-in-playlist", gHandler.AuthApi(playlistsApi.HandleToggleSongInPlaylist)) apisHandler.HandleFunc("PUT /increment-song-plays", gHandler.AuthApi(songDownloadApi.HandleIncrementSongPlaysInPlaylist)) diff --git a/handlers/apis/songs.go b/handlers/apis/songs.go index 997421b7..e3fdac41 100644 --- a/handlers/apis/songs.go +++ b/handlers/apis/songs.go @@ -3,18 +3,28 @@ package apis import ( "dankmuzikk/handlers" "dankmuzikk/log" + "dankmuzikk/services/history" "dankmuzikk/services/playlists/songs" "dankmuzikk/services/youtube/download" "net/http" ) type songDownloadHandler struct { - service *download.Service - songsService *songs.Service + service *download.Service + songsService *songs.Service + historyService *history.Service } -func NewDownloadHandler(service *download.Service, songsService *songs.Service) *songDownloadHandler { - return &songDownloadHandler{service, songsService} +func NewDownloadHandler( + service *download.Service, + songsService *songs.Service, + historyService *history.Service, +) *songDownloadHandler { + return &songDownloadHandler{ + service: service, + songsService: songsService, + historyService: historyService, + } } func (s *songDownloadHandler) HandleIncrementSongPlaysInPlaylist(w http.ResponseWriter, r *http.Request) { @@ -56,4 +66,15 @@ func (s *songDownloadHandler) HandlePlaySong(w http.ResponseWriter, r *http.Requ w.WriteHeader(http.StatusInternalServerError) return } + + profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) + if !profileIdCorrect { + log.Errorln("pashi nahui") + return + } + err = s.historyService.AddSongToHistory(id, profileId) + if err != nil { + log.Errorln(err) + return + } } diff --git a/handlers/handler.go b/handlers/handler.go index fb337db3..42d729f1 100644 --- a/handlers/handler.go +++ b/handlers/handler.go @@ -78,6 +78,20 @@ func (a *Handler) NoAuthPage(h http.HandlerFunc) http.HandlerFunc { } } +// OptionalAuthApi authenticates a page's handler optionally (without 401). +func (a *Handler) OptionalAuthApi(h http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + profile, err := a.authenticate(r) + if err != nil { + h(w, r) + return + } + ctx := context.WithValue(r.Context(), ProfileIdKey, profile.Id) + ctx = context.WithValue(ctx, FullNameKey, profile.Name) + h(w, r.WithContext(ctx)) + } +} + // AuthApi authenticates an API's handler. func (a *Handler) AuthApi(h http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { diff --git a/models/history.go b/models/history.go index 2d6645c8..cbc7775f 100644 --- a/models/history.go +++ b/models/history.go @@ -3,13 +3,12 @@ package models import "time" type History struct { - SongId uint `gorm:"primaryKey"` - ProfileId uint `gorm:"primaryKey"` - + Id uint `gorm:"primaryKey;autoIncrement"` + SongId uint + ProfileId uint CreatedAt time.Time - UpdatedAt time.Time } func (h History) GetId() uint { - return h.ProfileId | h.SongId + return h.Id } diff --git a/services/history/history.go b/services/history/history.go new file mode 100644 index 00000000..a4a487ba --- /dev/null +++ b/services/history/history.go @@ -0,0 +1,36 @@ +package history + +import ( + "dankmuzikk/db" + "dankmuzikk/entities" + "dankmuzikk/models" + "errors" +) + +type Service struct { + repo db.UnsafeCRUDRepo[models.History] + songRepo db.GetterRepo[models.Song] +} + +func New(repo db.UnsafeCRUDRepo[models.History], songRepo db.GetterRepo[models.Song]) *Service { + return &Service{ + repo: repo, + songRepo: songRepo, + } +} + +func (h *Service) AddSongToHistory(songYtId string, profileId uint) error { + song, err := h.songRepo.GetByConds("yt_id = ?", songYtId) + if err != nil { + return err + } + + return h.repo.Add(&models.History{ + ProfileId: profileId, + SongId: song[0].Id, + }) +} + +func (h *Service) Get() ([]entities.Song, error) { + return nil, errors.New("not implemented") +} From 20c25125dbbda7fddecc70e13ec254f0c65aabc5 Mon Sep 17 00:00:00 2001 From: Baraa Al-Masri Date: Thu, 23 May 2024 04:42:34 +0300 Subject: [PATCH 3/4] feat(history): add history on the home screen --- cmd/server/server.go | 2 +- handlers/apis/songs.go | 19 ++++----- handlers/pages/pages.go | 18 +++++++- services/history/history.go | 51 +++++++++++++++++++++-- views/pages/index.templ | 83 ++++++++++++++++++++++++++++++------- 5 files changed, 141 insertions(+), 32 deletions(-) diff --git a/cmd/server/server.go b/cmd/server/server.go index 479fbbd1..2872e052 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -70,7 +70,7 @@ func StartServer(staticFS embed.FS) error { }) pagesHandler.Handle("/muzikkx/", http.StripPrefix("/muzikkx", http.FileServer(http.Dir(config.Env().YouTube.MusicDir)))) - pagesRouter := pages.NewPagesHandler(profileRepo, playlistsService, jwtUtil, &search.ScraperSearch{}, downloadService) + pagesRouter := pages.NewPagesHandler(profileRepo, playlistsService, jwtUtil, &search.ScraperSearch{}, downloadService, historyService) pagesHandler.HandleFunc("/", gHandler.OptionalAuthPage(pagesRouter.HandleHomePage)) pagesHandler.HandleFunc("/signup", gHandler.AuthPage(pagesRouter.HandleSignupPage)) pagesHandler.HandleFunc("/login", gHandler.AuthPage(pagesRouter.HandleLoginPage)) diff --git a/handlers/apis/songs.go b/handlers/apis/songs.go index e3fdac41..0fcd6db3 100644 --- a/handlers/apis/songs.go +++ b/handlers/apis/songs.go @@ -60,21 +60,18 @@ func (s *songDownloadHandler) HandlePlaySong(w http.ResponseWriter, r *http.Requ return } - err := s.service.DownloadYoutubeSong(id) - if err != nil { - log.Errorln(err) - w.WriteHeader(http.StatusInternalServerError) - return - } - profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) - if !profileIdCorrect { - log.Errorln("pashi nahui") - return + if profileIdCorrect { + err := s.historyService.AddSongToHistory(id, profileId) + if err != nil { + log.Errorln(err) + } } - err = s.historyService.AddSongToHistory(id, profileId) + + err := s.service.DownloadYoutubeSong(id) if err != nil { log.Errorln(err) + w.WriteHeader(http.StatusInternalServerError) return } } diff --git a/handlers/pages/pages.go b/handlers/pages/pages.go index 2814a5f9..b90b1bea 100644 --- a/handlers/pages/pages.go +++ b/handlers/pages/pages.go @@ -7,6 +7,7 @@ import ( "dankmuzikk/handlers" "dankmuzikk/log" "dankmuzikk/models" + "dankmuzikk/services/history" "dankmuzikk/services/jwt" "dankmuzikk/services/playlists" "dankmuzikk/services/youtube/download" @@ -28,6 +29,7 @@ type pagesHandler struct { jwtUtil jwt.Manager[jwt.Json] ytSearch search.Service downloadService *download.Service + historyService *history.Service } func NewPagesHandler( @@ -36,6 +38,7 @@ func NewPagesHandler( jwtUtil jwt.Manager[jwt.Json], ytSearch search.Service, downloadService *download.Service, + historyService *history.Service, ) *pagesHandler { return &pagesHandler{ profileRepo: profileRepo, @@ -43,15 +46,26 @@ func NewPagesHandler( jwtUtil: jwtUtil, ytSearch: ytSearch, downloadService: downloadService, + historyService: historyService, } } func (p *pagesHandler) HandleHomePage(w http.ResponseWriter, r *http.Request) { + var recentPlays []entities.Song + var err error + profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) + if profileIdCorrect { + recentPlays, err = p.historyService.Get(profileId) + if err != nil { + log.Errorln(err) + } + } + if handlers.IsNoLayoutPage(r) { - pages.Index().Render(r.Context(), w) + pages.Index(recentPlays).Render(r.Context(), w) return } - layouts.Default(pages.Index()).Render(r.Context(), w) + layouts.Default(pages.Index(recentPlays)).Render(r.Context(), w) } func (p *pagesHandler) HandleAboutPage(w http.ResponseWriter, r *http.Request) { diff --git a/services/history/history.go b/services/history/history.go index a4a487ba..ff422f0c 100644 --- a/services/history/history.go +++ b/services/history/history.go @@ -4,7 +4,7 @@ import ( "dankmuzikk/db" "dankmuzikk/entities" "dankmuzikk/models" - "errors" + "time" ) type Service struct { @@ -31,6 +31,51 @@ func (h *Service) AddSongToHistory(songYtId string, profileId uint) error { }) } -func (h *Service) Get() ([]entities.Song, error) { - return nil, errors.New("not implemented") +func (h *Service) Get(profileId uint) ([]entities.Song, error) { + gigaQuery := `SELECT yt_id, title, artist, thumbnail_url, duration, h.created_at + FROM + histories h JOIN songs + ON + songs.id = h.song_id + WHERE h.profile_id = ? + ORDER BY h.created_at DESC;` + + rows, err := h.repo. + GetDB(). + Raw(gigaQuery, profileId). + Rows() + if err != nil { + return nil, err + } + defer rows.Close() + + songs := make([]entities.Song, 0) + for rows.Next() { + var song entities.Song + var addedAt time.Time + err = rows.Scan(&song.YtId, &song.Title, &song.Artist, &song.ThumbnailUrl, &song.Duration, &addedAt) + if err != nil { + continue + } + song.AddedAt = whenDidItHappen(addedAt) + songs = append(songs, song) + } + + return songs, nil +} + +func whenDidItHappen(t time.Time) string { + now := time.Now().UTC() + switch { + case t.Day() == now.Day() && t.Month() == now.Month() && t.Year() == now.Year(): + return "today" + case t.Day()+1 == now.Day() && t.Month() == now.Month() && t.Year() == now.Year(): + return "yesterday" + case t.Day()+5 < now.Day() && t.Month() == now.Month() && t.Year() == now.Year(): + return "last week" + case t.Day() == now.Day() && t.Month()+1 == now.Month() && t.Year() == now.Year(): + return "last month" + default: + return t.Format("2, January, 2006") + } } diff --git a/views/pages/index.templ b/views/pages/index.templ index f25f66a5..b05836fa 100644 --- a/views/pages/index.templ +++ b/views/pages/index.templ @@ -2,31 +2,84 @@ package pages import ( "dankmuzikk/views/components/navlink" + "dankmuzikk/entities" + "fmt" ) -templ Index() { +templ Index(recentPlays []entities.Song) {
-

What should you expect?

-

- DankMuzikk is music player that plays music from YouTube but without actually using YouTube, start by typing a song's name into the search bar (song's first load time is slow ~10s). -
- More details  - @navlink.NavLink("in about page", "", "/about") -
-
- And you can check the beta features here beta.dankmuzikk.com -
-
- Happy danking 🎉✨ -

+ if recentPlays != nil && len(recentPlays) != 0 { +

Recent plays

+
+ for _, song := range recentPlays { +
+
+
+
+

{ song.Title }

+

{ song.Artist }

+

Last played { song.AddedAt }

+
+
+ +
+
+
+ } +
+ } else { +

What should you expect?

+

+ DankMuzikk is music player that plays music from YouTube but without actually using YouTube, start by typing a song's name into the search bar (song's first load time is slow ~10s). +
+ More details  + @navlink.NavLink("in about page", "", "/about") +
+
+ And you can check the beta features here beta.dankmuzikk.com +
+
+ Happy danking 🎉✨ +

+ }
From 93f434b84bfe45f1509abddbf3a35c4ee3c5422a Mon Sep 17 00:00:00 2001 From: Baraa Al-Masri Date: Thu, 23 May 2024 04:45:18 +0300 Subject: [PATCH 4/4] chore(history): fix missing height on mobile --- views/pages/index.templ | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/pages/index.templ b/views/pages/index.templ index b05836fa..69130a88 100644 --- a/views/pages/index.templ +++ b/views/pages/index.templ @@ -13,7 +13,7 @@ templ Index(recentPlays []entities.Song) { class={ "w-full", "md:w-auto", "bg-accent-trans-20", "backdrop-blur-lg", "rounded-xl", "rounded-[10px]", "m-[10px]", "md:m-[20px]", "p-[20px]", "md:p-[40px]", "flex", "flex-col", "gap-y-6", - "text-secondary", + "text-secondary", "mb-[170px]", "lg:mb-0", } > if recentPlays != nil && len(recentPlays) != 0 {