diff --git a/.gitignore b/.gitignore index 54e7cfe..f4d9dba 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ personal_access_token +test.csv +collection.gob diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..4f19000 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "go-discogs"] + path = go-discogs + url = https://github.com/irlndts/go-discogs.git diff --git a/go.mod b/go.mod index 838794e..3f632d5 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module git.hrfee.pw/hrfee/discogs-pricer go 1.22.4 -require github.com/irlndts/go-discogs v0.3.6 // indirect +require github.com/hrfee/go-discogs v0.0.0-20240905170225-50ee0e6a98ae // indirect diff --git a/go.sum b/go.sum index 3516c17..ec9e25b 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ -github.com/irlndts/go-discogs v0.3.6 h1:3oIJEkLGQ1ffJcoo6wvtawPI4/SyHoRpnu25Y51U4wg= -github.com/irlndts/go-discogs v0.3.6/go.mod h1:UVQ05FdCzH4P/usnSxQDh77QYE37HvmPnSCgogioljo= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/hrfee/go-discogs v0.0.0-20240905170225-50ee0e6a98ae h1:9uX6ArLOVUzryMkT3VKud4c/5vNwWEwdR6vJHHvh2tw= +github.com/hrfee/go-discogs v0.0.0-20240905170225-50ee0e6a98ae/go.mod h1:Z34PS6moBg1QdybW9LkrqciRJHKdyQ9hpxvaiH8sFic= diff --git a/main.go b/main.go index 691300c..6cb5f8c 100644 --- a/main.go +++ b/main.go @@ -1,43 +1,290 @@ package main import ( + "encoding/csv" + "encoding/gob" + "encoding/json" "errors" + "flag" "fmt" "os" + "strconv" "strings" + "time" - "github.com/irlndts/go-discogs" + "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 ServiceWriter interface { - WriteRow(svcID string, name string, price string, purchaseDate time.Time, notes string) -}*/ +type ItemHash string type Client struct { - c discogs.Discogs + 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, token string) (*Client, error) { - client := Client{} +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 } -func main() { - token, err := os.ReadFile("personal_access_token") +// 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 { - panic(errors.New("no token found in ./personal_access_token")) + return []discogs.CollectionItemSource{}, err, false } - c, err := NewClient(CURRENCY, USER_AGENT, strings.TrimSuffix(string(token), "\n")) - fmt.Println("vim-go") + 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() }