Merge pull request 'discogs' (#2) from discogs into master
Reviewed-on: #2
This commit was merged in pull request #2.
	This commit is contained in:
		
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -2,4 +2,5 @@
 | 
				
			|||||||
/manager
 | 
					/manager
 | 
				
			||||||
*.properties
 | 
					*.properties
 | 
				
			||||||
.DS_Store
 | 
					.DS_Store
 | 
				
			||||||
*.csv
 | 
					*.csv
 | 
				
			||||||
 | 
					/vendor
 | 
				
			||||||
@@ -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.DiscogsToken != "" {
 | 
				
			||||||
 | 
								c.DiscogsToken = obscureStr(c.DiscogsToken, 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.DiscogsToken, 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))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,10 +1,11 @@
 | 
				
			|||||||
package config
 | 
					package config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Config struct {
 | 
					type Config struct {
 | 
				
			||||||
	DBUser string
 | 
						DBUser       string
 | 
				
			||||||
	DBPass string
 | 
						DBPass       string
 | 
				
			||||||
	DBHost string
 | 
						DBHost       string
 | 
				
			||||||
	DBPort string
 | 
						DBPort       string
 | 
				
			||||||
	DBName string
 | 
						DBName       string
 | 
				
			||||||
	Debug  bool
 | 
						DiscogsToken string
 | 
				
			||||||
 | 
						Debug        bool
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										112
									
								
								database/records.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								database/records.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,112 @@
 | 
				
			|||||||
 | 
					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(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),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,6 +22,7 @@
 | 
				
			|||||||
    <div class="wrapper">
 | 
					    <div class="wrapper">
 | 
				
			||||||
      <div id="header">
 | 
					      <div id="header">
 | 
				
			||||||
        <h1>Library</h1>
 | 
					        <h1>Library</h1>
 | 
				
			||||||
 | 
					        <a href="/records">records</a>
 | 
				
			||||||
        <a
 | 
					        <a
 | 
				
			||||||
          target="_blank"
 | 
					          target="_blank"
 | 
				
			||||||
          rel="noreferrer"
 | 
					          rel="noreferrer"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										176
									
								
								frontend/files/records/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								frontend/files/records/app.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,176 @@
 | 
				
			|||||||
 | 
					var sortState = {
 | 
				
			||||||
 | 
					  sortBy: "sortArtist",
 | 
				
			||||||
 | 
					  sortOrder: "asc",
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function init() {
 | 
				
			||||||
 | 
					  fetch("/api/records")
 | 
				
			||||||
 | 
					    .then((response) => response.json())
 | 
				
			||||||
 | 
					    .then((records) => {
 | 
				
			||||||
 | 
					      // prepare response
 | 
				
			||||||
 | 
					      records.forEach(apiResponseParsing);
 | 
				
			||||||
 | 
					      document.getElementById("search").addEventListener("input", (e) => {
 | 
				
			||||||
 | 
					        renderTable(search(records, e.target.value));
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      renderTable(records);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function renderTable(records, sortField) {
 | 
				
			||||||
 | 
					  if (sortField) {
 | 
				
			||||||
 | 
					    if (sortState.sortBy === sortField && sortState.sortOrder === "asc") {
 | 
				
			||||||
 | 
					      sortState.sortOrder = "desc";
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      sortState.sortOrder = "asc";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    sortState.sortBy = sortField;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  records.sort((one, two) =>
 | 
				
			||||||
 | 
					    (one[sortState.sortBy] + one["sortName"]).localeCompare(
 | 
				
			||||||
 | 
					      two[sortState.sortBy] + two["sortName"]
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  if (sortState.sortOrder === "desc") {
 | 
				
			||||||
 | 
					    records.reverse();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  records.forEach((e, i) => (e.rowNumber = i)); // re-key
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // rendering
 | 
				
			||||||
 | 
					  var recordElement = document.getElementById("records");
 | 
				
			||||||
 | 
					  recordElement.innerHTML = TableTemplate(records);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // add listeners for selecting record to view
 | 
				
			||||||
 | 
					  Array.from(recordElement.querySelectorAll("tbody tr"))
 | 
				
			||||||
 | 
					    .slice(1) // remove header from Array
 | 
				
			||||||
 | 
					    .forEach((row) => {
 | 
				
			||||||
 | 
					      row.addEventListener("click", (e) => {
 | 
				
			||||||
 | 
					        // add listener to swap current record
 | 
				
			||||||
 | 
					        document.getElementById("current").innerHTML = RecordTemplate(
 | 
				
			||||||
 | 
					          records[e.currentTarget.id]
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  // add sorting callbacks
 | 
				
			||||||
 | 
					  Array.from(
 | 
				
			||||||
 | 
					    recordElement.querySelectorAll("tbody tr th[data-sort-by]")
 | 
				
			||||||
 | 
					  ).forEach((row) => {
 | 
				
			||||||
 | 
					    row.addEventListener("click", function (e) {
 | 
				
			||||||
 | 
					      renderTable(records, e.target.dataset.sortBy); // only add callback when there's a sortBy attribute
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  // mark currently active column
 | 
				
			||||||
 | 
					  recordElement
 | 
				
			||||||
 | 
					    .querySelector("tbody tr th[data-sort-by=" + sortState.sortBy + "]")
 | 
				
			||||||
 | 
					    .classList.add(sortState.sortOrder);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function apiResponseParsing(record) {
 | 
				
			||||||
 | 
					  record.sortName = titleCleaner(record.name);
 | 
				
			||||||
 | 
					  record.artists = record.artists.map((artist) => {
 | 
				
			||||||
 | 
					    return artist.replace(/ \([0-9]+\)$/, "");
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  record.sortArtist = record.artists.reduce((acc, curr) => {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      acc +
 | 
				
			||||||
 | 
					      curr
 | 
				
			||||||
 | 
					        .replace(/^(An?|The)\s/i, "")
 | 
				
			||||||
 | 
					        .toLowerCase()
 | 
				
			||||||
 | 
					        .replaceAll('"', "")
 | 
				
			||||||
 | 
					        .replaceAll(":", "")
 | 
				
			||||||
 | 
					        .replaceAll("'", "")
 | 
				
			||||||
 | 
					        .replaceAll(" ", "")
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }, "");
 | 
				
			||||||
 | 
					  return record;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function search(records, searchBy) {
 | 
				
			||||||
 | 
					  searchBy = searchCleaner(searchBy);
 | 
				
			||||||
 | 
					  if (searchBy !== "") {
 | 
				
			||||||
 | 
					    records = records.filter(({ name, artists, genre, label, year }) => {
 | 
				
			||||||
 | 
					      return Object.values({
 | 
				
			||||||
 | 
					        name,
 | 
				
			||||||
 | 
					        artists: artists.join(" "),
 | 
				
			||||||
 | 
					        genre,
 | 
				
			||||||
 | 
					        label,
 | 
				
			||||||
 | 
					        year,
 | 
				
			||||||
 | 
					      }).find((field) => searchCleaner(field).indexOf(searchBy) !== -1);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return records;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function titleCleaner(title) {
 | 
				
			||||||
 | 
					  return title
 | 
				
			||||||
 | 
					    .replace('"', "")
 | 
				
			||||||
 | 
					    .replace(":", "")
 | 
				
			||||||
 | 
					    .replace(/^(An?|The)\s/i, "");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function searchCleaner(str) {
 | 
				
			||||||
 | 
					  return str
 | 
				
			||||||
 | 
					    .toLowerCase()
 | 
				
			||||||
 | 
					    .replaceAll('"', "")
 | 
				
			||||||
 | 
					    .replaceAll(":", "")
 | 
				
			||||||
 | 
					    .replaceAll("'", "")
 | 
				
			||||||
 | 
					    .replaceAll(" ", "");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function RecordTemplate({
 | 
				
			||||||
 | 
					  name,
 | 
				
			||||||
 | 
					  artists,
 | 
				
			||||||
 | 
					  coverURL,
 | 
				
			||||||
 | 
					  format,
 | 
				
			||||||
 | 
					  genre,
 | 
				
			||||||
 | 
					  identifier,
 | 
				
			||||||
 | 
					  label,
 | 
				
			||||||
 | 
					  year,
 | 
				
			||||||
 | 
					  discogsURL,
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  return `${coverURL ? `<img src="${coverURL}"/>` : ""}
 | 
				
			||||||
 | 
					  <h1>${name}</h1>
 | 
				
			||||||
 | 
					  <h2>${artists.join(", ")}</h2>
 | 
				
			||||||
 | 
					  <span>${identifier}</span><br/>
 | 
				
			||||||
 | 
					  <span>${genre}, ${label}, ${year}</span><br/>
 | 
				
			||||||
 | 
					  <span>${format}</span>
 | 
				
			||||||
 | 
					  <div>
 | 
				
			||||||
 | 
					    <a 
 | 
				
			||||||
 | 
					      target="_blank"
 | 
				
			||||||
 | 
					      href="${discogsURL}"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      Data provided by Discogs.
 | 
				
			||||||
 | 
					    </a>
 | 
				
			||||||
 | 
					  </div>`;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function TableRowTemplate({
 | 
				
			||||||
 | 
					  artists,
 | 
				
			||||||
 | 
					  identifier,
 | 
				
			||||||
 | 
					  label,
 | 
				
			||||||
 | 
					  rowNumber,
 | 
				
			||||||
 | 
					  name,
 | 
				
			||||||
 | 
					  year,
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  return `<tr class="tRow" id="${rowNumber}">
 | 
				
			||||||
 | 
					    <td class="name">
 | 
				
			||||||
 | 
					      ${name}
 | 
				
			||||||
 | 
					    </td>
 | 
				
			||||||
 | 
					    <td class="artist">${artists.join(", ")}</td>
 | 
				
			||||||
 | 
					    <td class="label">${label}</td>
 | 
				
			||||||
 | 
					    <td class="identifier">${identifier}</td>
 | 
				
			||||||
 | 
					    <td class="year">${year}</td>
 | 
				
			||||||
 | 
					  </tr>`;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function TableTemplate(records) {
 | 
				
			||||||
 | 
					  return `<table class="recordTable">
 | 
				
			||||||
 | 
					    <tr>
 | 
				
			||||||
 | 
					      <th data-sort-by="sortName" class="tHeader name">Name</th>
 | 
				
			||||||
 | 
					      <th data-sort-by="sortArtist" class="tHeader artist">Artist(s)</th>
 | 
				
			||||||
 | 
					      <th data-sort-by="label" class="tHeader label">Label</th>
 | 
				
			||||||
 | 
					      <th data-sort-by="identifier" class="tHeader identifier">Identifier</th>
 | 
				
			||||||
 | 
					      <th data-sort-by="year" class="tHeader year">Year</th>
 | 
				
			||||||
 | 
					    </tr>${records.reduce((acc, record) => {
 | 
				
			||||||
 | 
					      return acc.concat(TableRowTemplate(record));
 | 
				
			||||||
 | 
					    }, "")} </table>`;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								frontend/files/records/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								frontend/files/records/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 174 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								frontend/files/records/favicon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								frontend/files/records/favicon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 37 KiB  | 
							
								
								
									
										46
									
								
								frontend/files/records/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								frontend/files/records/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html>
 | 
				
			||||||
 | 
					  <head>
 | 
				
			||||||
 | 
					    <title>Library</title>
 | 
				
			||||||
 | 
					    <link
 | 
				
			||||||
 | 
					      rel="stylesheet"
 | 
				
			||||||
 | 
					      href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <link rel="stylesheet" href="style.css" />
 | 
				
			||||||
 | 
					    <link rel="icon" href="favicon.ico" type="image/x-icon" />
 | 
				
			||||||
 | 
					    <link
 | 
				
			||||||
 | 
					      href="https://fonts.googleapis.com/css?family=Libre+Baskerville:400,700&display=swap"
 | 
				
			||||||
 | 
					      as="style"
 | 
				
			||||||
 | 
					      rel="stylesheet preload prefetch"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <script type="text/javascript" src="app.js"></script>
 | 
				
			||||||
 | 
					    <script type="text/javascript">
 | 
				
			||||||
 | 
					      window.addEventListener("DOMContentLoaded", init);
 | 
				
			||||||
 | 
					    </script>
 | 
				
			||||||
 | 
					  </head>
 | 
				
			||||||
 | 
					  <body>
 | 
				
			||||||
 | 
					    <div class="wrapper">
 | 
				
			||||||
 | 
					      <div id="header">
 | 
				
			||||||
 | 
					        <h1>Records</h1>
 | 
				
			||||||
 | 
					        <a href="/">books</a>
 | 
				
			||||||
 | 
					        <a
 | 
				
			||||||
 | 
					          target="_blank"
 | 
				
			||||||
 | 
					          rel="noreferrer"
 | 
				
			||||||
 | 
					          href="https://git.yetaga.in/alazyreader/library"
 | 
				
			||||||
 | 
					          >git</a
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					        <div id="searchBox">
 | 
				
			||||||
 | 
					          <input
 | 
				
			||||||
 | 
					            id="search"
 | 
				
			||||||
 | 
					            type="text"
 | 
				
			||||||
 | 
					            name="search"
 | 
				
			||||||
 | 
					            placeholder="Search..."
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div id="current">No Record Selected</div>
 | 
				
			||||||
 | 
					      <div id="records"></div>
 | 
				
			||||||
 | 
					      <!-- Table goes here -->
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										284
									
								
								frontend/files/records/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										284
									
								
								frontend/files/records/style.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,284 @@
 | 
				
			|||||||
 | 
					/* http://meyerweb.com/eric/tools/css/reset/ 
 | 
				
			||||||
 | 
					   v2.0 | 20110126
 | 
				
			||||||
 | 
					   License: none (public domain)
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					html,
 | 
				
			||||||
 | 
					body,
 | 
				
			||||||
 | 
					div,
 | 
				
			||||||
 | 
					span,
 | 
				
			||||||
 | 
					applet,
 | 
				
			||||||
 | 
					object,
 | 
				
			||||||
 | 
					iframe,
 | 
				
			||||||
 | 
					h1,
 | 
				
			||||||
 | 
					h2,
 | 
				
			||||||
 | 
					h3,
 | 
				
			||||||
 | 
					h4,
 | 
				
			||||||
 | 
					h5,
 | 
				
			||||||
 | 
					h6,
 | 
				
			||||||
 | 
					p,
 | 
				
			||||||
 | 
					blockquote,
 | 
				
			||||||
 | 
					pre,
 | 
				
			||||||
 | 
					a,
 | 
				
			||||||
 | 
					abbr,
 | 
				
			||||||
 | 
					acronym,
 | 
				
			||||||
 | 
					address,
 | 
				
			||||||
 | 
					big,
 | 
				
			||||||
 | 
					cite,
 | 
				
			||||||
 | 
					code,
 | 
				
			||||||
 | 
					del,
 | 
				
			||||||
 | 
					dfn,
 | 
				
			||||||
 | 
					em,
 | 
				
			||||||
 | 
					img,
 | 
				
			||||||
 | 
					ins,
 | 
				
			||||||
 | 
					kbd,
 | 
				
			||||||
 | 
					q,
 | 
				
			||||||
 | 
					s,
 | 
				
			||||||
 | 
					samp,
 | 
				
			||||||
 | 
					small,
 | 
				
			||||||
 | 
					strike,
 | 
				
			||||||
 | 
					strong,
 | 
				
			||||||
 | 
					sub,
 | 
				
			||||||
 | 
					sup,
 | 
				
			||||||
 | 
					tt,
 | 
				
			||||||
 | 
					var,
 | 
				
			||||||
 | 
					b,
 | 
				
			||||||
 | 
					u,
 | 
				
			||||||
 | 
					i,
 | 
				
			||||||
 | 
					center,
 | 
				
			||||||
 | 
					dl,
 | 
				
			||||||
 | 
					dt,
 | 
				
			||||||
 | 
					dd,
 | 
				
			||||||
 | 
					ol,
 | 
				
			||||||
 | 
					ul,
 | 
				
			||||||
 | 
					li,
 | 
				
			||||||
 | 
					fieldset,
 | 
				
			||||||
 | 
					form,
 | 
				
			||||||
 | 
					label,
 | 
				
			||||||
 | 
					legend,
 | 
				
			||||||
 | 
					table,
 | 
				
			||||||
 | 
					caption,
 | 
				
			||||||
 | 
					tbody,
 | 
				
			||||||
 | 
					tfoot,
 | 
				
			||||||
 | 
					thead,
 | 
				
			||||||
 | 
					tr,
 | 
				
			||||||
 | 
					th,
 | 
				
			||||||
 | 
					td,
 | 
				
			||||||
 | 
					article,
 | 
				
			||||||
 | 
					aside,
 | 
				
			||||||
 | 
					canvas,
 | 
				
			||||||
 | 
					details,
 | 
				
			||||||
 | 
					embed,
 | 
				
			||||||
 | 
					figure,
 | 
				
			||||||
 | 
					figcaption,
 | 
				
			||||||
 | 
					footer,
 | 
				
			||||||
 | 
					header,
 | 
				
			||||||
 | 
					hgroup,
 | 
				
			||||||
 | 
					menu,
 | 
				
			||||||
 | 
					nav,
 | 
				
			||||||
 | 
					output,
 | 
				
			||||||
 | 
					ruby,
 | 
				
			||||||
 | 
					section,
 | 
				
			||||||
 | 
					summary,
 | 
				
			||||||
 | 
					time,
 | 
				
			||||||
 | 
					mark,
 | 
				
			||||||
 | 
					audio,
 | 
				
			||||||
 | 
					video {
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					  border: 0;
 | 
				
			||||||
 | 
					  font-size: 100%;
 | 
				
			||||||
 | 
					  font: inherit;
 | 
				
			||||||
 | 
					  vertical-align: baseline;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					article,
 | 
				
			||||||
 | 
					aside,
 | 
				
			||||||
 | 
					details,
 | 
				
			||||||
 | 
					figcaption,
 | 
				
			||||||
 | 
					figure,
 | 
				
			||||||
 | 
					footer,
 | 
				
			||||||
 | 
					header,
 | 
				
			||||||
 | 
					hgroup,
 | 
				
			||||||
 | 
					menu,
 | 
				
			||||||
 | 
					nav,
 | 
				
			||||||
 | 
					section {
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					body {
 | 
				
			||||||
 | 
					  line-height: 1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					ol,
 | 
				
			||||||
 | 
					ul {
 | 
				
			||||||
 | 
					  list-style: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					blockquote,
 | 
				
			||||||
 | 
					q {
 | 
				
			||||||
 | 
					  quotes: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					blockquote:before,
 | 
				
			||||||
 | 
					blockquote:after,
 | 
				
			||||||
 | 
					q:before,
 | 
				
			||||||
 | 
					q:after {
 | 
				
			||||||
 | 
					  content: "";
 | 
				
			||||||
 | 
					  content: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					table {
 | 
				
			||||||
 | 
					  border-collapse: collapse;
 | 
				
			||||||
 | 
					  border-spacing: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* site CSS starts here */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					body {
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#header {
 | 
				
			||||||
 | 
					  height: 30px;
 | 
				
			||||||
 | 
					  width: calc(100vw - 20px);
 | 
				
			||||||
 | 
					  padding: 4px 10px;
 | 
				
			||||||
 | 
					  background-color: #f7f3dc;
 | 
				
			||||||
 | 
					  border-bottom: 2px solid #d8d0a0;
 | 
				
			||||||
 | 
					  font-family: "Libre Baskerville", sans-serif;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#header h1 {
 | 
				
			||||||
 | 
					  font-size: xx-large;
 | 
				
			||||||
 | 
					  display: inline;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#searchBox {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  right: 10px;
 | 
				
			||||||
 | 
					  top: 7px;
 | 
				
			||||||
 | 
					  text-align: right;
 | 
				
			||||||
 | 
					  width: 400px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#searchBox input {
 | 
				
			||||||
 | 
					  width: 300px;
 | 
				
			||||||
 | 
					  font-size: 16px;
 | 
				
			||||||
 | 
					  background: #f9f8ed;
 | 
				
			||||||
 | 
					  padding: 2px 5px;
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  border-bottom: 2px solid #d8d0a0;
 | 
				
			||||||
 | 
					  font-family: "Libre Baskerville", sans-serif;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#searchBox input:focus {
 | 
				
			||||||
 | 
					  outline: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#searchBox input::placeholder {
 | 
				
			||||||
 | 
					  font-family: "Libre Baskerville", sans-serif;
 | 
				
			||||||
 | 
					  color: #d8d0a0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#current {
 | 
				
			||||||
 | 
					  background-color: #f7f3dc;
 | 
				
			||||||
 | 
					  width: calc(40vw - 40px);
 | 
				
			||||||
 | 
					  height: calc(100vh - 80px);
 | 
				
			||||||
 | 
					  padding: 20px;
 | 
				
			||||||
 | 
					  overflow: auto;
 | 
				
			||||||
 | 
					  float: left;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#records {
 | 
				
			||||||
 | 
					  width: calc(60vw - 40px);
 | 
				
			||||||
 | 
					  height: calc(100vh - 80px);
 | 
				
			||||||
 | 
					  padding: 20px;
 | 
				
			||||||
 | 
					  overflow: auto;
 | 
				
			||||||
 | 
					  float: left;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.recordTable th {
 | 
				
			||||||
 | 
					  font-weight: bold;
 | 
				
			||||||
 | 
					  text-align: left;
 | 
				
			||||||
 | 
					  font-family: "Libre Baskerville", sans-serif;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.recordTable th[data-sort-by] {
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.recordTable th[data-sort-by]::after {
 | 
				
			||||||
 | 
					  content: "\f0dc";
 | 
				
			||||||
 | 
					  font-family: FontAwesome;
 | 
				
			||||||
 | 
					  font-size: x-small;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  left: 4px;
 | 
				
			||||||
 | 
					  bottom: 2px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.recordTable th.asc::after {
 | 
				
			||||||
 | 
					  content: "\f0de";
 | 
				
			||||||
 | 
					  font-family: FontAwesome;
 | 
				
			||||||
 | 
					  font-size: x-small;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  left: 4px;
 | 
				
			||||||
 | 
					  bottom: 2px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.recordTable th.desc::after {
 | 
				
			||||||
 | 
					  content: "\f0dd";
 | 
				
			||||||
 | 
					  font-family: FontAwesome;
 | 
				
			||||||
 | 
					  font-size: x-small;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  left: 4px;
 | 
				
			||||||
 | 
					  bottom: 2px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.recordTable td,
 | 
				
			||||||
 | 
					.recordTable th {
 | 
				
			||||||
 | 
					  padding: 5px;
 | 
				
			||||||
 | 
					  min-width: 50px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tRow:nth-child(odd) {
 | 
				
			||||||
 | 
					  background: #f9f8ed;
 | 
				
			||||||
 | 
					  border-bottom: 1px solid #d8d0a0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.recordTable .tRow {
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.recordTable .onLoan {
 | 
				
			||||||
 | 
					  color: #bbb;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.recordTable .tRow .title {
 | 
				
			||||||
 | 
					  font-style: italic;
 | 
				
			||||||
 | 
					  max-width: 600px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#current h1 {
 | 
				
			||||||
 | 
					  font-size: x-large;
 | 
				
			||||||
 | 
					  font-weight: bold;
 | 
				
			||||||
 | 
					  font-style: italic;
 | 
				
			||||||
 | 
					  padding: 10px 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#current h2 {
 | 
				
			||||||
 | 
					  font-size: large;
 | 
				
			||||||
 | 
					  padding: 7px 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#current img {
 | 
				
			||||||
 | 
					  max-height: 400px;
 | 
				
			||||||
 | 
					  max-width: 100%;
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					  margin: 0 auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#current .description p {
 | 
				
			||||||
 | 
					  padding: 20px 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#current h1.onLoan {
 | 
				
			||||||
 | 
					  color: #bbb;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#current h2.onLoan {
 | 
				
			||||||
 | 
					  font-weight: bold;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										3
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								go.mod
									
									
									
									
									
								
							@@ -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
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								go.sum
									
									
									
									
									
								
							@@ -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=
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user