discogs basic backend

This commit is contained in:
David 2022-03-13 18:04:09 -04:00
parent 98584bbef6
commit 84803f1e3d
8 changed files with 214 additions and 43 deletions

View File

@ -7,6 +7,7 @@ import (
"log" "log"
"net/http" "net/http"
"strings" "strings"
"time"
"git.yetaga.in/alazyreader/library/config" "git.yetaga.in/alazyreader/library/config"
"git.yetaga.in/alazyreader/library/database" "git.yetaga.in/alazyreader/library/database"
@ -22,24 +23,44 @@ func max(a, b int) int {
return b return b
} }
func obscureStr(in string, l int) string {
return in[0:max(l, len(in))] + strings.Repeat("*", max(0, len(in)-l))
}
type Library interface { type Library interface {
GetAllBooks(context.Context) ([]media.Book, error) GetAllBooks(context.Context) ([]media.Book, error)
} }
type RecordCollection interface {
GetAllRecords(context.Context) ([]media.Record, error)
}
type Router struct { type Router struct {
static fs.FS static fs.FS
lib Library lib Library
rcol RecordCollection
}
func writeJSON(w http.ResponseWriter, b []byte, status int) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
w.Write(b)
w.Write([]byte("\n"))
} }
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if req.URL.Path == "/api" { if req.URL.Path == "/api/records" {
APIHandler(r.lib).ServeHTTP(w, req) RecordsAPIHandler(r.rcol).ServeHTTP(w, req)
return
}
if req.URL.Path == "/api/books" {
BooksAPIHandler(r.lib).ServeHTTP(w, req)
return return
} }
StaticHandler(r.static).ServeHTTP(w, req) StaticHandler(r.static).ServeHTTP(w, req)
} }
func APIHandler(l Library) http.Handler { func BooksAPIHandler(l Library) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
books, err := l.GetAllBooks(r.Context()) books, err := l.GetAllBooks(r.Context())
if err != nil { if err != nil {
@ -50,11 +71,25 @@ func APIHandler(l Library) http.Handler {
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
w.Header().Set("Content-Type", "application/json") writeJSON(w, b, http.StatusOK)
w.WriteHeader(http.StatusOK) })
w.Write(b) }
w.Write([]byte("\n"))
func RecordsAPIHandler(l RecordCollection) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
books, err := l.GetAllRecords(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
b, err := json.Marshal(books)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, b, http.StatusOK)
}) })
} }
@ -73,8 +108,11 @@ func main() {
log.Fatalln(err) log.Fatalln(err)
} }
if c.DBUser == "" || c.DBPass == "" || c.DBHost == "" || c.DBPort == "" || c.DBName == "" { if c.DBUser == "" || c.DBPass == "" || c.DBHost == "" || c.DBPort == "" || c.DBName == "" {
if c.DBPass != "" { // obscure password if c.DBPass != "" {
c.DBPass = c.DBPass[0:max(3, len(c.DBPass))] + strings.Repeat("*", max(0, len(c.DBPass)-3)) c.DBPass = obscureStr(c.DBPass, 3)
}
if c.Discogs_Token != "" {
c.Discogs_Token = obscureStr(c.Discogs_Token, 3)
} }
log.Fatalf("vars: %+v", c) log.Fatalf("vars: %+v", c)
} }
@ -91,10 +129,16 @@ func main() {
log.Fatalln(err) log.Fatalln(err)
} }
log.Printf("latest migration: %d; migrations run: %d", latest, run) log.Printf("latest migration: %d; migrations run: %d", latest, run)
discogsCache, err := database.NewDiscogsCache(c.Discogs_Token, time.Hour*24, "delta.mu.alpha")
if err != nil {
log.Fatalln(err)
}
go discogsCache.FlushCache(context.Background())
r := &Router{ r := &Router{
static: f, static: f,
lib: lib, lib: lib,
rcol: discogsCache,
} }
log.Println("Listening on http://localhost:8080/") log.Println("Listening on http://0.0.0.0:8080/")
log.Fatalln(http.ListenAndServe(":8080", r)) log.Fatalln(http.ListenAndServe(":8080", r))
} }

View File

@ -6,5 +6,6 @@ type Config struct {
DBHost string DBHost string
DBPort string DBPort string
DBName string DBName string
Discogs_Token string
Debug bool Debug bool
} }

