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 {