github.com/derat/nup@v0.0.0-20230418113745-15592ba7c620/cmd/nup/metadata/command_test.go (about)

     1  // Copyright 2023 Daniel Erat.
     2  // All rights reserved.
     3  
     4  package metadata
     5  
     6  import (
     7  	"context"
     8  	"encoding/json"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"path/filepath"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/derat/nup/cmd/nup/client"
    16  	"github.com/derat/nup/cmd/nup/client/files"
    17  	"github.com/derat/nup/server/db"
    18  	"github.com/derat/nup/test"
    19  	"github.com/google/go-cmp/cmp"
    20  	"golang.org/x/time/rate"
    21  )
    22  
    23  type testEnv struct {
    24  	t   *testing.T
    25  	mux *http.ServeMux
    26  	srv *httptest.Server
    27  
    28  	cfg *client.Config
    29  	api *api
    30  
    31  	recordings map[string]recording
    32  	releases   map[string]release
    33  }
    34  
    35  func newTestEnv(t *testing.T) *testEnv {
    36  	dir := t.TempDir()
    37  	env := testEnv{
    38  		t:   t,
    39  		mux: http.NewServeMux(),
    40  		cfg: &client.Config{
    41  			MusicDir:    filepath.Join(dir, "music"),
    42  			MetadataDir: filepath.Join(dir, "metadata"),
    43  		},
    44  		recordings: make(map[string]recording),
    45  		releases:   make(map[string]release),
    46  	}
    47  	env.mux.HandleFunc(recPathPrefix, env.handleRecording)
    48  	env.mux.HandleFunc(relPathPrefix, env.handleRelease)
    49  	env.srv = httptest.NewServer(env.mux)
    50  	env.api = newAPI(env.srv.URL)
    51  	env.api.limiter.SetLimit(rate.Inf)
    52  	return &env
    53  }
    54  
    55  func (env *testEnv) close() {
    56  	env.srv.Close()
    57  }
    58  
    59  func (env *testEnv) handleRecording(w http.ResponseWriter, req *http.Request) {
    60  	if rec, ok := env.recordings[req.URL.Path[len(recPathPrefix):]]; !ok {
    61  		http.NotFound(w, req)
    62  	} else {
    63  		env.writeJSON(w, rec)
    64  	}
    65  }
    66  
    67  func (env *testEnv) handleRelease(w http.ResponseWriter, req *http.Request) {
    68  	if rel, ok := env.releases[req.URL.Path[len(relPathPrefix):]]; !ok {
    69  		http.NotFound(w, req)
    70  	} else {
    71  		env.writeJSON(w, rel)
    72  	}
    73  }
    74  
    75  func (env *testEnv) writeJSON(w http.ResponseWriter, obj interface{}) {
    76  	w.Header().Set("Content-Type", "application/json")
    77  	if err := json.NewEncoder(w).Encode(obj); err != nil {
    78  		env.t.Error("Failed writing response:", err)
    79  	}
    80  }
    81  
    82  const (
    83  	recPathPrefix = "/ws/2/recording/"
    84  	relPathPrefix = "/ws/2/release/"
    85  )
    86  
    87  func TestScanSong_Release(t *testing.T) {
    88  	env := newTestEnv(t)
    89  	defer env.close()
    90  
    91  	song := test.Song0s
    92  	test.Must(t, test.CopySongs(env.cfg.MusicDir, song.Filename))
    93  
    94  	want := song
    95  	want.Title = "New Title"
    96  	want.Artist = "Artist A feat. B & C"
    97  	want.Album = "New Album"
    98  	want.AlbumArtist = "Artist A"
    99  	want.DiscSubtitle = "The Third Disc"
   100  	want.AlbumID = "5109de80-7946-41ea-9060-05d10da87219"
   101  	want.RecordingID = "4b834e6e-a694-4cbe-a715-f5b0e36c57e2"
   102  	want.OrigAlbumID = song.AlbumID
   103  	want.OrigRecordingID = song.RecordingID
   104  	want.Track = 4
   105  	want.Disc = 3
   106  	want.Date = time.Date(2020, 6, 7, 0, 0, 0, 0, time.UTC)
   107  
   108  	want.SHA1 = ""
   109  	want.Length = 0
   110  	want.TrackGain = 0
   111  	want.AlbumGain = 0
   112  	want.PeakAmp = 0
   113  
   114  	env.releases[song.AlbumID] = release{
   115  		Title:   want.Album,
   116  		Artists: []artistCredit{{Name: "Artist A"}},
   117  		ID:      want.AlbumID,
   118  		Media: []medium{
   119  			{Position: 1},
   120  			{Position: 2},
   121  			{
   122  				Title:    want.DiscSubtitle,
   123  				Position: 3,
   124  				Tracks: []track{
   125  					{Position: 1},
   126  					{Position: 2},
   127  					{Position: 3},
   128  					{
   129  						Title: want.Title,
   130  						Artists: []artistCredit{
   131  							{Name: "Artist A", JoinPhrase: " feat. "},
   132  							{Name: "B", JoinPhrase: " & "},
   133  							{Name: "C"},
   134  						},
   135  						Recording: recording{ID: want.RecordingID},
   136  						Position:  4,
   137  					},
   138  				},
   139  			},
   140  		},
   141  		ReleaseGroup: releaseGroup{FirstReleaseDate: date(want.Date)},
   142  	}
   143  	env.recordings[song.RecordingID] = recording{ID: want.RecordingID}
   144  
   145  	ctx := context.Background()
   146  	p := filepath.Join(env.cfg.MusicDir, song.Filename)
   147  	test.Must(t, scanSong(ctx, env.cfg, env.api, p, nil, nil))
   148  	if got, err := files.ReadSong(env.cfg, p, nil /* fi */, files.SkipAudioData, nil /* gc */); err != nil {
   149  		t.Error("ReadSong failed:", err)
   150  	} else if diff := cmp.Diff(want, *got); diff != "" {
   151  		t.Error("ReadSong returned unexpected results:\n" + diff)
   152  	}
   153  }
   154  
   155  // TODO: Add a test for a standalone recording (non-album track).
   156  // I think that this will be hard to do unless I add a song file
   157  // with a recording ID but no album ID.
   158  
   159  func TestSetAlbum(t *testing.T) {
   160  	// Helper functions to make it easier to create objects.
   161  	ms := func(title, rec string, sec int) *db.Song {
   162  		return &db.Song{
   163  			Title:       title,
   164  			RecordingID: rec,
   165  			Length:      float64(sec),
   166  		}
   167  	}
   168  	mt := func(title, rec string, sec int) track {
   169  		msec := int64(sec) * 1000
   170  		return track{
   171  			Title:     title,
   172  			Length:    msec,
   173  			Recording: recording{Title: title, ID: rec, Length: msec},
   174  		}
   175  	}
   176  	mr := func(id string, tls ...[]track) *release {
   177  		rel := release{ID: id}
   178  		for _, tl := range tls {
   179  			rel.Media = append(rel.Media, medium{Tracks: tl})
   180  		}
   181  		return &rel
   182  	}
   183  
   184  	for _, tc := range []struct {
   185  		desc   string
   186  		songs  []*db.Song
   187  		rel    *release
   188  		recIDs []string
   189  		want   []*db.Song // nil for error
   190  	}{
   191  		{
   192  			"direct mapping",
   193  			[]*db.Song{ms("a", "1", 60), ms("b", "2", 40), ms("c", "3", 120)},
   194  			mr("album", []track{mt("a0", "1", 60), mt("b0", "2", 40), mt("c0", "3", 120)}),
   195  			nil,
   196  			[]*db.Song{ms("a0", "1", 60), ms("b0", "2", 40), ms("c0", "3", 120)},
   197  		},
   198  		{
   199  			"reordered",
   200  			[]*db.Song{ms("a", "1", 60), ms("b", "2", 40), ms("c", "3", 120)},
   201  			mr("album", []track{mt("c0", "3", 120), mt("a0", "1", 60), mt("b0", "2", 40)}),
   202  			nil,
   203  			[]*db.Song{ms("a0", "1", 60), ms("b0", "2", 40), ms("c0", "3", 120)},
   204  		},
   205  		{
   206  			"new recordings",
   207  			[]*db.Song{ms("a", "1", 60), ms("b", "2", 40), ms("c", "3", 120), ms("d", "4", 50)},
   208  			mr("album", []track{mt("a0", "5", 61), mt("b0", "6", 43)}, []track{mt("c0", "7", 122), mt("d0", "8", 50)}),
   209  			nil,
   210  			[]*db.Song{ms("a0", "5", 60), ms("b0", "6", 40), ms("c0", "7", 120), ms("d0", "8", 50)},
   211  		},
   212  		{
   213  			"override recording IDs",
   214  			[]*db.Song{ms("a", "1", 60), ms("b", "2", 40)},
   215  			mr("album", []track{mt("a0", "3", 60), mt("c0", "4", 30), mt("b0", "5", 40)}),
   216  			[]string{"3", "5"},
   217  			[]*db.Song{ms("a0", "3", 60), ms("b0", "5", 40)},
   218  		},
   219  		{
   220  			"different lengths",
   221  			[]*db.Song{ms("a", "1", 60), ms("b", "2", 40)},
   222  			mr("album", []track{mt("a0", "3", 40), mt("b0", "2", 40)}),
   223  			nil,
   224  			nil, // should fail
   225  		},
   226  		{
   227  			"different track counts",
   228  			[]*db.Song{ms("a", "1", 60), ms("b", "2", 40)},
   229  			mr("album", []track{mt("a0", "1", 60)}),
   230  			nil,
   231  			nil, // should fail
   232  		},
   233  		{
   234  			"duplicate recordings",
   235  			[]*db.Song{ms("a", "1", 60), ms("b", "1", 60)},
   236  			mr("album", []track{mt("a0", "1", 60), mt("b0", "2", 40)}),
   237  			nil,
   238  			nil, // should fail
   239  		},
   240  	} {
   241  		t.Run(tc.desc, func(t *testing.T) {
   242  			got, err := setAlbum(tc.songs, tc.rel, tc.recIDs)
   243  			if err != nil {
   244  				if tc.want != nil {
   245  					t.Error("setAlbum failed:", err)
   246  				}
   247  				return
   248  			} else if tc.want == nil {
   249  				t.Errorf("setAlbum unexpectedly succeeded with %v", got)
   250  				return
   251  			}
   252  			for _, s := range tc.want {
   253  				s.AlbumID = tc.rel.ID // set for diff
   254  			}
   255  			if diff := cmp.Diff(got, tc.want); diff != "" {
   256  				t.Error("setAlbum returned wrong results:\n" + diff)
   257  			}
   258  		})
   259  
   260  	}
   261  }