discogs-pricer/main.go

291 lines
6.8 KiB
Go
Raw Permalink Normal View History

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:<personal access token>" 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()
}