From 9f9e1fd3234845a02cd02070fae3c75fed347385 Mon Sep 17 00:00:00 2001 From: Baraa Al-Masri Date: Sat, 18 May 2024 17:15:04 +0300 Subject: [PATCH 01/13] chore(jwt): change payload type to typed map --- handlers/handler.go | 10 +++------- handlers/pages/pages.go | 4 ++-- services/jwt/jwt_impl.go | 4 ++-- services/login/email.go | 19 +++++++++---------- services/login/google.go | 8 ++++---- 5 files changed, 20 insertions(+), 25 deletions(-) diff --git a/handlers/handler.go b/handlers/handler.go index 7c93598..fb337db 100644 --- a/handlers/handler.go +++ b/handlers/handler.go @@ -17,7 +17,7 @@ var noAuthPaths = []string{"/login", "/signup"} // Handler is handler for pages and APIs, where it wraps the common stuff in one place. type Handler struct { profileRepo db.GORMDBGetter - jwtUtil jwt.Decoder[any] + jwtUtil jwt.Decoder[jwt.Json] } // NewHandler returns a new AuthHandler instance. @@ -25,7 +25,7 @@ type Handler struct { // Where BaseDB doesn't provide column selection yet :( func NewHandler( accountRepo db.GORMDBGetter, - jwtUtil jwt.Decoder[any], + jwtUtil jwt.Decoder[jwt.Json], ) *Handler { return &Handler{accountRepo, jwtUtil} } @@ -107,11 +107,7 @@ func (a *Handler) authenticate(r *http.Request) (entities.Profile, error) { if err != nil { return entities.Profile{}, err } - payload, valid := theThing.Payload.(map[string]any) - if !valid || payload == nil { - return entities.Profile{}, err - } - username, validUsername := theThing.Payload.(map[string]any)["username"].(string) + username, validUsername := theThing.Payload["username"].(string) if !validUsername || username == "" { return entities.Profile{}, err } diff --git a/handlers/pages/pages.go b/handlers/pages/pages.go index 1167da4..a899521 100644 --- a/handlers/pages/pages.go +++ b/handlers/pages/pages.go @@ -24,14 +24,14 @@ const ( type pagesHandler struct { profileRepo db.GetterRepo[models.Profile] playlistsService *playlists.Service - jwtUtil jwt.Manager[any] + jwtUtil jwt.Manager[jwt.Json] ytSearch search.Service } func NewPagesHandler( profileRepo db.GetterRepo[models.Profile], playlistsService *playlists.Service, - jwtUtil jwt.Manager[any], + jwtUtil jwt.Manager[jwt.Json], ytSearch search.Service, ) *pagesHandler { return &pagesHandler{ diff --git a/services/jwt/jwt_impl.go b/services/jwt/jwt_impl.go index b55b369..f384723 100644 --- a/services/jwt/jwt_impl.go +++ b/services/jwt/jwt_impl.go @@ -14,8 +14,8 @@ type JWTImpl[T any] struct{} // NewJWTImpl returns a new JWTImpl instance, // and since session tokens are to validate users the working type is models.User -func NewJWTImpl() Manager[any] { - return &JWTImpl[any]{} +func NewJWTImpl() Manager[Json] { + return &JWTImpl[Json]{} } // Sign returns a JWT string(which will be the session token) based on the set JWT secret, diff --git a/services/login/email.go b/services/login/email.go index 9a56a63..2a165ae 100644 --- a/services/login/email.go +++ b/services/login/email.go @@ -18,14 +18,14 @@ type EmailLoginService struct { accountRepo db.CRUDRepo[models.Account] profileRepo db.CRUDRepo[models.Profile] otpRepo db.CRUDRepo[models.EmailVerificationCode] - jwtUtil jwt.Manager[any] + jwtUtil jwt.Manager[jwt.Json] } func NewEmailLoginService( accountRepo db.CRUDRepo[models.Account], profileRepo db.CRUDRepo[models.Profile], otpRepo db.CRUDRepo[models.EmailVerificationCode], - jwtUtil jwt.Manager[any], + jwtUtil jwt.Manager[jwt.Json], ) *EmailLoginService { return &EmailLoginService{ accountRepo: accountRepo, @@ -48,7 +48,7 @@ func (e *EmailLoginService) Login(user entities.LoginRequest) (string, error) { profile[0].Account = account[0] profile[0].AccountId = account[0].Id - verificationToken, err := e.jwtUtil.Sign(map[string]string{ + verificationToken, err := e.jwtUtil.Sign(jwt.Json{ "name": profile[0].Name, "email": profile[0].Account.Email, "username": profile[0].Username, @@ -78,7 +78,7 @@ func (e *EmailLoginService) Signup(user entities.SignupRequest) (string, error) return "", err } - verificationToken, err := e.jwtUtil.Sign(map[string]string{ + verificationToken, err := e.jwtUtil.Sign(jwt.Json{ "name": profile.Name, "email": profile.Account.Email, "username": profile.Username, @@ -91,23 +91,22 @@ func (e *EmailLoginService) Signup(user entities.SignupRequest) (string, error) } func (e *EmailLoginService) VerifyOtp(token string, otp entities.OtpRequest) (string, error) { - user, err := e.jwtUtil.Decode(token, jwt.VerificationToken) + tokeeeen, err := e.jwtUtil.Decode(token, jwt.VerificationToken) if err != nil { return "", err } - mappedUser := user.Payload.(map[string]any) - email, emailExists := mappedUser["email"].(string) + email, emailExists := tokeeeen.Payload["email"].(string) // TODO: ADD THE FUCKING ERRORS SUKA if !emailExists { return "", errors.New("missing email") } - name, nameExists := mappedUser["name"].(string) + name, nameExists := tokeeeen.Payload["name"].(string) // TODO: ADD THE FUCKING ERRORS SUKA if !nameExists { return "", errors.New("missing name") } - username, usernameExists := mappedUser["username"].(string) + username, usernameExists := tokeeeen.Payload["username"].(string) // TODO: ADD THE FUCKING ERRORS SUKA if !usernameExists { return "", errors.New("missing username") @@ -132,7 +131,7 @@ func (e *EmailLoginService) VerifyOtp(token string, otp entities.OtpRequest) (st return "", err } - sessionToken, err := e.jwtUtil.Sign(map[string]string{ + sessionToken, err := e.jwtUtil.Sign(jwt.Json{ "email": email, "name": name, "username": username, diff --git a/services/login/google.go b/services/login/google.go index 713dfab..f828df4 100644 --- a/services/login/google.go +++ b/services/login/google.go @@ -41,14 +41,14 @@ type GoogleLoginService struct { accountRepo db.CRUDRepo[models.Account] profileRepo db.CRUDRepo[models.Profile] otpRepo db.CRUDRepo[models.EmailVerificationCode] - jwtUtil jwt.Manager[any] + jwtUtil jwt.Manager[jwt.Json] } func NewGoogleLoginService( accountRepo db.CRUDRepo[models.Account], profileRepo db.CRUDRepo[models.Profile], otpRepo db.CRUDRepo[models.EmailVerificationCode], - jwtUtil jwt.Manager[any], + jwtUtil jwt.Manager[jwt.Json], ) *GoogleLoginService { return &GoogleLoginService{ accountRepo: accountRepo, @@ -79,7 +79,7 @@ func (g *GoogleLoginService) Login(state, code string) (string, error) { profile[0].Account = account[0] profile[0].AccountId = account[0].Id - verificationToken, err := g.jwtUtil.Sign(map[string]string{ + verificationToken, err := g.jwtUtil.Sign(jwt.Json{ "name": profile[0].Name, "email": profile[0].Account.Email, "username": profile[0].Username, @@ -107,7 +107,7 @@ func (g *GoogleLoginService) Signup(googleUser oauthUserInfo) (string, error) { return "", err } - verificationToken, err := g.jwtUtil.Sign(map[string]string{ + verificationToken, err := g.jwtUtil.Sign(jwt.Json{ "name": profile.Name, "email": profile.Account.Email, "username": profile.Username, From d6876be455bd4ff40e80a5b688c666db65c39eb8 Mon Sep 17 00:00:00 2001 From: Baraa Al-Masri Date: Sat, 18 May 2024 19:47:25 +0300 Subject: [PATCH 02/13] fix(otp): expiry date thingy --- handlers/apis/email_login.go | 13 ++++++++++++- models/verification_code.go | 1 - services/login/email.go | 8 ++++++-- services/login/errors.go | 8 +++++--- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/handlers/apis/email_login.go b/handlers/apis/email_login.go index eb02712..24cec19 100644 --- a/handlers/apis/email_login.go +++ b/handlers/apis/email_login.go @@ -119,7 +119,18 @@ func (e *emailLoginApi) HandleEmailOTPVerification(w http.ResponseWriter, r *htt } sessionToken, err := e.service.VerifyOtp(verificationToken.Value, reqBody) - // TODO: specify errors further suka + if errors.Is(err, login.ErrExpiredVerificationCode) { + status. + GenericError("Expired verification code!"). + Render(context.Background(), w) + return + } + if errors.Is(err, login.ErrInvalidVerificationCode) { + status. + GenericError("Invalid verification code!"). + Render(context.Background(), w) + return + } if err != nil { log.Error(err) // w.WriteHeader(http.StatusInternalServerError) diff --git a/models/verification_code.go b/models/verification_code.go index f7640c6..1c672fa 100644 --- a/models/verification_code.go +++ b/models/verification_code.go @@ -8,7 +8,6 @@ type EmailVerificationCode struct { Account Account Code string `gorm:"not null"` CreatedAt time.Time - UpdatedAt time.Time } func (e EmailVerificationCode) GetId() uint { diff --git a/services/login/email.go b/services/login/email.go index 2a165ae..50dff64 100644 --- a/services/login/email.go +++ b/services/login/email.go @@ -123,12 +123,16 @@ func (e *EmailLoginService) VerifyOtp(token string, otp entities.OtpRequest) (st } verCode := verCodes[len(verCodes)-1] defer func() { - _ = e.otpRepo.Delete("id = ?", verCode.Id) + _ = e.otpRepo.Delete("account_id = ?", account[0].Id) }() + if verCode.CreatedAt.Add(time.Hour / 2).Before(time.Now()) { + return "", ErrExpiredVerificationCode + } + err = bcrypt.CompareHashAndPassword([]byte(verCode.Code), []byte(otp.Code)) if err != nil { - return "", err + return "", ErrInvalidVerificationCode } sessionToken, err := e.jwtUtil.Sign(jwt.Json{ diff --git a/services/login/errors.go b/services/login/errors.go index 7773636..4ecc231 100644 --- a/services/login/errors.go +++ b/services/login/errors.go @@ -3,7 +3,9 @@ package login import "errors" var ( - ErrAccountNotFound = errors.New("account was not found") - ErrProfileNotFound = errors.New("profile was not found") - ErrAccountExists = errors.New("an account with the associated email already exists") + ErrAccountNotFound = errors.New("account was not found") + ErrProfileNotFound = errors.New("profile was not found") + ErrAccountExists = errors.New("an account with the associated email already exists") + ErrExpiredVerificationCode = errors.New("expired verification code") + ErrInvalidVerificationCode = errors.New("invalid verification code") ) From 44c36aaf18216ced8d6b5ea9b7088a769a2ea491 Mon Sep 17 00:00:00 2001 From: Baraa Al-Masri Date: Sat, 18 May 2024 21:20:59 +0300 Subject: [PATCH 03/13] chore(playlist): check owner before modifying songs --- cmd/server/server.go | 6 +- handlers/apis/playlist.go | 6 +- handlers/apis/songs.go | 8 ++- services/playlists/songs/songs.go | 63 ++++++++++++------- .../search/search_suggestions.templ | 2 +- 5 files changed, 54 insertions(+), 31 deletions(-) diff --git a/cmd/server/server.go b/cmd/server/server.go index cc7aedd..3d27a11 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -39,11 +39,11 @@ func StartServer(staticFS embed.FS) error { songRepo := db.NewBaseDB[models.Song](dbConn) playlistRepo := db.NewBaseDB[models.Playlist](dbConn) playlistOwnersRepo := db.NewBaseDB[models.PlaylistOwner](dbConn) - playlistSongssRepo := db.NewBaseDB[models.PlaylistSong](dbConn) + playlistSongsRepo := db.NewBaseDB[models.PlaylistSong](dbConn) downloadService := download.New(songRepo) - playlistsService := playlists.New(playlistRepo, playlistOwnersRepo, playlistSongssRepo, downloadService) - songsService := songs.New(playlistSongssRepo, songRepo, playlistRepo, downloadService) + playlistsService := playlists.New(playlistRepo, playlistOwnersRepo, playlistSongsRepo, downloadService) + songsService := songs.New(playlistSongsRepo, playlistOwnersRepo, songRepo, playlistRepo, downloadService) jwtUtil := jwt.NewJWTImpl() diff --git a/handlers/apis/playlist.go b/handlers/apis/playlist.go index b945944..397d6a4 100644 --- a/handlers/apis/playlist.go +++ b/handlers/apis/playlist.go @@ -53,7 +53,7 @@ func (p *playlistApi) HandleCreatePlaylist(w http.ResponseWriter, r *http.Reques } func (p *playlistApi) HandleToggleSongInPlaylist(w http.ResponseWriter, r *http.Request) { - _, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) + profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) if !profileIdCorrect { w.Write([]byte("🤷‍♂️")) return @@ -78,9 +78,9 @@ func (p *playlistApi) HandleToggleSongInPlaylist(w http.ResponseWriter, r *http. var err error switch removeSongFromPlaylist { case "false": - err = p.songService.AddSongToPlaylist(songId, playlistId) + err = p.songService.AddSongToPlaylist(songId, playlistId, profileId) case "true": - err = p.songService.RemoveSongFromPlaylist(songId, playlistId) + err = p.songService.RemoveSongFromPlaylist(songId, playlistId, profileId) } if err != nil { diff --git a/handlers/apis/songs.go b/handlers/apis/songs.go index 1e228a6..7978812 100644 --- a/handlers/apis/songs.go +++ b/handlers/apis/songs.go @@ -2,6 +2,7 @@ package apis import ( "dankmuzikk/entities" + "dankmuzikk/handlers" "dankmuzikk/log" "dankmuzikk/services/playlists/songs" "dankmuzikk/services/youtube/download" @@ -20,6 +21,11 @@ func NewDownloadHandler(service *download.Service, songsService *songs.Service) } func (s *songDownloadHandler) HandleIncrementSongPlaysInPlaylist(w http.ResponseWriter, r *http.Request) { + profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) + if !profileIdCorrect { + w.Write([]byte("🤷‍♂️")) + return + } songId := r.URL.Query().Get("song-id") if songId == "" { w.WriteHeader(http.StatusBadRequest) @@ -31,7 +37,7 @@ func (s *songDownloadHandler) HandleIncrementSongPlaysInPlaylist(w http.Response return } - err := s.songsService.IncrementSongPlays(songId, playlistId) + err := s.songsService.IncrementSongPlays(songId, playlistId, profileId) if err != nil { log.Errorln(err) w.WriteHeader(http.StatusInternalServerError) diff --git a/services/playlists/songs/songs.go b/services/playlists/songs/songs.go index df90500..5ea0ac8 100644 --- a/services/playlists/songs/songs.go +++ b/services/playlists/songs/songs.go @@ -9,24 +9,27 @@ import ( // Service represents songs in platlists management service, // where it adds and deletes songs to and from playlists type Service struct { - playlistSongRepo db.UnsafeCRUDRepo[models.PlaylistSong] - songRepo db.UnsafeCRUDRepo[models.Song] - playlistRepo db.UnsafeCRUDRepo[models.Playlist] - downloadService *download.Service + playlistSongRepo db.UnsafeCRUDRepo[models.PlaylistSong] + playlistOwnerRepo db.CRUDRepo[models.PlaylistOwner] + songRepo db.UnsafeCRUDRepo[models.Song] + playlistRepo db.UnsafeCRUDRepo[models.Playlist] + downloadService *download.Service } // New accepts repos lol, and returns a new instance to the songs playlists service. func New( playlistSongRepo db.UnsafeCRUDRepo[models.PlaylistSong], + playlistOwnerRepo db.CRUDRepo[models.PlaylistOwner], songRepo db.UnsafeCRUDRepo[models.Song], playlistRepo db.UnsafeCRUDRepo[models.Playlist], downloadService *download.Service, ) *Service { return &Service{ - playlistSongRepo: playlistSongRepo, - songRepo: songRepo, - playlistRepo: playlistRepo, - downloadService: downloadService, + playlistSongRepo: playlistSongRepo, + playlistOwnerRepo: playlistOwnerRepo, + songRepo: songRepo, + playlistRepo: playlistRepo, + downloadService: downloadService, } } @@ -34,15 +37,20 @@ func New( // checks if the actual song and playlist exist then adds the song to the given playlist, // and returns an occurring error. // TODO: check playlist's owner :) -func (s *Service) AddSongToPlaylist(songId, playlistPubId string) error { - song, err := s.songRepo.GetByConds("yt_id = ?", songId) +func (s *Service) AddSongToPlaylist(songId, playlistPubId string, ownerId uint) error { + playlist, err := s.playlistRepo.GetByConds("public_id = ?", playlistPubId) if err != nil { return err } - playlist, err := s.playlistRepo.GetByConds("public_id = ?", playlistPubId) + _, err = s.playlistOwnerRepo.GetByConds("profile_id = ? AND playlist_id = ?", ownerId, playlist[0].Id) if err != nil { return err } + song, err := s.songRepo.GetByConds("yt_id = ?", songId) + if err != nil { + return err + } + err = s.playlistSongRepo.Add(&models.PlaylistSong{ PlaylistId: playlist[0].Id, SongId: song[0].Id, @@ -57,28 +65,33 @@ func (s *Service) AddSongToPlaylist(songId, playlistPubId string) error { // IncrementSongPlays increases the song's play times in the given playlist. // Checks for the song and playlist first, yada yada... // TODO: check playlist's owner :) -func (s *Service) IncrementSongPlays(songId, playlistPubId string) error { - var song models.Song +func (s *Service) IncrementSongPlays(songId, playlistPubId string, ownerId uint) error { + var playlist models.Playlist err := s. songRepo. GetDB(). - Model(new(models.Song)). + Model(new(models.Playlist)). Select("id"). - Where("yt_id = ?", songId). - First(&song). + Where("public_id = ?", playlistPubId). + First(&playlist). Error if err != nil { return err } - var playlist models.Playlist + _, err = s.playlistOwnerRepo.GetByConds("profile_id = ? AND playlist_id = ?", ownerId, playlist.Id) + if err != nil { + return err + } + + var song models.Song err = s. songRepo. GetDB(). - Model(new(models.Playlist)). + Model(new(models.Song)). Select("id"). - Where("public_id = ?", playlistPubId). - First(&playlist). + Where("yt_id = ?", songId). + First(&song). Error if err != nil { return err @@ -110,12 +123,16 @@ func (s *Service) IncrementSongPlays(songId, playlistPubId string) error { // checks if the actual song and playlist exist then removes the song to the given playlist, // and returns an occurring error. // TODO: check playlist's owner :) -func (s *Service) RemoveSongFromPlaylist(songId, playlistPubId string) error { - song, err := s.songRepo.GetByConds("yt_id = ?", songId) +func (s *Service) RemoveSongFromPlaylist(songId, playlistPubId string, ownerId uint) error { + playlist, err := s.playlistRepo.GetByConds("public_id = ?", playlistPubId) if err != nil { return err } - playlist, err := s.playlistRepo.GetByConds("public_id = ?", playlistPubId) + _, err = s.playlistOwnerRepo.GetByConds("profile_id = ? AND playlist_id = ?", ownerId, playlist[0].Id) + if err != nil { + return err + } + song, err := s.songRepo.GetByConds("yt_id = ?", songId) if err != nil { return err } diff --git a/views/components/search/search_suggestions.templ b/views/components/search/search_suggestions.templ index c64f0fa..3e2ceb0 100644 --- a/views/components/search/search_suggestions.templ +++ b/views/components/search/search_suggestions.templ @@ -22,7 +22,7 @@ templ SearchSuggestions(suggestions []string, originalQuery string) { data-loading-target="#loading" data-loading-class-remove="hidden" data-loading-path={ fmt.Sprintf("/search?no_layout=true&query=%s", suggestion) } - class={ "w-full", "p-[10px]" } + class={ "w-full", "p-[10px]", "no-underline" } id={ fmt.Sprintf("search-suggestion-%d", i) } onClick={ searchNoRealod(suggestion) } > From 2c029ef7cfc0148fef5acbeee3bc01d041f36a72 Mon Sep 17 00:00:00 2001 From: Baraa Al-Masri Date: Sat, 18 May 2024 22:00:23 +0300 Subject: [PATCH 04/13] chore(playlist): remove song directly from playlist --- views/pages/playlist.templ | 92 ++++++++++++++++++++++++++++++++------ 1 file changed, 79 insertions(+), 13 deletions(-) diff --git a/views/pages/playlist.templ b/views/pages/playlist.templ index ff49d21..e58ef83 100644 --- a/views/pages/playlist.templ +++ b/views/pages/playlist.templ @@ -4,6 +4,7 @@ import ( "dankmuzikk/entities" "fmt" "dankmuzikk/views/components/navlink" + "dankmuzikk/views/components/popover" ) templ Playlist(pl entities.Playlist) { @@ -48,26 +49,34 @@ templ Playlist(pl entities.Playlist) { for _, song := range pl.Songs {
-
-

