package core import ( "archive/zip" "context" "fmt" "io" "os" "path/filepath" "github.com/Masterminds/squirrel" "github.com/deluan/navidrome/log" "github.com/deluan/navidrome/model" ) type Archiver interface { ZipAlbum(ctx context.Context, id string, w io.Writer) error ZipArtist(ctx context.Context, id string, w io.Writer) error ZipPlaylist(ctx context.Context, id string, w io.Writer) error } func NewArchiver(ds model.DataStore) Archiver { return &archiver{ds: ds} } type archiver struct { ds model.DataStore } type createHeader func(idx int, mf model.MediaFile) *zip.FileHeader func (a *archiver) ZipAlbum(ctx context.Context, id string, out io.Writer) error { mfs, err := a.ds.MediaFile(ctx).FindByAlbum(id) if err != nil { log.Error(ctx, "Error loading mediafiles from album", "id", id, err) return err } return a.zipTracks(ctx, id, out, mfs, a.createHeader) } func (a *archiver) ZipArtist(ctx context.Context, id string, out io.Writer) error { mfs, err := a.ds.MediaFile(ctx).GetAll(model.QueryOptions{ Sort: "album", Filters: squirrel.Eq{"album_artist_id": id}, }) if err != nil { log.Error(ctx, "Error loading mediafiles from artist", "id", id, err) return err } return a.zipTracks(ctx, id, out, mfs, a.createHeader) } func (a *archiver) ZipPlaylist(ctx context.Context, id string, out io.Writer) error { pls, err := a.ds.Playlist(ctx).Get(id) if err != nil { log.Error(ctx, "Error loading mediafiles from playlist", "id", id, err) return err } return a.zipTracks(ctx, id, out, pls.Tracks, a.createPlaylistHeader) } func (a *archiver) zipTracks(ctx context.Context, id string, out io.Writer, mfs model.MediaFiles, ch createHeader) error { z := zip.NewWriter(out) for idx, mf := range mfs { _ = a.addFileToZip(ctx, z, mf, ch(idx, mf)) } err := z.Close() if err != nil { log.Error(ctx, "Error closing zip file", "id", id, err) } return err } func (a *archiver) createHeader(idx int, mf model.MediaFile) *zip.FileHeader { _, file := filepath.Split(mf.Path) return &zip.FileHeader{ Name: fmt.Sprintf("%s/%s", mf.Album, file), Modified: mf.UpdatedAt, Method: zip.Store, } } func (a *archiver) createPlaylistHeader(idx int, mf model.MediaFile) *zip.FileHeader { _, file := filepath.Split(mf.Path) return &zip.FileHeader{ Name: fmt.Sprintf("%d - %s-%s", idx, mf.AlbumArtist, file), Modified: mf.UpdatedAt, Method: zip.Store, } } func (a *archiver) addFileToZip(ctx context.Context, z *zip.Writer, mf model.MediaFile, zh *zip.FileHeader) error { w, err := z.CreateHeader(zh) if err != nil { log.Error(ctx, "Error creating zip entry", "file", mf.Path, err) return err } f, err := os.Open(mf.Path) defer func() { _ = f.Close() }() if err != nil { log.Error(ctx, "Error opening file for zipping", "file", mf.Path, err) return err } _, err = io.Copy(w, f) if err != nil { log.Error(ctx, "Error zipping file", "file", mf.Path, err) return err } return nil }