package database import ( "context" "encoding/gob" "errors" "fmt" "log" "os" "strconv" "sync" "time" "git.yetaga.in/alazyreader/library/media" "github.com/irlndts/go-discogs" ) type DiscogsCache struct { authToken string m sync.Mutex cache []media.Record maxCacheAge time.Duration lastRefresh time.Time client discogs.Discogs username string persistence bool persistFile string } type persistence struct { CachedRecordSlice []media.Record LastRefresh time.Time } func NewDiscogsCache(token string, maxCacheAge time.Duration, username string, persist bool, persistFile string) (*DiscogsCache, error) { client, err := discogs.New(&discogs.Options{ UserAgent: "library.yetaga.in personal collection cache", Token: token, }) if err != nil { return nil, err } cache := &DiscogsCache{ authToken: token, client: client, maxCacheAge: maxCacheAge, username: username, persistence: persist, persistFile: persistFile, } if cache.persistence && cache.persistFile != "" { p := &persistence{} f, err := os.Open(cache.persistFile) if errors.Is(err, os.ErrNotExist) { log.Printf("%s not found, skipping file load...", cache.persistFile) return cache, nil } if err != nil { return cache, fmt.Errorf("error opening cache file %s: %w", cache.persistFile, err) } err = gob.NewDecoder(f).Decode(p) if err != nil { return cache, fmt.Errorf("error readhing from cache file %s: %w", cache.persistFile, err) } log.Printf("loaded %d records from %s", len(p.CachedRecordSlice), cache.persistFile) cache.cache = p.CachedRecordSlice cache.lastRefresh = p.LastRefresh if time.Now().After(cache.lastRefresh.Add(cache.maxCacheAge)) { log.Printf("cache expired, running refresh...") go func() { err := cache.FlushCache(context.Background()) if err != nil { log.Printf("error loading discogs content: %v", err) } }() } } return cache, nil } func (c *DiscogsCache) FlushCache(ctx context.Context) error { c.m.Lock() defer c.m.Unlock() records, err := c.fetchRecords(ctx, nil) if err != nil { return err } return c.saveRecordsToCache(ctx, records) } func (c *DiscogsCache) GetAllRecords(ctx context.Context) ([]media.Record, error) { c.m.Lock() defer c.m.Unlock() if time.Now().After(c.lastRefresh.Add(c.maxCacheAge)) { records, err := c.fetchRecords(ctx, nil) if err != nil { return c.cache, err } err = c.saveRecordsToCache(ctx, records) return c.cache, err } return c.cache, nil } func (c *DiscogsCache) saveRecordsToCache(ctx context.Context, records []media.Record) error { c.cache = records c.lastRefresh = time.Now() if c.persistence && c.persistFile != "" { p := persistence{ CachedRecordSlice: c.cache, LastRefresh: c.lastRefresh, } f, err := os.OpenFile(c.persistFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) if err != nil { return fmt.Errorf("error opening cache file %s: %w", c.persistFile, err) } err = gob.NewEncoder(f).Encode(p) if err != nil { return fmt.Errorf("error writing to cache file %s: %w", c.persistFile, err) } log.Printf("wrote %d records to %s", len(c.cache), c.persistFile) } return nil } func (c *DiscogsCache) fetchRecords(ctx context.Context, pagination *discogs.Pagination) ([]media.Record, error) { records := []media.Record{} if pagination == nil { pagination = getPagination(1) } log.Printf("calling discogs API, page %v", pagination.Page) coll, err := c.client.CollectionItemsByFolder(c.username, 0, pagination) if err != nil { return records, fmt.Errorf("error loading collection: %w", err) } log.Printf("length: %v, first item in list: %s", len(coll.Items), coll.Items[0].BasicInformation.Title) for i := range coll.Items { records = append(records, collectionItemToRecord(&coll.Items[i])) } // recurse down the list if coll.Pagination.URLs.Next != "" { coll, err := c.fetchRecords(ctx, getPagination(pagination.Page+1)) if err != nil { return records, err } records = append(records, coll...) } return records, nil } func getPagination(page int) *discogs.Pagination { return &discogs.Pagination{Page: page, Sort: "added", SortOrder: "asc", PerPage: 100} } func collectionItemToRecord(item *discogs.CollectionItemSource) media.Record { artists := []string{} for _, artist := range item.BasicInformation.Artists { artists = append(artists, artist.Name) } year := strconv.Itoa(item.BasicInformation.Year) if year == "0" { year = "" } return media.Record{ ID: item.ID, AlbumName: item.BasicInformation.Title, Artists: artists, Identifier: item.BasicInformation.Labels[0].Catno, Format: item.BasicInformation.Formats[0].Name, Genre: item.BasicInformation.Genres[0], Label: item.BasicInformation.Labels[0].Name, Year: year, CoverURL: item.BasicInformation.CoverImage, DiscogsURL: fmt.Sprintf("https://www.discogs.com/release/%v", item.ID), } }