package main import ( "fmt" "io" "os" "path" "path/filepath" "time" ) // An AlbumCoverCache is used to fetch and cache Album Cover image from www.bedetheque.com type AlbumCoverCache struct { basepath string getter HTTPGetter // time to live of the cache, data which is older than this TTL will be automatically removed TTL time.Duration } // NewAlbumCoverCache is creating a new cache at specified location on the fs func NewAlbumCoverCache(path string, maxRequest uint, window time.Duration) (*AlbumCoverCache, error) { res := &AlbumCoverCache{ basepath: path, getter: NewRateLimitedGetter(maxRequest, window), TTL: 3 * 31 * 24 * time.Hour, // 3 Months } err := os.MkdirAll(filepath.Join(res.basepath, "covers"), 0755) if err != nil { return nil, err } return res, nil } // CoverPath gets the path of the cover in the cache func (c *AlbumCoverCache) coverPath(a *Album) string { return filepath.Join(c.basepath, "covers", fmt.Sprintf("%d%s", a.ID, path.Ext(a.CoverURL))) } type teeReaderCloser struct { r io.Reader cr, cw io.Closer } func (t *teeReaderCloser) Read(p []byte) (int, error) { return t.r.Read(p) } func (t *teeReaderCloser) Close() error { err1 := t.cr.Close() err2 := t.cw.Close() if err1 == nil && err2 == nil { return nil } if err1 == nil { return err2 } if err2 == nil { return err1 } return fmt.Errorf("%s;%s", err1, err2) } // NewTeeReadCloser creates a new ReadCloser that writes to w the byte it reads from r func NewTeeReadCloser(r io.ReadCloser, w io.WriteCloser) io.ReadCloser { return &teeReaderCloser{ r: io.TeeReader(r, w), cr: r, cw: w, } } func (c *AlbumCoverCache) fetch(a *Album) (io.ReadCloser, error) { resp, err := c.getter.Get(a.CoverURL) if err != nil { return nil, err } f, err := os.Create(c.coverPath(a)) if err != nil { resp.Body.Close() return nil, err } return NewTeeReadCloser(resp.Body, f), nil } // GetCover retrieves from the cache or either from www.bedetheque.com the Cover of an album func (c *AlbumCoverCache) GetCover(a *Album) (io.ReadCloser, error) { // we should lock the cache while we are using it info, err := os.Stat(c.coverPath(a)) if err != nil { if os.IsNotExist(err) == false { return nil, err } return c.fetch(a) } // check TTL if info.ModTime().Before(time.Now().Add(-c.TTL)) == true { return c.fetch(a) } f, err := os.Open(c.coverPath(a)) if err != nil { return nil, err } return f, nil }