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/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() 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) +} 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) diff --git a/app/services/archive/zip.go b/app/services/archive/zip.go new file mode 100644 index 0000000..13c5fc5 --- /dev/null +++ b/app/services/archive/zip.go @@ -0,0 +1,89 @@ +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 + } + + 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 +} diff --git a/app/services/playlists/playlists.go b/app/services/playlists/playlists.go index 3156ca2..5c65a96 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,51 @@ 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 + } + + fileNames := make([]string, len(pl.Songs)) + for i, song := range pl.Songs { + ogFile, err := os.Open(fmt.Sprintf("%s/%s.mp3", config.Env().YouTube.MusicDir, song.YtId)) + if err != nil { + return nil, err + } + newShit, err := os.OpenFile( + fmt.Sprintf("%s/%d-%s.mp3", config.Env().YouTube.MusicDir, i+1, song.Title), + os.O_WRONLY|os.O_CREATE, 0644, + ) + io.Copy(newShit, ogFile) + fileNames[i] = newShit.Name() + _ = newShit.Close() + _ = ogFile.Close() + } + + zip, err := p.zipService.CreateZip() + if err != nil { + return nil, err + } + + for _, fileName := range fileNames { + file, err := os.Open(fileName) + if err != nil { + return nil, err + } + err = zip.AddFile(file) + if err != nil { + return nil, err + } + _ = file.Close() + _ = os.Remove(file.Name()) + } + + defer func() { + }() + + return zip.Deflate() +} diff --git a/app/services/playlists/songs/songs.go b/app/services/playlists/songs/songs.go index 34e0107..b52f265 100644 --- a/app/services/playlists/songs/songs.go +++ b/app/services/playlists/songs/songs.go @@ -116,6 +116,11 @@ func (s *Service) ToggleSongInPlaylist(songId, playlistPubId string, ownerId uin if err != nil { return } + err = s.downloadService.DownloadYoutubeSongQueue(songId) + if err != nil { + return + } + return true, s.downloadService.DownloadYoutubeSongQueue(songId) } else { return false, s. diff --git a/app/static/js/player.js b/app/static/js/player.js index bacc9bc..2ec13fb 100644 --- a/app/static/js/player.js +++ b/app/static/js/player.js @@ -542,6 +542,30 @@ 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 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 +1102,7 @@ document window.Player = {}; window.Player.downloadSongToDevice = downloadSongToDevice; +window.Player.downloadPlaylistToDevice = downloadPlaylistToDevice; window.Player.showPlayer = show; window.Player.hidePlayer = hide; window.Player.playSingleSong = playSingleSong; 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 {