package main import ( "encoding/csv" "encoding/gob" "encoding/json" "errors" "flag" "fmt" "os" "strconv" "strings" "time" "github.com/hrfee/go-discogs" ) var ( CURRENCY = "GBP" USER_AGENT = "discogs-pricer/0.0 +https://git.hrfee.pw/hrfee/discogs-pricer" STORE = "./collection.gob" PER_PAGE = 75 NotesFieldID = 4 // Test, otherwise use 3 noPaToken = errors.New(`put "hrfee:" in ./personal_access_token and re-run.`) ) type ItemHash string type Client struct { Username string c discogs.Discogs StorePath string CollectionCache struct { Items map[ItemHash]CollectionItem // Hash of a load of IDs to Collection Item Order []ItemHash } } func NewClient(currency string, userAgent string, username, token string, storePath string) (*Client, error) { client := Client{Username: username, StorePath: storePath} var err error client.c, err = discogs.New(&discogs.Options{ UserAgent: userAgent, Currency: currency, Token: token, // URL: "http://localhost:8097", }) if err == nil { client.ClearCollectionCache() err = client.ReadCollectionCache() } return &client, err } // Items, error, and whether or not there's more pages to go. func (c *Client) collection(pageNo int, allocate bool) ([]discogs.CollectionItemSource, error, bool) { fmt.Printf("Getting collection page %d for user %s\n", pageNo, c.Username) cf, err := c.c.CollectionItemsByFolder(c.Username, 0, &discogs.Pagination{ Sort: "added", SortOrder: "asc", Page: pageNo, PerPage: PER_PAGE, }) if err != nil { return []discogs.CollectionItemSource{}, err, false } if allocate { c.CollectionCache.Order = make([]ItemHash, 0, cf.Pagination.Pages*PER_PAGE) } return cf.Items, err, cf.Pagination.Page < cf.Pagination.Pages } func (c *Client) ClearCollectionCache() { c.CollectionCache.Order = []ItemHash{} c.CollectionCache.Items = map[ItemHash]CollectionItem{} } func (c *Client) PopulateCollectionCache() error { more := true var err error var p []discogs.CollectionItemSource i := 0 for more { p, err, more = c.collection(i, i == 0) if err != nil { return err } for i := range p { ci := CollectionItem{p[i]} hash := ci.Hash() if _, ok := c.CollectionCache.Items[hash]; ok { continue } c.CollectionCache.Order = append(c.CollectionCache.Order, hash) c.CollectionCache.Items[hash] = ci } i++ } return c.StoreCollectionCache() } func (c *Client) StoreCollectionCache() error { file, err := os.Create(c.StorePath) if err != nil { return err } defer file.Close() enc := gob.NewEncoder(file) return enc.Encode(c.CollectionCache) } func (c *Client) ReadCollectionCache() error { file, err := os.Open(c.StorePath) if err != nil { if errors.Is(err, os.ErrNotExist) { return nil } return err } defer file.Close() dec := gob.NewDecoder(file) return dec.Decode(&c.CollectionCache) } func (c *Client) CollectionJSON() (string, error) { if c.CollectionCache.Order == nil || len(c.CollectionCache.Order) == 0 { if err := c.PopulateCollectionCache(); err != nil { return "", err } if err := c.StoreCollectionCache(); err != nil { return "", err } } v, _ := json.MarshalIndent(c.CollectionCache.Items, "", "\t") return string(v), nil } func (c *Client) WriteCSVTemplate(path string) error { if c.CollectionCache.Order == nil || len(c.CollectionCache.Order) == 0 { if err := c.PopulateCollectionCache(); err != nil { return err } if err := c.StoreCollectionCache(); err != nil { return err } } file, err := os.Create(path) if err != nil { return err } defer file.Close() w := csv.NewWriter(file) w.Write(HeaderRow) for _, ciHash := range c.CollectionCache.Order { w.Write(c.CollectionCache.Items[ciHash].RowFromCollectionItem()) } return nil } func (c *Client) TestEditField() { err := c.c.EditFieldsInstance(c.Username, 1, 10191384, 313879623, discogs.NotesField, "a new 2nd value!") fmt.Printf("Error: %v\n", err) } var HeaderRow = strings.Split( "Folder ID,Release ID,Instance ID,Artist - Name,Date Added,Price,Purchased Date,Additional Notes", ",", ) type CollectionItem struct { discogs.CollectionItemSource } // Not a real hash, but identifies a record uniquely enough. func (ci CollectionItem) Hash() ItemHash { return ItemHash(strconv.Itoa(ci.ID) + "|" + strconv.Itoa(ci.InstanceID)) } func ParseTime(t string) (time.Time, error) { return time.Parse("2006-01-02T15:04:05-07:00", t) } func (ci CollectionItem) RawNotes() string { notes := "" for _, n := range ci.Notes { if n.FieldID == discogs.NotesField { notes = n.Value } } return notes } type ParsedNotes struct { Price string PurchaseDate time.Time Additional string } func (ci CollectionItem) ParseNotes() ParsedNotes { notes := ci.RawNotes() pn := ParsedNotes{} dateString := "" n, err := fmt.Sscanf(notes, "Purchase Price: %s, Purchase Date: %s.%s", &(pn.Price), &dateString, &(pn.Additional)) if n <= 0 || err != nil { pn.Additional = notes } pn.PurchaseDate, err = time.Parse("2006-01-02", dateString) if err != nil || dateString == "" || pn.PurchaseDate.IsZero() { pn.PurchaseDate, _ = ParseTime(ci.DateAdded) } return pn } func (ci CollectionItem) RowFromCollectionItem() []string { dateAdded, err := ParseTime(ci.DateAdded) if err != nil { panic(err) } notes := ci.ParseNotes() row := []string{ strconv.Itoa(ci.FolderID), strconv.Itoa(ci.ID), strconv.Itoa(ci.InstanceID), ci.BasicInformation.Artists[0].Name + " - " + ci.BasicInformation.Title, dateAdded.Format("2006-01-02"), notes.Price, notes.PurchaseDate.Format("2006-01-02"), notes.Additional, } return row } func main() { patContent, err := os.ReadFile("personal_access_token") if err != nil { panic(noPaToken) } userAndToken := strings.Split(string(patContent), ":") if len(userAndToken) != 2 { panic(noPaToken) } c, err := NewClient(CURRENCY, USER_AGENT, userAndToken[0], strings.TrimSuffix(string(userAndToken[1]), "\n"), STORE) if err != nil { panic(err) } reloadCollection := false printCollection := false csvTemplatePath := "" flag.BoolVar(&reloadCollection, "reload", false, "pass to force reload of collection cache.") flag.BoolVar(&printCollection, "print", false, "pass to print JSON representation of collection.") flag.StringVar(&csvTemplatePath, "template", "", "filepath to write template csv to.") flag.Parse() args := false flag.Visit(func(*flag.Flag) { args = true }) if !args { flag.Usage() } if reloadCollection { c.ClearCollectionCache() if err := c.PopulateCollectionCache(); err != nil { panic(err) } } if printCollection { v, err := c.CollectionJSON() if err != nil { panic(err) } fmt.Println(v) } if csvTemplatePath != "" { if err := c.WriteCSVTemplate(csvTemplatePath); err != nil { panic(err) } } // c.TestEditField() }