View File

@ -49,15 +49,15 @@ func (m *MySQL) PrepareDatabase(ctx context.Context) error {
return fmt.Errorf("uninitialized mysql client") return fmt.Errorf("uninitialized mysql client")
} }
tablecheck := `SELECT count(*) AS count tablecheck := fmt.Sprintf(`SELECT count(*) AS count
FROM information_schema.TABLES FROM information_schema.TABLES
WHERE TABLE_NAME = '` + m.versionTable + `' WHERE TABLE_NAME = '%s'
AND TABLE_SCHEMA in (SELECT DATABASE());` AND TABLE_SCHEMA in (SELECT DATABASE());`, m.versionTable)
tableschema := `CREATE TABLE ` + m.versionTable + `( tableschema := fmt.Sprintf(`CREATE TABLE %s (
id INT NOT NULL, id INT NOT NULL,
name VARCHAR(100) NOT NULL, name VARCHAR(100) NOT NULL,
datetime DATE, datetime DATE,
PRIMARY KEY (id))` PRIMARY KEY (id))`, m.versionTable)
var versionTableExists int var versionTableExists int
m.connection.QueryRowContext(ctx, tablecheck).Scan(&versionTableExists) m.connection.QueryRowContext(ctx, tablecheck).Scan(&versionTableExists)
@ -73,8 +73,9 @@ func (m *MySQL) GetLatestMigration(ctx context.Context) (int, error) {
return 0, fmt.Errorf("uninitialized mysql client") return 0, fmt.Errorf("uninitialized mysql client")
} }
migrationCheck := fmt.Sprintf("SELECT COALESCE(MAX(id), 0) FROM %s", m.versionTable)
var latestMigration int var latestMigration int
err := m.connection.QueryRowContext(ctx, "SELECT COALESCE(MAX(id), 0) FROM "+m.versionTable).Scan(&latestMigration) err := m.connection.QueryRowContext(ctx, migrationCheck).Scan(&latestMigration)
return latestMigration, err return latestMigration, err
} }
@ -97,6 +98,9 @@ func (m *MySQL) RunMigrations(ctx context.Context) (int, int, error) {
} }
mig.id, mig.name = id, name mig.id, mig.name = id, name
mig.content, err = fs.ReadFile(migrationsFS, m.migrationsDirectory+"/"+dir[f].Name()) mig.content, err = fs.ReadFile(migrationsFS, m.migrationsDirectory+"/"+dir[f].Name())
if err != nil {
return 0, 0, fmt.Errorf("failure loading migration: %w", err)
}
migrations[mig.id] = mig migrations[mig.id] = mig
} }
} }
@ -116,6 +120,7 @@ func (m *MySQL) RunMigrations(ctx context.Context) (int, int, error) {
if err != nil { if err != nil {
return latestMigrationRan, 0, err return latestMigrationRan, 0, err
} }
migrationLogSql := fmt.Sprintf("INSERT INTO %s (id, name, datetime) VALUES (?, ?, ?)", m.versionTable)
migrationsRun := 0 migrationsRun := 0
for migrationsToRun := true; migrationsToRun; _, migrationsToRun = migrations[latestMigrationRan+1] { for migrationsToRun := true; migrationsToRun; _, migrationsToRun = migrations[latestMigrationRan+1] {
mig := migrations[latestMigrationRan+1] mig := migrations[latestMigrationRan+1]
@ -127,7 +132,7 @@ func (m *MySQL) RunMigrations(ctx context.Context) (int, int, error) {
} }
return latestMigrationRan, migrationsRun, err return latestMigrationRan, migrationsRun, err
} }
_, err = tx.ExecContext(ctx, "INSERT INTO "+m.versionTable+" (id, name, datetime) VALUES (?, ?, ?)", mig.id, mig.name, time.Now()) _, err = tx.ExecContext(ctx, migrationLogSql, mig.id, mig.name, time.Now())
if err != nil { if err != nil {
nestederr := tx.Rollback() nestederr := tx.Rollback()
if nestederr != nil { if nestederr != nil {
@ -147,26 +152,14 @@ func (m *MySQL) GetAllBooks(ctx context.Context) ([]media.Book, error) {
return nil, fmt.Errorf("uninitialized mysql client") return nil, fmt.Errorf("uninitialized mysql client")
} }
allBooksQuery := fmt.Sprintf(`SELECT
id, title, authors, sortauthor, isbn10, isbn13, format,
genre, publisher, series, volume, year, signed,
description, notes, onloan, coverurl
FROM %s`, m.tableName)
books := []media.Book{} books := []media.Book{}
rows, err := m.connection.QueryContext(ctx, ` rows, err := m.connection.QueryContext(ctx, allBooksQuery)
SELECT id,
title,
authors,
sortauthor,
isbn10,
isbn13,
format,
genre,
publisher,
series,
volume,
year,
signed,
description,
notes,
onloan,
coverurl
FROM `+m.tableName)
if err != nil { if err != nil {
return nil, err return nil, err
} }

111
database/records.go Normal file
View File

@ -0,0 +1,111 @@
package database
import (
"context"
"fmt"
"log"
"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
}
func NewDiscogsCache(token string, maxCacheAge time.Duration, username string) (*DiscogsCache, error) {
client, err := discogs.New(&discogs.Options{
UserAgent: "library.yetaga.in personal collection cache",
Token: token,
})
if err != nil {
return nil, err
}
return &DiscogsCache{
authToken: token,
client: client,
maxCacheAge: maxCacheAge,
username: username,
}, nil
}
func (c *DiscogsCache) FlushCache(ctx context.Context) error {
c.m.Lock()
defer c.m.Unlock()
records, err := c.fetchRecords(ctx, nil)
c.cache = records
c.lastRefresh = time.Now()
return err
}
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)
c.cache = records
c.lastRefresh = time.Now()
return c.cache, err
}
return c.cache, nil
}
func (c *DiscogsCache) fetchRecords(ctx context.Context, pagination *discogs.Pagination) ([]media.Record, error) {
records := []media.Record{}
if pagination == nil {
pagination = getPagination(0)
}
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)
}
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),
}
}

