120 lines
3.5 KiB
Go
120 lines
3.5 KiB
Go
|
|
package playlists
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"errors"
|
||
|
|
"io"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/navidrome/navidrome/conf"
|
||
|
|
"github.com/navidrome/navidrome/log"
|
||
|
|
"github.com/navidrome/navidrome/model"
|
||
|
|
"github.com/navidrome/navidrome/model/request"
|
||
|
|
"github.com/navidrome/navidrome/utils/ioutils"
|
||
|
|
"golang.org/x/text/unicode/norm"
|
||
|
|
)
|
||
|
|
|
||
|
|
func (s *playlists) ImportFile(ctx context.Context, folder *model.Folder, filename string) (*model.Playlist, error) {
|
||
|
|
pls, err := s.parsePlaylist(ctx, filename, folder)
|
||
|
|
if err != nil {
|
||
|
|
log.Error(ctx, "Error parsing playlist", "path", filepath.Join(folder.AbsolutePath(), filename), err)
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
log.Debug(ctx, "Found playlist", "name", pls.Name, "lastUpdated", pls.UpdatedAt, "path", pls.Path, "numTracks", len(pls.Tracks))
|
||
|
|
err = s.updatePlaylist(ctx, pls)
|
||
|
|
if err != nil {
|
||
|
|
log.Error(ctx, "Error updating playlist", "path", filepath.Join(folder.AbsolutePath(), filename), err)
|
||
|
|
}
|
||
|
|
return pls, err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *playlists) ImportM3U(ctx context.Context, reader io.Reader) (*model.Playlist, error) {
|
||
|
|
owner, _ := request.UserFrom(ctx)
|
||
|
|
pls := &model.Playlist{
|
||
|
|
OwnerID: owner.ID,
|
||
|
|
Public: false,
|
||
|
|
Sync: false,
|
||
|
|
}
|
||
|
|
err := s.parseM3U(ctx, pls, nil, reader)
|
||
|
|
if err != nil {
|
||
|
|
log.Error(ctx, "Error parsing playlist", err)
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
err = s.ds.Playlist(ctx).Put(pls)
|
||
|
|
if err != nil {
|
||
|
|
log.Error(ctx, "Error saving playlist", err)
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
return pls, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *playlists) parsePlaylist(ctx context.Context, playlistFile string, folder *model.Folder) (*model.Playlist, error) {
|
||
|
|
pls, err := s.newSyncedPlaylist(folder.AbsolutePath(), playlistFile)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
file, err := os.Open(pls.Path)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
defer file.Close()
|
||
|
|
|
||
|
|
reader := ioutils.UTF8Reader(file)
|
||
|
|
extension := strings.ToLower(filepath.Ext(playlistFile))
|
||
|
|
switch extension {
|
||
|
|
case ".nsp":
|
||
|
|
err = s.parseNSP(ctx, pls, reader)
|
||
|
|
default:
|
||
|
|
err = s.parseM3U(ctx, pls, folder, reader)
|
||
|
|
}
|
||
|
|
return pls, err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *playlists) updatePlaylist(ctx context.Context, newPls *model.Playlist) error {
|
||
|
|
owner, _ := request.UserFrom(ctx)
|
||
|
|
|
||
|
|
// Try to find existing playlist by path. Since filesystem normalization differs across
|
||
|
|
// platforms (macOS uses NFD, Linux/Windows use NFC), we try both forms to match
|
||
|
|
// playlists that may have been imported on a different platform.
|
||
|
|
pls, err := s.ds.Playlist(ctx).FindByPath(newPls.Path)
|
||
|
|
if errors.Is(err, model.ErrNotFound) {
|
||
|
|
// Try alternate normalization form
|
||
|
|
altPath := norm.NFD.String(newPls.Path)
|
||
|
|
if altPath == newPls.Path {
|
||
|
|
altPath = norm.NFC.String(newPls.Path)
|
||
|
|
}
|
||
|
|
if altPath != newPls.Path {
|
||
|
|
pls, err = s.ds.Playlist(ctx).FindByPath(altPath)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if err != nil && !errors.Is(err, model.ErrNotFound) {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if err == nil && !pls.Sync {
|
||
|
|
log.Debug(ctx, "Playlist already imported and not synced", "playlist", pls.Name, "path", pls.Path)
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
if err == nil {
|
||
|
|
log.Info(ctx, "Updating synced playlist", "playlist", pls.Name, "path", newPls.Path)
|
||
|
|
newPls.ID = pls.ID
|
||
|
|
newPls.Name = pls.Name
|
||
|
|
newPls.Comment = pls.Comment
|
||
|
|
newPls.OwnerID = pls.OwnerID
|
||
|
|
newPls.Public = pls.Public
|
||
|
|
newPls.EvaluatedAt = &time.Time{}
|
||
|
|
} else {
|
||
|
|
log.Info(ctx, "Adding synced playlist", "playlist", newPls.Name, "path", newPls.Path, "owner", owner.UserName)
|
||
|
|
newPls.OwnerID = owner.ID
|
||
|
|
// For NSP files, Public may already be set from the file; for M3U, use server default
|
||
|
|
if !newPls.IsSmartPlaylist() {
|
||
|
|
newPls.Public = conf.Server.DefaultPlaylistPublicVisibility
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return s.ds.Playlist(ctx).Put(newPls)
|
||
|
|
}
|