{ song.Title }

-

{ song.Artist }

-

Added on { song.AddedAt }

- if song.PlayTimes == 1 { -

Played once

- } else if song.PlayTimes > 1 { -

Played { fmt.Sprint( song.PlayTimes) } times

- } +
+
+

{ song.Title }

+

{ song.Artist }

+

Added on { song.AddedAt }

+ if song.PlayTimes == 1 { +

Played once

+ } else if song.PlayTimes > 1 { +

Played { fmt.Sprint( song.PlayTimes) } times

+ } +
+
+ @popover.Popover(song.YtId, "Options", songOptionsToggle(), songOptions(song, pl.PublicId)) +
} @@ -80,6 +89,34 @@ templ Playlist(pl entities.Playlist) { } +templ songOptionsToggle() { + + + + + +} + +templ songOptions(song entities.Song, playlistId string) { +
+

Song's options

+ +
+} + templ backButton() {
@@ -100,3 +137,32 @@ script playPlaylist(playlist entities.Playlist) { script playSongFromPlaylist(songId string, playlist entities.Playlist) { window.Player.playSongFromPlaylist(songId, playlist) } + +script removeSongFromPlaylist(songId, playlistId string) { + Utils.showLoading(); + fetch("/api/toggle-song-in-playlist?song-id=" + songId + "&playlist-id=" + playlistId + "&remove=true", { + method: "PUT", + }) + .then((res) => { + if (res.ok) { + fetch("/playlist/"+playlistId+"?no_layout=true") + .then((res) => res.text()) + .then((page) => { + if (!page || page.length === 0) { + window.location.reload() + return; + } + document.getElementById("main-contents").innerHTML = page; + }) + .catch(() => { + window.location.reload() + }) + } + }) + .catch((err) => { + window.alert(err); + }) + .finally(() => { + Utils.hideLoading(); + }); +} From fba1fb538e3d9918ac9af675af9ef72c61ab010c Mon Sep 17 00:00:00 2001 From: Baraa Al-Masri Date: Sat, 18 May 2024 22:08:36 +0300 Subject: [PATCH 05/13] fix(playlist): remove the actual song's element instead of reloading the whole playlist, which is not so nice on the network --- views/pages/playlist.templ | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/views/pages/playlist.templ b/views/pages/playlist.templ index e58ef83..75c2717 100644 --- a/views/pages/playlist.templ +++ b/views/pages/playlist.templ @@ -52,6 +52,7 @@ templ Playlist(pl entities.Playlist) { "bg-secondary-trans-20", "rounded-[10px]", "p-2", "lg:p-5", "flex", "flex-row", "items-center", "gap-5", "justify-between", } + id={ "song-" + song.YtId } >
{ if (res.ok) { - fetch("/playlist/"+playlistId+"?no_layout=true") - .then((res) => res.text()) - .then((page) => { - if (!page || page.length === 0) { - window.location.reload() - return; - } - document.getElementById("main-contents").innerHTML = page; - }) - .catch(() => { - window.location.reload() - }) + document.getElementById("song-" + songId).remove(); + } else { + window.location.reload() } }) .catch((err) => { - window.alert(err); + window.location.reload() }) .finally(() => { Utils.hideLoading(); From 4b78e4265d11f6208071117e0e4c18784cde54c4 Mon Sep 17 00:00:00 2001 From: Baraa Al-Masri Date: Sat, 18 May 2024 22:10:10 +0300 Subject: [PATCH 06/13] chore(playlist): show errors when removing a song --- views/pages/playlist.templ | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/views/pages/playlist.templ b/views/pages/playlist.templ index 75c2717..66dc1e6 100644 --- a/views/pages/playlist.templ +++ b/views/pages/playlist.templ @@ -148,11 +148,11 @@ script removeSongFromPlaylist(songId, playlistId string) { if (res.ok) { document.getElementById("song-" + songId).remove(); } else { - window.location.reload() + window.alert("Oopsie something went wrong!"); } }) .catch((err) => { - window.location.reload() + window.alert("Oopsie something went wrong!\n", err); }) .finally(() => { Utils.hideLoading(); From 7a5be1944be774ade07945f9dc2138191784419f Mon Sep 17 00:00:00 2001 From: Baraa Al-Masri Date: Sat, 18 May 2024 22:30:08 +0300 Subject: [PATCH 07/13] chore(player): change current song's bg in playlist --- static/js/player.js | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/static/js/player.js b/static/js/player.js index 7914537..ee17d05 100644 --- a/static/js/player.js +++ b/static/js/player.js @@ -73,6 +73,7 @@ class PlaylistPlayer { * @param {string} songYtIt */ play(songYtIt = "") { + this.setSongNotPlayingStyle(); this.#currentSongIndex = this.#currentPlaylist.songs.findIndex( (song) => song.yt_id === songYtIt, ); @@ -82,9 +83,11 @@ class PlaylistPlayer { const songToPlay = this.#currentPlaylist.songs[this.#currentSongIndex]; playNewSong(songToPlay); this.#updateSongPlays(); + this.setSongPlayingStyle(); } next(shuffle = false, loop = false) { + this.setSongNotPlayingStyle(); if ( !loop && !shuffle && @@ -93,19 +96,19 @@ class PlaylistPlayer { stopMuzikk(); return; } - this.#currentSongIndex = shuffle ? Math.floor(Math.random() * this.#currentPlaylist.songs.length) : loop && this.#currentSongIndex + 1 >= this.#currentPlaylist.songs.length ? 0 : this.#currentSongIndex + 1; - const songToPlay = this.#currentPlaylist.songs[this.#currentSongIndex]; playNewSong(songToPlay); this.#updateSongPlays(); + this.setSongPlayingStyle(); } previous(shuffle = false, loop = false) { + this.setSongNotPlayingStyle(); if (!loop && !shuffle && this.#currentSongIndex - 1 < 0) { stopMuzikk(); return; @@ -115,9 +118,24 @@ class PlaylistPlayer { : loop && this.#currentSongIndex - 1 < 0 ? this.#currentPlaylist.songs.length - 1 : this.#currentSongIndex - 1; + this.setSongNotPlayingStyle(); const songToPlay = this.#currentPlaylist.songs[this.#currentSongIndex]; playNewSong(songToPlay); this.#updateSongPlays(); + this.setSongPlayingStyle(); + } + + setSongPlayingStyle() { + document.getElementById( + "song-" + this.#currentPlaylist.songs[this.#currentSongIndex].yt_id, + ).style.backgroundColor = "var(--accent-color-30)"; + } + + setSongNotPlayingStyle() { + for (const song of this.#currentPlaylist.songs) { + document.getElementById("song-" + song.yt_id).style.backgroundColor = + "var(--secondary-color-20)"; + } } /** From 67eba760c8bdc85baa3468ac5586622cd9478839 Mon Sep 17 00:00:00 2001 From: Baraa Al-Masri Date: Sat, 18 May 2024 22:52:25 +0300 Subject: [PATCH 08/13] fix(playlist): fix songs order (chrono order) --- services/playlists/playlists.go | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/services/playlists/playlists.go b/services/playlists/playlists.go index 7c94a51..024ff7d 100644 --- a/services/playlists/playlists.go +++ b/services/playlists/playlists.go @@ -6,6 +6,7 @@ import ( "dankmuzikk/models" "dankmuzikk/services/youtube/download" "fmt" + "slices" "strings" "time" @@ -169,17 +170,22 @@ func (p *Service) Get(playlistPubId string, ownerId uint) (entities.Playlist, er Title: dbPlaylists[0].Title, }, ErrEmptyPlaylist } + slices.SortFunc(playlistSongs, func(i, j models.PlaylistSong) int { + return i.CreatedAt.Compare(j.CreatedAt) + }) mappedPlaylistSongsToPlaysSuka := make(map[uint]int) mappedPlaylistSongsToCreatedAtSuka := make(map[uint]time.Time) - for _, playlistSong := range playlistSongs { + songsIdOrderSuka := make(map[int]uint) + for i, playlistSong := range playlistSongs { mappedPlaylistSongsToPlaysSuka[playlistSong.SongId] = playlistSong.PlayTimes mappedPlaylistSongsToCreatedAtSuka[playlistSong.SongId] = playlistSong.CreatedAt + songsIdOrderSuka[i] = playlistSong.SongId } - songs := make([]entities.Song, len(dbPlaylists[0].Songs)) - for i, song := range dbPlaylists[0].Songs { - songs[i] = entities.Song{ + mappedSongByIdSuka := make(map[uint]entities.Song) + for _, song := range dbPlaylists[0].Songs { + mappedSongByIdSuka[song.Id] = entities.Song{ YtId: song.YtId, Title: song.Title, Artist: song.Artist, @@ -190,6 +196,12 @@ func (p *Service) Get(playlistPubId string, ownerId uint) (entities.Playlist, er } } + songs := make([]entities.Song, len(dbPlaylists[0].Songs)) + for i := 0; i < len(songs); i++ { + songId := songsIdOrderSuka[i] + songs[i] = mappedSongByIdSuka[songId] + } + return entities.Playlist{ PublicId: dbPlaylists[0].PublicId, Title: dbPlaylists[0].Title, From 1bf68bf1a53a5db6e879cb964aa6bd0d74922943 Mon Sep 17 00:00:00 2001 From: Baraa Al-Masri Date: Sat, 18 May 2024 23:48:14 +0300 Subject: [PATCH 09/13] chore(playlist): improve performance of fetching songs in playlist --- services/playlists/playlists.go | 70 +++++++++++---------------------- 1 file changed, 22 insertions(+), 48 deletions(-) diff --git a/services/playlists/playlists.go b/services/playlists/playlists.go index 024ff7d..b15f029 100644 --- a/services/playlists/playlists.go +++ b/services/playlists/playlists.go @@ -6,7 +6,6 @@ import ( "dankmuzikk/models" "dankmuzikk/services/youtube/download" "fmt" - "slices" "strings" "time" @@ -139,7 +138,6 @@ func (p *Service) Get(playlistPubId string, ownerId uint) (entities.Playlist, er Id: ownerId, }). Where("public_id = ?", playlistPubId). - Preload("Songs"). Association("Playlist"). Find(&dbPlaylists) if err != nil { @@ -149,57 +147,33 @@ func (p *Service) Get(playlistPubId string, ownerId uint) (entities.Playlist, er return entities.Playlist{}, ErrUnauthorizedToSeePlaylist } - var playlistSongs []models.PlaylistSong - err = p. - playlistSongsRepo. + gigaQuery := `SELECT yt_id, title, artist, thumbnail_url, duration, ps.created_at, ps.play_times + FROM + playlist_owners po JOIN playlist_songs ps ON po.playlist_id = ps.playlist_id + JOIN songs + ON ps.song_id = songs.id + WHERE ps.playlist_id = ? AND po.profile_id = ? + ORDER BY ps.created_at;` + + rows, err := p.repo. GetDB(). - Model(new(models.PlaylistSong)). - Where("playlist_id = ?", dbPlaylists[0].Id). - Select("song_id", "play_times", "votes", "created_at"). - Find(&playlistSongs). - Error + Raw(gigaQuery, dbPlaylists[0].Id, ownerId). + Rows() if err != nil { - return entities.Playlist{ - PublicId: dbPlaylists[0].PublicId, - Title: dbPlaylists[0].Title, - }, err - } - if len(playlistSongs) == 0 { - return entities.Playlist{ - PublicId: dbPlaylists[0].PublicId, - Title: dbPlaylists[0].Title, - }, ErrEmptyPlaylist - } - slices.SortFunc(playlistSongs, func(i, j models.PlaylistSong) int { - return i.CreatedAt.Compare(j.CreatedAt) - }) - - mappedPlaylistSongsToPlaysSuka := make(map[uint]int) - mappedPlaylistSongsToCreatedAtSuka := make(map[uint]time.Time) - songsIdOrderSuka := make(map[int]uint) - for i, playlistSong := range playlistSongs { - mappedPlaylistSongsToPlaysSuka[playlistSong.SongId] = playlistSong.PlayTimes - mappedPlaylistSongsToCreatedAtSuka[playlistSong.SongId] = playlistSong.CreatedAt - songsIdOrderSuka[i] = playlistSong.SongId + return entities.Playlist{}, err } + defer rows.Close() - mappedSongByIdSuka := make(map[uint]entities.Song) - for _, song := range dbPlaylists[0].Songs { - mappedSongByIdSuka[song.Id] = entities.Song{ - YtId: song.YtId, - Title: song.Title, - Artist: song.Artist, - ThumbnailUrl: song.ThumbnailUrl, - Duration: song.Duration, - PlayTimes: mappedPlaylistSongsToPlaysSuka[song.Id], - AddedAt: mappedPlaylistSongsToCreatedAtSuka[song.Id].Format("2, January, 2006"), + 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, &song.PlayTimes) + if err != nil { + continue } - } - - songs := make([]entities.Song, len(dbPlaylists[0].Songs)) - for i := 0; i < len(songs); i++ { - songId := songsIdOrderSuka[i] - songs[i] = mappedSongByIdSuka[songId] + song.AddedAt = addedAt.Format("2, January, 2006") + songs = append(songs, song) } return entities.Playlist{ From 461d77abb11a9d902a26acec9fb69d052bbc33a3 Mon Sep 17 00:00:00 2001 From: Baraa Al-Masri Date: Sun, 19 May 2024 10:25:30 +0300 Subject: [PATCH 10/13] chore(popover): move popover's bg to its child --- views/components/playlist/new_playlis_popover.templ | 2 +- views/components/playlist/popover.templ | 2 +- views/components/popover/popover.templ | 3 +-- views/components/themeswitch/themeswitch.templ | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/views/components/playlist/new_playlis_popover.templ b/views/components/playlist/new_playlis_popover.templ index 0c1aa28..0f9ad5c 100644 --- a/views/components/playlist/new_playlis_popover.templ +++ b/views/components/playlist/new_playlis_popover.templ @@ -8,7 +8,7 @@ templ NewPlaylistPopover() { templ newPlaylistPopover() {
+

Save this song to...

    diff --git a/views/components/popover/popover.templ b/views/components/popover/popover.templ index cba290f..3204e14 100644 --- a/views/components/popover/popover.templ +++ b/views/components/popover/popover.templ @@ -21,8 +21,7 @@ templ Popover(id, title string, button, child templ.Component) { id={ fmt.Sprintf("popover-%s", id) } class={ "hidden", "absolute", "z-30", "top-[45px]", "right-[0px]", - "bg-primary-trans-20", "backdrop-blur-lg", "shadow-md", "p-[10px]", "rounded-[5px]", "text-secondary", - "min-w-[150px]", + "shadow-md", "min-w-[150px]", } > @child diff --git a/views/components/themeswitch/themeswitch.templ b/views/components/themeswitch/themeswitch.templ index 93d52ce..bc85c0c 100644 --- a/views/components/themeswitch/themeswitch.templ +++ b/views/components/themeswitch/themeswitch.templ @@ -25,7 +25,7 @@ templ ThemeSwitch() { } templ themeSwitch() { -
      +
        for _, theme := range themes {
+ } } From 9ce2b65c0647056d678762d8249d5bfcd7969016 Mon Sep 17 00:00:00 2001 From: Baraa Al-Masri Date: Sun, 19 May 2024 12:25:19 +0300 Subject: [PATCH 13/13] chore(ytdl): add to queue then start the actual thing --- services/youtube/download/download.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/youtube/download/download.go b/services/youtube/download/download.go index 0cfd273..1775277 100644 --- a/services/youtube/download/download.go +++ b/services/youtube/download/download.go @@ -36,6 +36,10 @@ func (d *Service) DownloadYoutubeSong(req entities.Song) error { return nil } + err = d.DownloadYoutubeSongQueue(req.YtId) + if err != nil { + return err + } resp, err := http.Get(fmt.Sprintf("%s/download/%s", config.Env().YouTube.DownloaderUrl, req.YtId)) if err != nil { return err