From 44de4ac206d262d5e1b5f1d0af536027dbe858b5 Mon Sep 17 00:00:00 2001 From: Baraa Al-Masri Date: Sun, 30 Jun 2024 23:18:22 +0300 Subject: [PATCH 01/13] feat(archive): add zip archiver --- app/services/archive/zip.go | 91 +++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 app/services/archive/zip.go diff --git a/app/services/archive/zip.go b/app/services/archive/zip.go new file mode 100644 index 0000000..8f7640b --- /dev/null +++ b/app/services/archive/zip.go @@ -0,0 +1,91 @@ +package archive + +import ( + "archive/zip" + "bytes" + "io" + "os" +) + +const tmpDir = "/tmp" + +type Service struct{} + +func NewService() *Service { + return &Service{} +} + +func (z *Service) CreateZip() (Archive, error) { + zipFile, err := os.CreateTemp(tmpDir, "playlist_*.zip") + if err != nil { + return nil, err + } + return newZip(zipFile), nil +} + +type Archive interface { + AddFile(*os.File) error + RemoveFile(string) error + Deflate() (io.Reader, error) +} + +type Zip struct { + files []*os.File + zipW *zip.Writer + zipF *os.File +} + +func newZip(zipFile *os.File) *Zip { + zipWriter := zip.NewWriter(zipFile) + return &Zip{ + zipF: zipFile, + zipW: zipWriter, + } +} + +func (z *Zip) AddFile(f *os.File) error { + stat, err := f.Stat() + if err != nil { + return err + } + header, err := zip.FileInfoHeader(stat) + if err != nil { + return err + } + header.Method = zip.Deflate + fileInArchive, err := z.zipW.CreateHeader(header) + if err != nil { + return err + } + _, err = io.Copy(fileInArchive, f) + if err != nil { + return err + } + f.Close() + + return nil + +} + +func (z *Zip) RemoveFile(_ string) error { + panic("not implemented") // TODO: Implement +} + +func (z *Zip) Deflate() (io.Reader, error) { + defer func() { + _ = z.zipF.Close() + _ = os.Remove(z.zipF.Name()) + }() + _ = z.zipW.Flush() + _ = z.zipW.Close() + + z.zipF.Seek(0, 0) + + buf := bytes.NewBuffer([]byte{}) + _, err := io.Copy(buf, z.zipF) + if err != nil { + return nil, err + } + + return buf, nil +} From 578d2ea2bca98b3aa8caa19fef3c10a130fd7eeb Mon Sep 17 00:00:00 2001 From: Baraa Al-Masri Date: Sun, 30 Jun 2024 23:18:52 +0300 Subject: [PATCH 02/13] feat(playlist): add download playlist --- app/services/playlists/playlists.go | 44 ++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/app/services/playlists/playlists.go b/app/services/playlists/playlists.go index 3156ca2..bad5431 100644 --- a/app/services/playlists/playlists.go +++ b/app/services/playlists/playlists.go @@ -1,12 +1,16 @@ package playlists import ( + "dankmuzikk/config" "dankmuzikk/db" "dankmuzikk/entities" "dankmuzikk/models" + "dankmuzikk/services/archive" "dankmuzikk/services/nanoid" "errors" "fmt" + "io" + "os" "time" ) @@ -16,6 +20,7 @@ type Service struct { repo db.UnsafeCRUDRepo[models.Playlist] playlistOwnersRepo db.CRUDRepo[models.PlaylistOwner] playlistSongsRepo db.UnsafeCRUDRepo[models.PlaylistSong] + zipService *archive.Service } // New accepts a playlist repo, a playlist pwners, and returns a new instance to the playlists service. @@ -23,8 +28,14 @@ func New( repo db.UnsafeCRUDRepo[models.Playlist], playlistOwnersRepo db.CRUDRepo[models.PlaylistOwner], playlistSongsRepo db.UnsafeCRUDRepo[models.PlaylistSong], + zipService *archive.Service, ) *Service { - return &Service{repo, playlistOwnersRepo, playlistSongsRepo} + return &Service{ + repo: repo, + playlistOwnersRepo: playlistOwnersRepo, + playlistSongsRepo: playlistSongsRepo, + zipService: zipService, + } } // CreatePlaylist creates a new playlist with with provided details for the given account's profile. @@ -287,3 +298,34 @@ func (p *Service) GetAllMappedForAddPopover(ownerId uint) ([]entities.Playlist, return playlists, mappedPlaylists, nil } + +// Download zips the provided playlist, +// then returns an io.Reader with the playlist's songs, and an occurring error. +func (p *Service) Download(playlistPubId string, ownerId uint) (io.Reader, error) { + pl, _, err := p.Get(playlistPubId, ownerId) + if err != nil { + return nil, err + } + + files := make([]*os.File, len(pl.Songs)) + for i, song := range pl.Songs { + files[i], err = os.Open(fmt.Sprintf("%s/%s.mp3", config.Env().YouTube.MusicDir, song.YtId)) + if err != nil { + return nil, err + } + } + + zip, err := p.zipService.CreateZip() + if err != nil { + return nil, err + } + + for _, file := range files { + err = zip.AddFile(file) + if err != nil { + return nil, err + } + } + + return zip.Deflate() +} From ecec51dc36ee4cf1d3e6aaa5474d305a2ee8a1ab Mon Sep 17 00:00:00 2001 From: Baraa Al-Masri Date: Sun, 30 Jun 2024 23:19:18 +0300 Subject: [PATCH 03/13] chore(seeder): remove seeder lol --- app/cmd/seeder/seeder.go | 124 --------------------------------------- app/main.go | 3 - 2 files changed, 127 deletions(-) delete mode 100644 app/cmd/seeder/seeder.go diff --git a/app/cmd/seeder/seeder.go b/app/cmd/seeder/seeder.go deleted file mode 100644 index 11661c2..0000000 --- a/app/cmd/seeder/seeder.go +++ /dev/null @@ -1,124 +0,0 @@ -package seeder - -import ( - "dankmuzikk/db" - "dankmuzikk/entities" - "dankmuzikk/log" - "dankmuzikk/models" - playlistspkg "dankmuzikk/services/playlists" - "dankmuzikk/tests" - "math/rand" - "time" - - "gorm.io/gorm" -) - -var ( - dbConn *gorm.DB - accountRepo db.UnsafeCRUDRepo[models.Account] - profileRepo db.UnsafeCRUDRepo[models.Profile] - songRepo db.UnsafeCRUDRepo[models.Song] - playlistRepo db.UnsafeCRUDRepo[models.Playlist] - playlistSongsRepo db.UnsafeCRUDRepo[models.PlaylistSong] - playlistOwnerRepo db.UnsafeCRUDRepo[models.PlaylistOwner] - - profiles = tests.Profiles() - songs = tests.Songs() - playlists = tests.Playlists() - - random = rand.New(rand.NewSource(time.Now().UnixMicro())) -) - -func SeedDb() error { - var err error - - dbConn, err = db.Connector() - if err != nil { - return err - } - - accountRepo = db.NewBaseDB[models.Account](dbConn) - profileRepo = db.NewBaseDB[models.Profile](dbConn) - songRepo = db.NewBaseDB[models.Song](dbConn) - playlistRepo = db.NewBaseDB[models.Playlist](dbConn) - playlistSongsRepo = db.NewBaseDB[models.PlaylistSong](dbConn) - playlistOwnerRepo = db.NewBaseDB[models.PlaylistOwner](dbConn) - - playlistService := playlistspkg.New(playlistRepo, playlistOwnerRepo, nil) - - pl, err := playlistService.GetAll(400) - if err != nil { - return err - } - log.Infof("%+v\n", pl) - - err = playlistService.CreatePlaylist(entities.Playlist{ - Title: "Danki Muzikki", - }, 400) - if err != nil { - return err - } - - err = playlistService.DeletePlaylist("a1a4b25f6eac4fb08222d14cadcfc7cd", 400) - if err != nil { - return err - } - - return nil - - err = seedProfiles() - if err != nil { - return err - } - - err = seedPlaylists() - if err != nil { - return err - } - - err = addPlaylistsToProfiles() - if err != nil { - return err - } - - return nil -} - -func seedProfiles() error { - for i := range profiles { - err := profileRepo.Add(&profiles[i]) - if err != nil { - return err - } - } - return nil -} - -func seedPlaylists() error { - for i := range playlists { - for _, song := range playlists[i].Songs { - err := songRepo.Add(song) - if err != nil { - return err - } - } - err := playlistRepo.Add(&playlists[i]) - if err != nil { - return err - } - } - return nil -} - -func addPlaylistsToProfiles() error { - for i := 0; i < len(profiles)*3; i++ { - // ignore errors because there will be duplicates lol - _ = playlistOwnerRepo.Add(&models.PlaylistOwner{ - ProfileId: profiles[i%len(profiles)].Id, - PlaylistId: playlists[random.Intn(len(playlists))].Id, - Permissions: models.JoinerPermission, - }) - random.Seed(time.Now().UnixNano()) - } - return nil -} diff --git a/app/main.go b/app/main.go index c151154..8949908 100644 --- a/app/main.go +++ b/app/main.go @@ -2,7 +2,6 @@ package main import ( "dankmuzikk/cmd/migrator" - "dankmuzikk/cmd/seeder" "dankmuzikk/cmd/server" "dankmuzikk/log" "embed" @@ -22,8 +21,6 @@ func main() { err = server.StartServer(static) case "migrate", "migration", "theotherthing": err = migrator.Migrate() - case "seed", "seeder", "theotherotherthing": - err = seeder.SeedDb() } if err != nil { log.Fatalln(log.ErrorLevel, err) From 136f10fc3d190c4641b9d34b9d6f4130295ec527 Mon Sep 17 00:00:00 2001 From: Baraa Al-Masri Date: Sun, 30 Jun 2024 23:19:38 +0300 Subject: [PATCH 04/13] feat(playlist): add download playlist endpoint --- app/handlers/apis/playlist.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/app/handlers/apis/playlist.go b/app/handlers/apis/playlist.go index 3354fb5..4ca261d 100644 --- a/app/handlers/apis/playlist.go +++ b/app/handlers/apis/playlist.go @@ -11,6 +11,7 @@ import ( "dankmuzikk/views/components/ui" "dankmuzikk/views/pages" "encoding/json" + "io" "net/http" ) @@ -208,3 +209,27 @@ func (p *playlistApi) HandleGetPlaylistsForPopover(w http.ResponseWriter, r *htt playlist.PlaylistsSelector(songId, playlists, songsInPlaylists). Render(r.Context(), w) } + +func (p *playlistApi) HandleDonwnloadPlaylist(w http.ResponseWriter, r *http.Request) { + profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) + if !profileIdCorrect { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("🤷‍♂️")) + return + } + + playlistId := r.URL.Query().Get("playlist-id") + if playlistId == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + + playlistZip, err := p.service.Download(playlistId, profileId) + if err != nil { + log.Errorln(err) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("🤷‍♂️")) + return + } + _, _ = io.Copy(w, playlistZip) +} From b668f763b103e974f4032c1af079898d9d73e9a3 Mon Sep 17 00:00:00 2001 From: Baraa Al-Masri Date: Sun, 30 Jun 2024 23:21:43 +0300 Subject: [PATCH 05/13] feat(playlist): utilize download playlist;s api --- app/static/js/player.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/app/static/js/player.js b/app/static/js/player.js index bacc9bc..7f472b9 100644 --- a/app/static/js/player.js +++ b/app/static/js/player.js @@ -542,6 +542,31 @@ async function downloadToApp() { throw new Error("not implemented!"); } +/** + * @param {string} plPubId + * @param {plTitle} plTitle + */ +async function downloadPlaylistToDevice(plPubId, plTitle) { + Utils.showLoading(); + await fetch(`/api/playlist/zip?playlist-id=${plPubId}`) + .then(async (res) => { + if (!res.ok) { + throw new Error(await res.text()); + return; + } + return res.blob(); + }) + .then((playlistZip) => { + const a = document.createElement("a"); + a.href = URL.createObjectURL(playlistZip); + a.download = `${plTitle}.zip`; + a.click(); + }) + .finally(() => { + Utils.hideLoading(); + }); +} + function show() { muzikkContainerEl.style.display = "block"; } @@ -1078,6 +1103,7 @@ document window.Player = {}; window.Player.downloadSongToDevice = downloadSongToDevice; +window.Player.downloadPlaylistToDevice = downloadPlaylistToDevice; window.Player.showPlayer = show; window.Player.hidePlayer = hide; window.Player.playSingleSong = playSingleSong; From a36461cebebf2baaf74ec7832685c4dbe2d1c56f Mon Sep 17 00:00:00 2001 From: Baraa Al-Masri Date: Sun, 30 Jun 2024 23:22:37 +0300 Subject: [PATCH 06/13] chore(playlist): map the endpoint lol --- app/cmd/server/server.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/cmd/server/server.go b/app/cmd/server/server.go index 9364c06..7d40808 100644 --- a/app/cmd/server/server.go +++ b/app/cmd/server/server.go @@ -8,6 +8,7 @@ import ( "dankmuzikk/handlers/pages" "dankmuzikk/log" "dankmuzikk/models" + "dankmuzikk/services/archive" "dankmuzikk/services/history" "dankmuzikk/services/jwt" "dankmuzikk/services/login" @@ -44,8 +45,9 @@ func StartServer(staticFS embed.FS) error { historyRepo := db.NewBaseDB[models.History](dbConn) playlistVotersRepo := db.NewBaseDB[models.PlaylistSongVoter](dbConn) + zipService := archive.NewService() downloadService := download.New(songRepo) - playlistsService := playlists.New(playlistRepo, playlistOwnersRepo, playlistSongsRepo) + playlistsService := playlists.New(playlistRepo, playlistOwnersRepo, playlistSongsRepo, zipService) songsService := songs.New(playlistSongsRepo, playlistOwnersRepo, songRepo, playlistRepo, playlistVotersRepo, downloadService) historyService := history.New(historyRepo, songRepo) @@ -112,6 +114,7 @@ func StartServer(staticFS embed.FS) error { apisHandler.HandleFunc("PUT /playlist/public", gHandler.AuthApi(playlistsApi.HandleTogglePublicPlaylist)) apisHandler.HandleFunc("PUT /playlist/join", gHandler.AuthApi(playlistsApi.HandleToggleJoinPlaylist)) apisHandler.HandleFunc("DELETE /playlist", gHandler.AuthApi(playlistsApi.HandleDeletePlaylist)) + apisHandler.HandleFunc("GET /playlist/zip", gHandler.AuthApi(playlistsApi.HandleDonwnloadPlaylist)) apisHandler.HandleFunc("GET /history/{page}", gHandler.AuthApi(historyApi.HandleGetMoreHistoryItems)) applicationHandler := http.NewServeMux() From a5f0773af3a6b45c485853c11bbe8ff6786d82c7 Mon Sep 17 00:00:00 2001 From: Baraa Al-Masri Date: Sun, 30 Jun 2024 23:22:58 +0300 Subject: [PATCH 07/13] chore(playlist): add the actual download button --- app/views/components/playlist/options.templ | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/views/components/playlist/options.templ b/app/views/components/playlist/options.templ index 3ac4453..dd0c7ec 100644 --- a/app/views/components/playlist/options.templ +++ b/app/views/components/playlist/options.templ @@ -66,6 +66,18 @@ templ playlistOptions(playlist entities.Playlist) { Play next } + if perm, ok := ctx.Value("playlist-permission").(models.PlaylistPermissions); ok && (perm & models.JoinerPermission) != 0 {