View File

@ -4,7 +4,7 @@ var sortState = {
}; };
function init() { function init() {
fetch("/api") fetch("/api/books")
.then((response) => response.json()) .then((response) => response.json())
.then((books) => { .then((books) => {
// prepare response // prepare response

3
go.mod
View File

@ -2,8 +2,11 @@ module git.yetaga.in/alazyreader/library
go 1.16 go 1.16
replace github.com/irlndts/go-discogs v0.3.5 => git.yetaga.in/alazyreader/go-discogs v0.3.6
require ( require (
github.com/gdamore/tcell v1.4.0 github.com/gdamore/tcell v1.4.0
github.com/go-sql-driver/mysql v1.6.0 github.com/go-sql-driver/mysql v1.6.0
github.com/irlndts/go-discogs v0.3.5
github.com/kelseyhightower/envconfig v1.4.0 github.com/kelseyhightower/envconfig v1.4.0
) )

7
go.sum
View File

@ -1,9 +1,15 @@
git.yetaga.in/alazyreader/go-discogs v0.3.5 h1:XcwcFJP0p1eQQ6OQhScYiNM8+vymeEO4V+M9H9x58os=
git.yetaga.in/alazyreader/go-discogs v0.3.5/go.mod h1:UVQ05FdCzH4P/usnSxQDh77QYE37HvmPnSCgogioljo=
git.yetaga.in/alazyreader/go-discogs v0.3.6 h1:VhV8/uhnWsxae6PvIVtXOfO4eWWqShX6DkiN2hFsZ8U=
git.yetaga.in/alazyreader/go-discogs v0.3.6/go.mod h1:UVQ05FdCzH4P/usnSxQDh77QYE37HvmPnSCgogioljo=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU= github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU=
github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0= github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
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/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
@ -14,3 +20,4 @@ golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeo
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -21,4 +21,16 @@ type Book struct {
} }
type Record struct { type Record struct {
ID int `json:"-"`
AlbumName string `json:"name"`
Artists []string `json:"artists"`
SortArtist string `json:"sortArtist"`
Identifier string `json:"identifier"`
Format string `json:"format"`
Genre string `json:"genre"`
Label string `json:"label"`
Year string `json:"year"`
Description string `json:"description"`
CoverURL string `json:"coverURL"`
DiscogsURL string `json:"discogsURL"`
} }