Skip to content

Commit

Permalink
Merge pull request #38 from mbaraa/feat/plays-history
Browse files Browse the repository at this point in the history
Feat: recent plays history
  • Loading branch information
mbaraa authored May 23, 2024
2 parents 9c9b75f + 93f434b commit b6832bd
Show file tree
Hide file tree
Showing 9 changed files with 225 additions and 26 deletions.
1 change: 1 addition & 0 deletions cmd/migrator/migrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func Migrate() error {
new(models.Playlist),
new(models.PlaylistSong),
new(models.PlaylistOwner),
new(models.History),
)
if err != nil {
return err
Expand Down
9 changes: 6 additions & 3 deletions cmd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"dankmuzikk/handlers/pages"
"dankmuzikk/log"
"dankmuzikk/models"
"dankmuzikk/services/history"
"dankmuzikk/services/jwt"
"dankmuzikk/services/login"
"dankmuzikk/services/playlists"
Expand Down Expand Up @@ -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()

Expand All @@ -67,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))
Expand All @@ -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()
Expand All @@ -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))
Expand Down
3 changes: 2 additions & 1 deletion db/allowed_models.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
26 changes: 22 additions & 4 deletions handlers/apis/songs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -50,6 +60,14 @@ func (s *songDownloadHandler) HandlePlaySong(w http.ResponseWriter, r *http.Requ
return
}

profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint)
if profileIdCorrect {
err := s.historyService.AddSongToHistory(id, profileId)
if err != nil {
log.Errorln(err)
}
}

err := s.service.DownloadYoutubeSong(id)
if err != nil {
log.Errorln(err)
Expand Down
14 changes: 14 additions & 0 deletions handlers/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
18 changes: 16 additions & 2 deletions handlers/pages/pages.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"dankmuzikk/handlers"
"dankmuzikk/log"
"dankmuzikk/models"
"dankmuzikk/services/history"
"dankmuzikk/services/jwt"
"dankmuzikk/services/playlists"
"dankmuzikk/services/youtube/download"
Expand All @@ -28,6 +29,7 @@ type pagesHandler struct {
jwtUtil jwt.Manager[jwt.Json]
ytSearch search.Service
downloadService *download.Service
historyService *history.Service
}

func NewPagesHandler(
Expand All @@ -36,22 +38,34 @@ func NewPagesHandler(
jwtUtil jwt.Manager[jwt.Json],
ytSearch search.Service,
downloadService *download.Service,
historyService *history.Service,
) *pagesHandler {
return &pagesHandler{
profileRepo: profileRepo,
playlistsService: playlistsService,
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) {
Expand Down
14 changes: 14 additions & 0 deletions models/history.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package models

import "time"

type History struct {
Id uint `gorm:"primaryKey;autoIncrement"`
SongId uint
ProfileId uint
CreatedAt time.Time
}

func (h History) GetId() uint {
return h.Id
}
81 changes: 81 additions & 0 deletions services/history/history.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package history

import (
"dankmuzikk/db"
"dankmuzikk/entities"
"dankmuzikk/models"
"time"
)

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(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")
}
}
85 changes: 69 additions & 16 deletions views/pages/index.templ
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,84 @@ package pages

import (
"dankmuzikk/views/components/navlink"
"dankmuzikk/entities"
"fmt"
)

templ Index() {
templ Index(recentPlays []entities.Song) {
<main class={ "w-full", "h-full", "flex", "flex-col", "justify-center", "items-center" }>
<div class={ "flex", "justify-center" }>
<section
class={
"w-full", "md:w-2/3", "bg-accent-trans-20", "backdrop-blur-lg", "rounded-xl",
"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",
}
>
<h2 class={ "text-xl" }>What should you expect?</h2>
<p>
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).
<br/>
More details&nbsp;
@navlink.NavLink("in about page", "", "/about")
<br/>
<br/>
And you can check the beta features here <a href="https://beta.dankmuzikk.com">beta.dankmuzikk.com</a>
<br/>
<br/>
Happy danking 🎉✨
</p>
if recentPlays != nil && len(recentPlays) != 0 {
<h1 class={ "text-secondary", "text-3xl", "lg:text-4xl" }>Recent plays</h1>
<div
class={
"w-full", "overflow-y-scroll", "min-h-[40vh]", "max-h-[50vh]", "md:max-h-[65vh]",
"flex", "flex-col", "gap-5", "lg:my-10",
}
>
for _, song := range recentPlays {
<div
class={
"bg-secondary-trans-20", "rounded-[10px]", "p-2", "lg:p-5",
"flex", "flex-row", "items-center", "gap-5", "justify-between",
}
id={ "song-" + song.YtId }
>
<div
class={
"w-[80px]", "h-[80px]", "lg:w-[125px]", "lg:h-[125px]", "rounded-md", "bg-accent", "cursor-pointer",
songThumb(fmt.Sprintf("url(\"%s\")", song.ThumbnailUrl)), "bg-repeat", "bg-cover", "bg-center",
}
onClick={ playSong(song) }
></div>
<div class={ "w-11/12", "flex", "justify-between", "items-center" }>
<div
class={ "cursor-pointer", "flex", "flex-col", "lg:gap-2" }
onClick={ playSong(song) }
>
<p class={ "text-lg", "max-w-[200px]", "md:max-w-[300px]", "lg:max-w-[600px]", "overflow-hidden", "text-nowrap", "text-ellipsis" }>{ song.Title }</p>
<p class={ "text-md", "max-w-[200px]", "md:max-w-[300px]", "lg:max-w-[600px]", "overflow-hidden", "text-nowrap", "text-ellipsis" }>{ song.Artist }</p>
<p class={ "text-md", "max-w-[200px]", "lg:max-w-[600px]", "overflow-hidden", "text-nowrap", "text-ellipsis" }>Last played { song.AddedAt }</p>
</div>
<div class={ "w-[30px]", "h-auto", "relative" }>
<button
class={
"popover-trigger", "p-2", "rounded-md", "hover:bg-accent-trans-20",
"flex", "justify-center", "items-center",
}
title="Download song"
type="button"
onClick={ downloadSong(song.YtId, song.Title) }
>
<img class={ "max-w-[30px]", "h-[25px]", "md:h-[30px]" } src="/static/icons/download-icon.svg" alt="Download song"/>
</button>
</div>
</div>
</div>
}
</div>
} else {
<h2 class={ "text-xl" }>What should you expect?</h2>
<p>
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).
<br/>
More details&nbsp;
@navlink.NavLink("in about page", "", "/about")
<br/>
<br/>
And you can check the beta features here <a href="https://beta.dankmuzikk.com">beta.dankmuzikk.com</a>
<br/>
<br/>
Happy danking 🎉✨
</p>
}
</section>
</div>
</main>
Expand Down

0 comments on commit b6832bd

Please sign in to comment.