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

     1  // Copyright 2022 Daniel Erat.
     2  // All rights reserved.
     3  
     4  package query
     5  
     6  import (
     7  	"context"
     8  	"encoding/json"
     9  	"errors"
    10  	"flag"
    11  	"fmt"
    12  	"net/http"
    13  	"net/url"
    14  	"os"
    15  	"path/filepath"
    16  	"strings"
    17  
    18  	"github.com/derat/nup/cmd/nup/client"
    19  	"github.com/derat/nup/server/db"
    20  	"github.com/google/subcommands"
    21  )
    22  
    23  type Command struct {
    24  	Cfg *client.Config
    25  
    26  	filename string // Song.Filename (i.e. relative to Cfg.MusicDir)
    27  	path     string // Song.Filename as absolute path or relative to CWD
    28  	pretty   bool   // pretty-print JSON objects
    29  	printID  bool   // print Song.SongID instead of JSON objects
    30  	single   bool   // require exactly one song to be matched
    31  }
    32  
    33  func (*Command) Name() string     { return "query" }
    34  func (*Command) Synopsis() string { return "run song queries against the server" }
    35  func (*Command) Usage() string {
    36  	return `query <flags>:
    37  	Query the server and and print JSON-marshaled songs to stdout.
    38  
    39  `
    40  }
    41  
    42  func (cmd *Command) SetFlags(f *flag.FlagSet) {
    43  	f.StringVar(&cmd.filename, "filename", "", "Song filename (relative to music dir) to query for")
    44  	f.StringVar(&cmd.path, "path", "", "Song path (resolved to music dir) to query for")
    45  	f.BoolVar(&cmd.pretty, "pretty", false, "Pretty-print JSON objects")
    46  	f.BoolVar(&cmd.printID, "print-id", false, "Print song IDs instead of full JSON objects")
    47  	f.BoolVar(&cmd.single, "single", false, "Require exactly one song to be matched")
    48  }
    49  
    50  func (cmd *Command) Execute(ctx context.Context, _ *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
    51  	req, err := cmd.makeRequest(ctx)
    52  	if err != nil {
    53  		fmt.Fprintln(os.Stderr, "Failed creating request:", err)
    54  		return subcommands.ExitUsageError
    55  	}
    56  
    57  	resp, err := http.DefaultClient.Do(req)
    58  	if err != nil {
    59  		fmt.Fprintln(os.Stderr, "Request failed:", err)
    60  		return subcommands.ExitFailure
    61  	}
    62  	defer resp.Body.Close()
    63  	if resp.StatusCode != http.StatusOK {
    64  		fmt.Fprintln(os.Stderr, "Got non-OK status:", resp.Status)
    65  		return subcommands.ExitFailure
    66  	}
    67  
    68  	var songs []*db.Song
    69  	if err := json.NewDecoder(resp.Body).Decode(&songs); err != nil {
    70  		fmt.Fprintln(os.Stderr, "Failed decoding songs:", err)
    71  		return subcommands.ExitFailure
    72  	}
    73  	if cmd.single && len(songs) != 1 {
    74  		fmt.Fprintf(os.Stderr, "Got %d songs instead of 1\n", len(songs))
    75  		return subcommands.ExitFailure
    76  	}
    77  
    78  	switch {
    79  	case cmd.printID:
    80  		for _, song := range songs {
    81  			fmt.Println(song.SongID)
    82  		}
    83  	default:
    84  		enc := json.NewEncoder(os.Stdout)
    85  		if cmd.pretty {
    86  			enc.SetIndent("", "  ")
    87  		}
    88  		for i, song := range songs {
    89  			if err := enc.Encode(&song); err != nil {
    90  				fmt.Fprintf(os.Stderr, "Failed encoding song %d: %v\n", i, err)
    91  				return subcommands.ExitFailure
    92  			}
    93  		}
    94  	}
    95  
    96  	return subcommands.ExitSuccess
    97  }
    98  
    99  func (cmd *Command) makeRequest(ctx context.Context) (*http.Request, error) {
   100  	vals := make(url.Values)
   101  	if cmd.path != "" {
   102  		// Use -path to set -filename.
   103  		if cmd.Cfg.MusicDir == "" {
   104  			return nil, errors.New("music dir needed for -path but not specified in config file")
   105  		}
   106  		abs, err := filepath.Abs(cmd.path)
   107  		if err != nil {
   108  			return nil, err
   109  		}
   110  		if !strings.HasPrefix(abs, cmd.Cfg.MusicDir+"/") {
   111  			return nil, fmt.Errorf("path %q not under music dir %q", abs, cmd.Cfg.MusicDir)
   112  		}
   113  		if cmd.filename, err = filepath.Rel(cmd.Cfg.MusicDir, abs); err != nil {
   114  			return nil, err
   115  		}
   116  	}
   117  	if cmd.filename != "" {
   118  		vals.Set("filename", cmd.filename)
   119  	}
   120  	if len(vals) == 0 {
   121  		return nil, errors.New("no query parameters supplied")
   122  	}
   123  
   124  	qurl := cmd.Cfg.GetURL("/query")
   125  	qurl.RawQuery = vals.Encode()
   126  	req, err := http.NewRequestWithContext(ctx, "GET", qurl.String(), nil)
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  	req.SetBasicAuth(cmd.Cfg.Username, cmd.Cfg.Password)
   131  	return req, nil
   132  }