diff --git a/.gitignore b/.gitignore index f4d9dba..235fd2a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ personal_access_token test.csv +*.csv +*.ods collection.gob diff --git a/main.go b/main.go index 6cb5f8c..d482a76 100644 --- a/main.go +++ b/main.go @@ -7,10 +7,12 @@ import ( "errors" "flag" "fmt" + "io" "os" "strconv" "strings" "time" + "unicode" "github.com/hrfee/go-discogs" ) @@ -25,7 +27,7 @@ var ( NotesFieldID = 4 // Test, otherwise use 3 - noPaToken = errors.New(`put "hrfee:" in ./personal_access_token and re-run.`) + noPaToken = errors.New(`put ":" in ./personal_access_token and re-run.`) ) type ItemHash string @@ -140,7 +142,7 @@ func (c *Client) CollectionJSON() (string, error) { return string(v), nil } -func (c *Client) WriteCSVTemplate(path string) error { +func (c *Client) WriteCSVTemplate(path string, currency string) error { if c.CollectionCache.Order == nil || len(c.CollectionCache.Order) == 0 { if err := c.PopulateCollectionCache(); err != nil { return err @@ -155,20 +157,123 @@ func (c *Client) WriteCSVTemplate(path string) error { } defer file.Close() w := csv.NewWriter(file) + defer w.Flush() w.Write(HeaderRow) for _, ciHash := range c.CollectionCache.Order { - w.Write(c.CollectionCache.Items[ciHash].RowFromCollectionItem()) + l := c.CollectionCache.Items[ciHash].RowFromCollectionItem(currency) + err := w.Write(l) + if err != nil { + err = fmt.Errorf("failed writing line \"%s\": %v", l, err) + return err + } } return nil } +func (c *Client) ReadFromFilledCSV(path string, currency string, skipFirstNRows int) (int, error) { + file, err := os.Open(path) + if err != nil { + return 0, err + } + defer file.Close() + r := csv.NewReader(file) + i := 0 + for err == nil { + var l []string + l, err = r.Read() + if err != nil { + break + } + + if l[0] == "Folder ID" { + continue + } + + if i < skipFirstNRows { + i++ + fmt.Printf("skipped row %d = \"%s\"\n", i, l[cArtistName]) + continue + } + + pp := parsePrice(l[cPrice], currency) + pps := parsePrice(l[cShipping], currency) + + pn := ParsedNotes{ + Price: pp, + ShippingPrice: pps, + Seller: l[cSeller], + Additional: l[cAdditionalNotes], + } + pn.PurchaseDate, err = time.Parse("2006-01-02", l[cPurchaseDate]) + if err != nil { + return i, err + } + + /*folderID, _ := strconv.Atoi(l[cFolderID]) + releaseID, _ := strconv.Atoi(l[cReleaseID]) + instanceID, _ := strconv.Atoi(l[cInstanceID])*/ + + pnString := pn.ToString(currency) + + // fmt.Printf("For \"%s\": %s\n", l[cArtistName], pnString) + regenerated := parseNotes(pnString, "2006-01-02T15:04:05-07:00", currency) + pn.RoughPanickingEqual(regenerated) + + folderID, err := strconv.Atoi(l[cFolderID]) + if err != nil { + panic(err) + } + releaseID, err := strconv.Atoi(l[cReleaseID]) + if err != nil { + panic(err) + } + instanceID, err := strconv.Atoi(l[cInstanceID]) + if err != nil { + panic(err) + } + + err = c.c.EditFieldsInstance( + c.Username, + folderID, + releaseID, + instanceID, + discogs.NotesField, + pnString, + ) + if err != nil { + panic(fmt.Errorf("failed on %d-%d-%d \"%s\": %v", folderID, releaseID, instanceID, l[cArtistName], err)) + } + + fmt.Printf("logged change for %d = \"%s\"\n", i, l[cArtistName]) + i++ + } + if errors.Is(err, io.EOF) { + return i, nil + } + return i, err +} + 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) } +const ( + cFolderID = iota + cReleaseID + cInstanceID + cArtistName + cFormat + cDateAdded + cPrice + cShipping + cPurchaseDate + cSeller + cAdditionalNotes +) + var HeaderRow = strings.Split( - "Folder ID,Release ID,Instance ID,Artist - Name,Date Added,Price,Purchased Date,Additional Notes", + "Folder ID,Release ID,Instance ID,Artist - Name,Format,Date Added,Price,Shipping (p/ Item),Purchased Date,Seller,Additional Notes", ",", ) @@ -196,40 +301,156 @@ func (ci CollectionItem) RawNotes() string { } type ParsedNotes struct { - Price string - PurchaseDate time.Time - Additional string + Price ParsedPrice + ShippingPrice ParsedPrice + PurchaseDate time.Time + Seller string + Additional string } -func (ci CollectionItem) ParseNotes() ParsedNotes { +func (p1 ParsedNotes) RoughPanickingEqual(p2 ParsedNotes) { + if p1.Price.String() != p2.Price.String() { + panic(fmt.Errorf("p1.Price != p2.Price: %v != %v", p1.Price, p2.Price)) + } + if p1.ShippingPrice.String() != p2.ShippingPrice.String() { + panic(fmt.Errorf("p1.ShippingPrice != p2.ShippingPrice: %v != %v", p1.ShippingPrice, p2.ShippingPrice)) + } + // Don't care about date + if p1.Seller != p2.Seller { + panic(fmt.Errorf("p1.Seller != p2.Seller: \"%v\" != \"%v\"", p1.Seller, p2.Seller)) + } + if p1.Additional != p2.Additional { + panic(fmt.Errorf("p1.Additional != p2.Additional: \"%v\" != \"%v\"", p1.Additional, p2.Additional)) + } +} + +type ParsedPrice struct { + Currency string + Major, Minor int +} + +func (pp ParsedPrice) String() string { + return fmt.Sprintf("%s%d.%02d", pp.Currency, pp.Major, pp.Minor) +} + +func (pp *ParsedPrice) FromFloat(p float64) { + pp.Major = int(p) + pp.Minor = int((float64(p) - float64(int(p))) * float64(100)) +} + +func (pp ParsedPrice) IsZero() bool { return pp.Major == 0 && pp.Minor == 0 } + +func parsePrice(p string, currency string) ParsedPrice { + parts := strings.SplitN(p, ".", 2) + major := parts[0] + existingCurrency := "" + for i := 0; i < len(major); i++ { + if !unicode.IsDigit(rune(major[i])) { + existingCurrency += string(rune(major[i])) + } else { + major = major[i:] + break + } + } + iMajor, _ := strconv.Atoi(major) + iMinor := 0 + if len(parts) > 1 { + parts[1] = strings.ReplaceAll(parts[1], " ", "") + var err error + iMinor, err = strconv.Atoi(parts[1]) + if err != nil { + panic(err) + } + } + pp := ParsedPrice{ + Currency: existingCurrency, + Major: iMajor, + Minor: iMinor, + } + if currency != "" { + pp.Currency = currency + } + return pp +} + +func (pn ParsedNotes) ToString(currency string) string { + price := pn.Price.String() + if !pn.ShippingPrice.IsZero() { + price += " (+" + pn.ShippingPrice.String() + ")" + } + return fmt.Sprintf("Price: %s, Purchase Date: %s, From: %s. %s", price, pn.PurchaseDate.Format("2006-01-02"), pn.Seller, pn.Additional) +} + +func (ci CollectionItem) ParseNotes(currency string) ParsedNotes { notes := ci.RawNotes() + return parseNotes(notes, ci.DateAdded, currency) +} + +func parseNotes(notes string, dateAdded string, currency string) ParsedNotes { pn := ParsedNotes{} dateString := "" - n, err := fmt.Sscanf(notes, "Purchase Price: %s, Purchase Date: %s.%s", &(pn.Price), &dateString, &(pn.Additional)) + priceString := "" + if strings.HasPrefix(notes, "Price:") { + noPrice := strings.TrimPrefix(notes, "Price: ") + priceString = strings.Split(notes, ", Purchase Date")[0] + notes = "P" + strings.SplitN(noPrice, ", P", 2)[1] + } + // fmt.Printf("Parsing \"%s\"\n", notes) + n, err := fmt.Sscanf(notes, "Purchase Date: %s, From: %s. %s", &dateString, &(pn.Seller), &(pn.Additional)) if n <= 0 || err != nil { pn.Additional = notes } + + end := strings.Split(notes, "From: ") + if len(end) > 1 { + splitInd := strings.Index(end[1], ". ") + if splitInd != -1 { + pn.Seller = end[1][:splitInd] + pn.Additional = end[1][splitInd+len(". "):] + } + } + + priceSplit := strings.Split(priceString, "(+") + // fmt.Printf("ps \"%s\" split to %+v\n", priceString, priceSplit) + // fmt.Printf("got dateString \"%s\"\n", dateString) + pn.Price = parsePrice(priceSplit[0], currency) + pn.ShippingPrice = ParsedPrice{Currency: currency} + if len(priceSplit) > 1 { + pn.ShippingPrice = parsePrice(strings.ReplaceAll(priceSplit[1], ")", ""), currency) + } + + dateString = strings.TrimSuffix(dateString, ",") pn.PurchaseDate, err = time.Parse("2006-01-02", dateString) if err != nil || dateString == "" || pn.PurchaseDate.IsZero() { - pn.PurchaseDate, _ = ParseTime(ci.DateAdded) + if dateString != "" { + fmt.Printf("date error: %v\n", err) + } + pn.PurchaseDate, _ = ParseTime(dateAdded) } return pn } -func (ci CollectionItem) RowFromCollectionItem() []string { +func (ci CollectionItem) RowFromCollectionItem(currency string) []string { dateAdded, err := ParseTime(ci.DateAdded) if err != nil { panic(err) } - notes := ci.ParseNotes() + format := "?" + if len(ci.BasicInformation.Formats) != 0 { + format = ci.BasicInformation.Formats[0].Name + } + notes := ci.ParseNotes(currency) row := []string{ strconv.Itoa(ci.FolderID), strconv.Itoa(ci.ID), strconv.Itoa(ci.InstanceID), ci.BasicInformation.Artists[0].Name + " - " + ci.BasicInformation.Title, + format, dateAdded.Format("2006-01-02"), - notes.Price, + notes.Price.String(), + notes.ShippingPrice.String(), notes.PurchaseDate.Format("2006-01-02"), + notes.Seller, notes.Additional, } return row @@ -253,9 +474,16 @@ func main() { reloadCollection := false printCollection := false csvTemplatePath := "" + csvApplyFromPath := "" + skipRowsOnApplyFrom := 0 + currency := "£" + 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.StringVar(&csvApplyFromPath, "apply", "", "filepath to csv to apply notes from.") + flag.IntVar(&skipRowsOnApplyFrom, "skip", 0, "number of rows to skip applying from, useful if previous attempt failed.") + flag.StringVar(¤cy, "currency", "£", "currency symbol to fill in.") flag.Parse() @@ -281,10 +509,17 @@ func main() { } if csvTemplatePath != "" { - if err := c.WriteCSVTemplate(csvTemplatePath); err != nil { + if err := c.WriteCSVTemplate(csvTemplatePath, currency); err != nil { panic(err) } } + if csvApplyFromPath != "" { + n, err := c.ReadFromFilledCSV(csvApplyFromPath, currency, skipRowsOnApplyFrom) + if err != nil { + panic(fmt.Errorf("failed at %d: %v", n, err)) + } + } + // c.TestEditField() }