Move backend to subdir
authorJacob Casper <dev@jacobcasper.com>
Fri, 17 Apr 2020 02:18:49 +0000 (21:18 -0500)
committerJacob Casper <dev@jacobcasper.com>
Fri, 17 Apr 2020 02:18:49 +0000 (21:18 -0500)
37 files changed:
.gitignore
backend/client/client.go [new file with mode: 0644]
backend/db/db.go [new file with mode: 0644]
backend/env/env.go [new file with mode: 0644]
backend/go.mod [new file with mode: 0644]
backend/go.sum [new file with mode: 0644]
backend/main.go [new file with mode: 0644]
backend/migrations/001-create_artist.sql [new file with mode: 0644]
backend/migrations/002-create_genre.sql [new file with mode: 0644]
backend/migrations/003-create_artist_genre_xref.sql [new file with mode: 0644]
backend/migrations/004-create_scraped_artist.sql [new file with mode: 0644]
backend/migrations/005-alter_artist_add_popularity.sql [new file with mode: 0644]
backend/routes/artist/artist.go [new file with mode: 0644]
backend/routes/genre/genre.go [new file with mode: 0644]
backend/routes/routes.go [new file with mode: 0644]
backend/scrape/graph/graph.go [new file with mode: 0644]
backend/scrape/seed/seed.go [new file with mode: 0644]
backend/types/artist.go [new file with mode: 0644]
backend/types/genre.go [new file with mode: 0644]
client/client.go [deleted file]
db/db.go [deleted file]
env/env.go [deleted file]
go.mod [deleted file]
go.sum [deleted file]
main.go [deleted file]
migrations/001-create_artist.sql [deleted file]
migrations/002-create_genre.sql [deleted file]
migrations/003-create_artist_genre_xref.sql [deleted file]
migrations/004-create_scraped_artist.sql [deleted file]
migrations/005-alter_artist_add_popularity.sql [deleted file]
routes/artist/artist.go [deleted file]
routes/genre/genre.go [deleted file]
routes/routes.go [deleted file]
scrape/graph/graph.go [deleted file]
scrape/seed/seed.go [deleted file]
types/artist.go [deleted file]
types/genre.go [deleted file]

index f166cd5..f6cbb54 100644 (file)
@@ -1,4 +1,4 @@
 brackets*
 brackets*
-*/seed/seed
+backend/scrape/seed/seed
 secrets.sh
 vendor/
 secrets.sh
 vendor/
diff --git a/backend/client/client.go b/backend/client/client.go
new file mode 100644 (file)
index 0000000..394ee5f
--- /dev/null
@@ -0,0 +1,26 @@
+package client
+
+import (
+       "context"
+       "fmt"
+       "github.com/zmb3/spotify"
+       "golang.org/x/oauth2/clientcredentials"
+       "os"
+)
+
+func Get() (*spotify.Client, error) {
+       config := &clientcredentials.Config{
+               ClientID:     os.Getenv("SPOTIFY_ID"),
+               ClientSecret: os.Getenv("SPOTIFY_SECRET"),
+               TokenURL:     spotify.TokenURL,
+       }
+
+       token, err := config.Token(context.Background())
+       if err != nil {
+               return nil, fmt.Errorf("client: %w", err)
+       }
+
+       client := spotify.Authenticator{}.NewClient(token)
+       client.AutoRetry = true
+       return &client, nil
+}
diff --git a/backend/db/db.go b/backend/db/db.go
new file mode 100644 (file)
index 0000000..3c04e68
--- /dev/null
@@ -0,0 +1,20 @@
+package db
+
+import (
+       "database/sql"
+       "sync"
+)
+
+// A DB that can be locked, as SQLite can't be concurrently written to.
+type DB struct {
+       Db *sql.DB
+       Mu *sync.Mutex
+}
+
+func New() (*DB, error) {
+       db, err := sql.Open("sqlite3", "brackets.sqlite?_journal_mode=WAL&_foreign_keys=on")
+       if err != nil {
+               return nil, err
+       }
+       return &DB{Db: db, Mu: &sync.Mutex{}}, err
+}
diff --git a/backend/env/env.go b/backend/env/env.go
new file mode 100644 (file)
index 0000000..9efbf2e
--- /dev/null
@@ -0,0 +1,25 @@
+package env
+
+import (
+       "git.jacobcasper.com/brackets/client"
+       "git.jacobcasper.com/brackets/db"
+       "github.com/zmb3/spotify"
+)
+
+type Env struct {
+       Db *db.DB
+       C  *spotify.Client
+}
+
+func New() (*Env, error) {
+       db, err := db.New()
+       if err != nil {
+               return nil, err
+       }
+
+       client, err := client.Get()
+       if err != nil {
+               return nil, err
+       }
+       return &Env{Db: db, C: client}, nil
+}
diff --git a/backend/go.mod b/backend/go.mod
new file mode 100644 (file)
index 0000000..a51656d
--- /dev/null
@@ -0,0 +1,9 @@
+module git.jacobcasper.com/brackets
+
+go 1.14
+
+require (
+       github.com/mattn/go-sqlite3 v2.0.3+incompatible
+       github.com/zmb3/spotify v0.0.0-20200413172747-49e8144a06f7
+       golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
+)
diff --git a/backend/go.sum b/backend/go.sum
new file mode 100644 (file)
index 0000000..5c58929
--- /dev/null
@@ -0,0 +1,17 @@
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
+github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/zmb3/spotify v0.0.0-20200413172747-49e8144a06f7 h1:EsuM9MBGEmyCEQn2Tz4bO6IyUCgCovK3eZm9rrBJ9FE=
+github.com/zmb3/spotify v0.0.0-20200413172747-49e8144a06f7/go.mod h1:CYu0Uo+YYMlUX39zUTsCU9j3SpK3l1eB8oLykXF7R7w=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
diff --git a/backend/main.go b/backend/main.go
new file mode 100644 (file)
index 0000000..702fd48
--- /dev/null
@@ -0,0 +1,28 @@
+package main
+
+import (
+       "git.jacobcasper.com/brackets/env"
+       "git.jacobcasper.com/brackets/routes/artist"
+       "git.jacobcasper.com/brackets/routes/genre"
+       "git.jacobcasper.com/brackets/scrape/graph"
+       _ "github.com/mattn/go-sqlite3"
+       "log"
+       "net/http"
+)
+
+func main() {
+       env, err := env.New()
+       if err != nil {
+               log.Fatal("Could not set up Env: ", err.Error())
+       }
+
+       http.HandleFunc("/artist/", artist.Index(env))
+       http.HandleFunc("/artist/genre", artist.ByGenre(env))
+       http.HandleFunc("/artist/add", artist.Add(env))
+
+       http.HandleFunc("/genre", genre.Index(env))
+
+       go graph.Scrape(env)
+
+       log.Fatal(http.ListenAndServe(":8080", nil))
+}
diff --git a/backend/migrations/001-create_artist.sql b/backend/migrations/001-create_artist.sql
new file mode 100644 (file)
index 0000000..7910b80
--- /dev/null
@@ -0,0 +1,4 @@
+CREATE TABLE ARTIST (
+    ID TEXT PRIMARY KEY,
+    NAME TEXT 
+);
diff --git a/backend/migrations/002-create_genre.sql b/backend/migrations/002-create_genre.sql
new file mode 100644 (file)
index 0000000..6224609
--- /dev/null
@@ -0,0 +1,4 @@
+CREATE TABLE GENRE (
+  ID INTEGER PRIMARY KEY AUTOINCREMENT,
+  NAME TEXT NOT NULL UNIQUE
+);
diff --git a/backend/migrations/003-create_artist_genre_xref.sql b/backend/migrations/003-create_artist_genre_xref.sql
new file mode 100644 (file)
index 0000000..25b9f42
--- /dev/null
@@ -0,0 +1,8 @@
+CREATE TABLE ARTIST_GENRE_XREF (
+  ID INTEGER PRIMARY KEY AUTOINCREMENT,
+  ARTIST_ID NOT NULL,
+  GENRE_ID NOT NULL,
+  UNIQUE(ARTIST_ID, GENRE_ID),
+  FOREIGN KEY(ARTIST_ID) REFERENCES ARTIST(ID),
+  FOREIGN KEY(GENRE_ID) REFERENCES GENRE(ID)
+);
diff --git a/backend/migrations/004-create_scraped_artist.sql b/backend/migrations/004-create_scraped_artist.sql
new file mode 100644 (file)
index 0000000..bc51487
--- /dev/null
@@ -0,0 +1,5 @@
+CREATE TABLE SCRAPED_ARTIST (
+  ARTIST_ID TEXT UNIQUE NOT NULL,
+  SCRAPED BOOLEAN NOT NULL DEFAULT 0,
+  FOREIGN KEY(ARTIST_ID) REFERENCES ARTIST(ID)
+);
diff --git a/backend/migrations/005-alter_artist_add_popularity.sql b/backend/migrations/005-alter_artist_add_popularity.sql
new file mode 100644 (file)
index 0000000..fdd4e49
--- /dev/null
@@ -0,0 +1,3 @@
+ALTER TABLE ARTIST
+ADD COLUMN POPULARITY INT NOT NULL DEFAULT 0
+CHECK (POPULARITY >= 0 AND POPULARITY <= 100);
diff --git a/backend/routes/artist/artist.go b/backend/routes/artist/artist.go
new file mode 100644 (file)
index 0000000..d26f66d
--- /dev/null
@@ -0,0 +1,207 @@
+package artist
+
+import (
+       "database/sql"
+       "encoding/json"
+       "git.jacobcasper.com/brackets/env"
+       "git.jacobcasper.com/brackets/routes"
+       "git.jacobcasper.com/brackets/types"
+       "github.com/zmb3/spotify"
+       "log"
+       "net/http"
+)
+
+func Index(env *env.Env) routes.Handler {
+       return func(w http.ResponseWriter, r *http.Request) {
+               if r.Method != "GET" {
+                       http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
+                       return
+               }
+
+               w.Header().Set("Content-Type", "application/json")
+
+               artistId := r.FormValue("id")
+               if artistId != "" {
+                       artist := types.Artist{}
+                       row := env.Db.Db.QueryRow(`
+SELECT ID, NAME, POPULARITY
+FROM ARTIST
+WHERE ID = ?`,
+                               artistId,
+                       )
+                       if err := row.Scan(&artist.ID, &artist.Name, &artist.Popularity); err != nil {
+                               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+                               return
+                       }
+                       b, err := json.Marshal(artist)
+                       if err != nil {
+                               log.Print(err)
+                               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+                               return
+                       }
+                       w.Write(b)
+                       return
+               }
+
+               rows, err := env.Db.Db.Query(`
+SELECT ID, NAME, POPULARITY
+FROM ARTIST
+LIMIT 20`,
+               )
+               if err != nil {
+                       http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+                       return
+               }
+               defer rows.Close()
+
+               artists := make([]types.Artist, 0)
+               for rows.Next() {
+                       artist := types.Artist{}
+                       if err := rows.Scan(&artist.ID, &artist.Name, &artist.Popularity); err != nil {
+                               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+                               return
+                       }
+                       artists = append(artists, artist)
+               }
+               if err = rows.Err(); err != nil {
+                       log.Print(err)
+                       http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+                       return
+               }
+               b, err := json.Marshal(artists)
+               if err != nil {
+                       log.Print(err)
+                       http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+                       return
+               }
+               w.Write(b)
+       }
+}
+
+func Add(env *env.Env) routes.Handler {
+       return func(w http.ResponseWriter, r *http.Request) {
+               if r.Method != "POST" {
+                       http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
+                       return
+               }
+
+               r.ParseForm()
+               artistId := r.PostForm.Get("id")
+
+               if artistId == "" {
+                       http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
+                       return
+               }
+
+               artist, err := env.C.GetArtist(spotify.ID(artistId))
+               if err != nil {
+                       log.Printf("Failed to retrieve artist %s: %s", artistId, err.Error())
+                       http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+                       return
+               }
+
+               env.Db.Mu.Lock()
+               defer env.Db.Mu.Unlock()
+               env.Db.Db.Exec(`
+INSERT INTO ARTIST
+(ID, NAME, POPULARITY)
+VALUES (?, ?, ?)`,
+                       artist.ID,
+                       artist.Name,
+                       artist.Popularity,
+               )
+
+               for _, genre := range artist.Genres {
+                       var genreId int64
+                       row := env.Db.Db.QueryRow(`
+SELECT ID
+FROM GENRE
+WHERE NAME = lower(?)
+`,
+                               genre,
+                       )
+
+                       err := row.Scan(&genreId)
+                       if err == sql.ErrNoRows {
+                               result, err := env.Db.Db.Exec(`
+INSERT INTO GENRE
+(NAME)
+VALUES (?)`,
+                                       genre,
+                               )
+                               if err != nil {
+                                       log.Printf("Failed to insert genre %s: %s", genre, err.Error())
+                                       http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+                                       return
+                               }
+
+                               genreId, err = result.LastInsertId()
+                               if err != nil {
+                                       log.Print("Failed to retrieve last insert id: ", err.Error())
+                                       http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+                                       return
+                               }
+                       }
+
+                       env.Db.Db.Exec(`
+INSERT INTO ARTIST_GENRE_XREF
+(ARTIST_ID, GENRE_ID)
+VALUES (?, ?)`,
+                               artist.ID,
+                               genreId,
+                       )
+               }
+               w.WriteHeader(http.StatusCreated)
+       }
+}
+
+func ByGenre(env *env.Env) routes.Handler {
+       return func(w http.ResponseWriter, r *http.Request) {
+               if r.Method != "GET" {
+                       http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
+                       return
+               }
+
+               w.Header().Set("Content-Type", "application/json")
+               genreName := r.FormValue("genre_name")
+               if genreName != "" {
+                       rows, err := env.Db.Db.Query(`
+SELECT a.ID, a.NAME
+FROM ARTIST a
+JOIN ARTIST_GENRE_XREF x ON a.ID = x.ARTIST_ID
+JOIN GENRE g ON g.ID = x.GENRE_ID
+WHERE g.NAME = lower(?)
+`,
+                               genreName,
+                       )
+                       if err != nil {
+                               log.Print(err)
+                               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+                               return
+                       }
+                       defer rows.Close()
+
+                       artists := make([]types.Artist, 0)
+                       for rows.Next() {
+                               artist := types.Artist{}
+                               if err := rows.Scan(&artist.ID, &artist.Name, &artist.Popularity); err != nil {
+                                       http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+                                       return
+                               }
+                               artists = append(artists, artist)
+                       }
+                       if err = rows.Err(); err != nil {
+                               log.Print(err)
+                               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+                               return
+                       }
+                       b, err := json.Marshal(artists)
+                       if err != nil {
+                               log.Print(err)
+                               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+                               return
+                       }
+                       w.Write(b)
+               }
+       }
+}
diff --git a/backend/routes/genre/genre.go b/backend/routes/genre/genre.go
new file mode 100644 (file)
index 0000000..06dfa8e
--- /dev/null
@@ -0,0 +1,77 @@
+package genre
+
+import (
+       "encoding/json"
+       "git.jacobcasper.com/brackets/env"
+       "git.jacobcasper.com/brackets/routes"
+       "git.jacobcasper.com/brackets/types"
+       "log"
+       "net/http"
+)
+
+func Index(env *env.Env) routes.Handler {
+       return func(w http.ResponseWriter, r *http.Request) {
+               if r.Method != "GET" {
+                       http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
+                       return
+               }
+
+               w.Header().Set("Content-Type", "application/json")
+
+               genreName := r.FormValue("name")
+               if genreName != "" {
+                       genre := types.Genre{}
+                       row := env.Db.Db.QueryRow(`
+SELECT ID, NAME
+FROM GENRE
+WHERE NAME = lower(?)`,
+                               genreName,
+                       )
+                       if err := row.Scan(&genre.ID, &genre.Name); err != nil {
+                               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+                               return
+                       }
+                       b, err := json.Marshal(genre)
+                       if err != nil {
+                               log.Print(err)
+                               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+                               return
+                       }
+                       w.Write(b)
+                       return
+               }
+
+               rows, err := env.Db.Db.Query(`
+SELECT ID, NAME
+FROM GENRE
+LIMIT 20`,
+               )
+               if err != nil {
+                       http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+                       return
+               }
+               defer rows.Close()
+
+               genres := make([]types.Genre, 0)
+               for rows.Next() {
+                       genre := types.Genre{}
+                       if err := rows.Scan(&genre.ID, &genre.Name); err != nil {
+                               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+                               return
+                       }
+                       genres = append(genres, genre)
+               }
+               if err = rows.Err(); err != nil {
+                       log.Print(err)
+                       http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+                       return
+               }
+               b, err := json.Marshal(genres)
+               if err != nil {
+                       log.Print(err)
+                       http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+                       return
+               }
+               w.Write(b)
+       }
+}
diff --git a/backend/routes/routes.go b/backend/routes/routes.go
new file mode 100644 (file)
index 0000000..2df045d
--- /dev/null
@@ -0,0 +1,7 @@
+package routes
+
+import (
+       "net/http"
+)
+
+type Handler func(http.ResponseWriter, *http.Request)
diff --git a/backend/scrape/graph/graph.go b/backend/scrape/graph/graph.go
new file mode 100644 (file)
index 0000000..cc1dc1e
--- /dev/null
@@ -0,0 +1,86 @@
+package graph
+
+import (
+       "git.jacobcasper.com/brackets/env"
+       "github.com/zmb3/spotify"
+       "log"
+       "net/http"
+       "net/url"
+       "time"
+)
+
+func Scrape(env *env.Env) {
+infinite:
+       for {
+               time.Sleep(time.Second * 5)
+               rows, err := env.Db.Db.Query(`
+SELECT ID
+FROM ARTIST
+WHERE ID NOT IN (
+  SELECT ARTIST_ID
+       FROM SCRAPED_ARTIST
+       WHERE SCRAPED == 1
+)`,
+               )
+               if err != nil {
+                       log.Print(err)
+                       continue infinite
+               }
+               defer rows.Close()
+
+               var artistId string
+               for rows.Next() {
+                       if err := rows.Scan(&artistId); err != nil {
+                               log.Print(err)
+                               continue infinite
+                       }
+
+                       artists, err := env.C.GetRelatedArtists(spotify.ID(artistId))
+                       if err != nil {
+                               log.Print(err)
+                               continue infinite
+                       }
+
+                       success := true
+               postArtists:
+                       for _, artist := range artists {
+                               row := env.Db.Db.QueryRow(`
+SELECT EXISTS (
+       SELECT 1
+       FROM ARTIST
+       WHERE ID = ?
+)
+`,
+                                       artist.ID,
+                               )
+                               var exists bool
+                               if err := row.Scan(&exists); err != nil {
+                                       // We don't care, this was a short circuit check
+                               }
+                               if exists {
+                                       continue postArtists
+                               }
+
+                               resp, err := http.PostForm("http://localhost:8080/artist/add", url.Values{"id": {string(artist.ID)}})
+                               if err != nil {
+                                       log.Print(err)
+                                       success = false
+                                       continue postArtists
+                               }
+                               if resp.StatusCode != http.StatusCreated {
+                                       success = false
+                               }
+                       }
+
+                       if success {
+                               env.Db.Mu.Lock()
+                               env.Db.Db.Exec(`
+REPLACE INTO SCRAPED_ARTIST (ARTIST_ID, SCRAPED)
+VALUES (?, 1)`,
+                                       string(artistId),
+                               )
+                               env.Db.Mu.Unlock()
+                       }
+               }
+       }
+}
diff --git a/backend/scrape/seed/seed.go b/backend/scrape/seed/seed.go
new file mode 100644 (file)
index 0000000..7425c91
--- /dev/null
@@ -0,0 +1,32 @@
+package main
+
+import (
+       "git.jacobcasper.com/brackets/client"
+       _ "github.com/mattn/go-sqlite3"
+       "log"
+       "net/http"
+       "net/url"
+)
+
+func main() {
+
+       client, err := client.Get()
+       if err != nil {
+               log.Fatal("Could not get client: ", err.Error())
+       }
+
+       _, page, err := client.FeaturedPlaylists()
+
+       for _, playlist := range page.Playlists {
+               tracks, err := client.GetPlaylistTracks(playlist.ID)
+               if err != nil {
+                       log.Printf("Couldn't retrieve playlist %s.", string(playlist.ID))
+                       continue
+               }
+               for _, trackPage := range tracks.Tracks {
+                       for _, artist := range trackPage.Track.Artists {
+                               http.PostForm("http://localhost:8080/artist/add", url.Values{"id": {string(artist.ID)}})
+                       }
+               }
+       }
+}
diff --git a/backend/types/artist.go b/backend/types/artist.go
new file mode 100644 (file)
index 0000000..07be6a8
--- /dev/null
@@ -0,0 +1,9 @@
+package types
+
+import "github.com/zmb3/spotify"
+
+type Artist struct {
+       ID         spotify.ID `json:"id"`
+       Name       string     `json:"name"`
+       Popularity int        `json:"popularity"`
+}
diff --git a/backend/types/genre.go b/backend/types/genre.go
new file mode 100644 (file)
index 0000000..708b48b
--- /dev/null
@@ -0,0 +1,6 @@
+package types
+
+type Genre struct {
+       ID   int    `json:"id"`
+       Name string `json:"name"`
+}
diff --git a/client/client.go b/client/client.go
deleted file mode 100644 (file)
index 394ee5f..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-package client
-
-import (
-       "context"
-       "fmt"
-       "github.com/zmb3/spotify"
-       "golang.org/x/oauth2/clientcredentials"
-       "os"
-)
-
-func Get() (*spotify.Client, error) {
-       config := &clientcredentials.Config{
-               ClientID:     os.Getenv("SPOTIFY_ID"),
-               ClientSecret: os.Getenv("SPOTIFY_SECRET"),
-               TokenURL:     spotify.TokenURL,
-       }
-
-       token, err := config.Token(context.Background())
-       if err != nil {
-               return nil, fmt.Errorf("client: %w", err)
-       }
-
-       client := spotify.Authenticator{}.NewClient(token)
-       client.AutoRetry = true
-       return &client, nil
-}
diff --git a/db/db.go b/db/db.go
deleted file mode 100644 (file)
index 3c04e68..0000000
--- a/db/db.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package db
-
-import (
-       "database/sql"
-       "sync"
-)
-
-// A DB that can be locked, as SQLite can't be concurrently written to.
-type DB struct {
-       Db *sql.DB
-       Mu *sync.Mutex
-}
-
-func New() (*DB, error) {
-       db, err := sql.Open("sqlite3", "brackets.sqlite?_journal_mode=WAL&_foreign_keys=on")
-       if err != nil {
-               return nil, err
-       }
-       return &DB{Db: db, Mu: &sync.Mutex{}}, err
-}
diff --git a/env/env.go b/env/env.go
deleted file mode 100644 (file)
index 9efbf2e..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-package env
-
-import (
-       "git.jacobcasper.com/brackets/client"
-       "git.jacobcasper.com/brackets/db"
-       "github.com/zmb3/spotify"
-)
-
-type Env struct {
-       Db *db.DB
-       C  *spotify.Client
-}
-
-func New() (*Env, error) {
-       db, err := db.New()
-       if err != nil {
-               return nil, err
-       }
-
-       client, err := client.Get()
-       if err != nil {
-               return nil, err
-       }
-       return &Env{Db: db, C: client}, nil
-}
diff --git a/go.mod b/go.mod
deleted file mode 100644 (file)
index a51656d..0000000
--- a/go.mod
+++ /dev/null
@@ -1,9 +0,0 @@
-module git.jacobcasper.com/brackets
-
-go 1.14
-
-require (
-       github.com/mattn/go-sqlite3 v2.0.3+incompatible
-       github.com/zmb3/spotify v0.0.0-20200413172747-49e8144a06f7
-       golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
-)
diff --git a/go.sum b/go.sum
deleted file mode 100644 (file)
index 5c58929..0000000
--- a/go.sum
+++ /dev/null
@@ -1,17 +0,0 @@
-cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
-github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
-github.com/zmb3/spotify v0.0.0-20200413172747-49e8144a06f7 h1:EsuM9MBGEmyCEQn2Tz4bO6IyUCgCovK3eZm9rrBJ9FE=
-github.com/zmb3/spotify v0.0.0-20200413172747-49e8144a06f7/go.mod h1:CYu0Uo+YYMlUX39zUTsCU9j3SpK3l1eB8oLykXF7R7w=
-golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg=
-golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
-golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
-google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
diff --git a/main.go b/main.go
deleted file mode 100644 (file)
index 702fd48..0000000
--- a/main.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package main
-
-import (
-       "git.jacobcasper.com/brackets/env"
-       "git.jacobcasper.com/brackets/routes/artist"
-       "git.jacobcasper.com/brackets/routes/genre"
-       "git.jacobcasper.com/brackets/scrape/graph"
-       _ "github.com/mattn/go-sqlite3"
-       "log"
-       "net/http"
-)
-
-func main() {
-       env, err := env.New()
-       if err != nil {
-               log.Fatal("Could not set up Env: ", err.Error())
-       }
-
-       http.HandleFunc("/artist/", artist.Index(env))
-       http.HandleFunc("/artist/genre", artist.ByGenre(env))
-       http.HandleFunc("/artist/add", artist.Add(env))
-
-       http.HandleFunc("/genre", genre.Index(env))
-
-       go graph.Scrape(env)
-
-       log.Fatal(http.ListenAndServe(":8080", nil))
-}
diff --git a/migrations/001-create_artist.sql b/migrations/001-create_artist.sql
deleted file mode 100644 (file)
index 7910b80..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-CREATE TABLE ARTIST (
-    ID TEXT PRIMARY KEY,
-    NAME TEXT 
-);
diff --git a/migrations/002-create_genre.sql b/migrations/002-create_genre.sql
deleted file mode 100644 (file)
index 6224609..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-CREATE TABLE GENRE (
-  ID INTEGER PRIMARY KEY AUTOINCREMENT,
-  NAME TEXT NOT NULL UNIQUE
-);
diff --git a/migrations/003-create_artist_genre_xref.sql b/migrations/003-create_artist_genre_xref.sql
deleted file mode 100644 (file)
index 25b9f42..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-CREATE TABLE ARTIST_GENRE_XREF (
-  ID INTEGER PRIMARY KEY AUTOINCREMENT,
-  ARTIST_ID NOT NULL,
-  GENRE_ID NOT NULL,
-  UNIQUE(ARTIST_ID, GENRE_ID),
-  FOREIGN KEY(ARTIST_ID) REFERENCES ARTIST(ID),
-  FOREIGN KEY(GENRE_ID) REFERENCES GENRE(ID)
-);
diff --git a/migrations/004-create_scraped_artist.sql b/migrations/004-create_scraped_artist.sql
deleted file mode 100644 (file)
index bc51487..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-CREATE TABLE SCRAPED_ARTIST (
-  ARTIST_ID TEXT UNIQUE NOT NULL,
-  SCRAPED BOOLEAN NOT NULL DEFAULT 0,
-  FOREIGN KEY(ARTIST_ID) REFERENCES ARTIST(ID)
-);
diff --git a/migrations/005-alter_artist_add_popularity.sql b/migrations/005-alter_artist_add_popularity.sql
deleted file mode 100644 (file)
index fdd4e49..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-ALTER TABLE ARTIST
-ADD COLUMN POPULARITY INT NOT NULL DEFAULT 0
-CHECK (POPULARITY >= 0 AND POPULARITY <= 100);
diff --git a/routes/artist/artist.go b/routes/artist/artist.go
deleted file mode 100644 (file)
index d26f66d..0000000
+++ /dev/null
@@ -1,207 +0,0 @@
-package artist
-
-import (
-       "database/sql"
-       "encoding/json"
-       "git.jacobcasper.com/brackets/env"
-       "git.jacobcasper.com/brackets/routes"
-       "git.jacobcasper.com/brackets/types"
-       "github.com/zmb3/spotify"
-       "log"
-       "net/http"
-)
-
-func Index(env *env.Env) routes.Handler {
-       return func(w http.ResponseWriter, r *http.Request) {
-               if r.Method != "GET" {
-                       http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
-                       return
-               }
-
-               w.Header().Set("Content-Type", "application/json")
-
-               artistId := r.FormValue("id")
-               if artistId != "" {
-                       artist := types.Artist{}
-                       row := env.Db.Db.QueryRow(`
-SELECT ID, NAME, POPULARITY
-FROM ARTIST
-WHERE ID = ?`,
-                               artistId,
-                       )
-                       if err := row.Scan(&artist.ID, &artist.Name, &artist.Popularity); err != nil {
-                               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-                               return
-                       }
-                       b, err := json.Marshal(artist)
-                       if err != nil {
-                               log.Print(err)
-                               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-                               return
-                       }
-                       w.Write(b)
-                       return
-               }
-
-               rows, err := env.Db.Db.Query(`
-SELECT ID, NAME, POPULARITY
-FROM ARTIST
-LIMIT 20`,
-               )
-               if err != nil {
-                       http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-                       return
-               }
-               defer rows.Close()
-
-               artists := make([]types.Artist, 0)
-               for rows.Next() {
-                       artist := types.Artist{}
-                       if err := rows.Scan(&artist.ID, &artist.Name, &artist.Popularity); err != nil {
-                               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-                               return
-                       }
-                       artists = append(artists, artist)
-               }
-               if err = rows.Err(); err != nil {
-                       log.Print(err)
-                       http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-                       return
-               }
-               b, err := json.Marshal(artists)
-               if err != nil {
-                       log.Print(err)
-                       http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-                       return
-               }
-               w.Write(b)
-       }
-}
-
-func Add(env *env.Env) routes.Handler {
-       return func(w http.ResponseWriter, r *http.Request) {
-               if r.Method != "POST" {
-                       http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
-                       return
-               }
-
-               r.ParseForm()
-               artistId := r.PostForm.Get("id")
-
-               if artistId == "" {
-                       http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
-                       return
-               }
-
-               artist, err := env.C.GetArtist(spotify.ID(artistId))
-               if err != nil {
-                       log.Printf("Failed to retrieve artist %s: %s", artistId, err.Error())
-                       http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-                       return
-               }
-
-               env.Db.Mu.Lock()
-               defer env.Db.Mu.Unlock()
-               env.Db.Db.Exec(`
-INSERT INTO ARTIST
-(ID, NAME, POPULARITY)
-VALUES (?, ?, ?)`,
-                       artist.ID,
-                       artist.Name,
-                       artist.Popularity,
-               )
-
-               for _, genre := range artist.Genres {
-                       var genreId int64
-                       row := env.Db.Db.QueryRow(`
-SELECT ID
-FROM GENRE
-WHERE NAME = lower(?)
-`,
-                               genre,
-                       )
-
-                       err := row.Scan(&genreId)
-                       if err == sql.ErrNoRows {
-                               result, err := env.Db.Db.Exec(`
-INSERT INTO GENRE
-(NAME)
-VALUES (?)`,
-                                       genre,
-                               )
-                               if err != nil {
-                                       log.Printf("Failed to insert genre %s: %s", genre, err.Error())
-                                       http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-                                       return
-                               }
-
-                               genreId, err = result.LastInsertId()
-                               if err != nil {
-                                       log.Print("Failed to retrieve last insert id: ", err.Error())
-                                       http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-                                       return
-                               }
-                       }
-
-                       env.Db.Db.Exec(`
-INSERT INTO ARTIST_GENRE_XREF
-(ARTIST_ID, GENRE_ID)
-VALUES (?, ?)`,
-                               artist.ID,
-                               genreId,
-                       )
-               }
-               w.WriteHeader(http.StatusCreated)
-       }
-}
-
-func ByGenre(env *env.Env) routes.Handler {
-       return func(w http.ResponseWriter, r *http.Request) {
-               if r.Method != "GET" {
-                       http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
-                       return
-               }
-
-               w.Header().Set("Content-Type", "application/json")
-               genreName := r.FormValue("genre_name")
-               if genreName != "" {
-                       rows, err := env.Db.Db.Query(`
-SELECT a.ID, a.NAME
-FROM ARTIST a
-JOIN ARTIST_GENRE_XREF x ON a.ID = x.ARTIST_ID
-JOIN GENRE g ON g.ID = x.GENRE_ID
-WHERE g.NAME = lower(?)
-`,
-                               genreName,
-                       )
-                       if err != nil {
-                               log.Print(err)
-                               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-                               return
-                       }
-                       defer rows.Close()
-
-                       artists := make([]types.Artist, 0)
-                       for rows.Next() {
-                               artist := types.Artist{}
-                               if err := rows.Scan(&artist.ID, &artist.Name, &artist.Popularity); err != nil {
-                                       http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-                                       return
-                               }
-                               artists = append(artists, artist)
-                       }
-                       if err = rows.Err(); err != nil {
-                               log.Print(err)
-                               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-                               return
-                       }
-                       b, err := json.Marshal(artists)
-                       if err != nil {
-                               log.Print(err)
-                               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-                               return
-                       }
-                       w.Write(b)
-               }
-       }
-}
diff --git a/routes/genre/genre.go b/routes/genre/genre.go
deleted file mode 100644 (file)
index 06dfa8e..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-package genre
-
-import (
-       "encoding/json"
-       "git.jacobcasper.com/brackets/env"
-       "git.jacobcasper.com/brackets/routes"
-       "git.jacobcasper.com/brackets/types"
-       "log"
-       "net/http"
-)
-
-func Index(env *env.Env) routes.Handler {
-       return func(w http.ResponseWriter, r *http.Request) {
-               if r.Method != "GET" {
-                       http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
-                       return
-               }
-
-               w.Header().Set("Content-Type", "application/json")
-
-               genreName := r.FormValue("name")
-               if genreName != "" {
-                       genre := types.Genre{}
-                       row := env.Db.Db.QueryRow(`
-SELECT ID, NAME
-FROM GENRE
-WHERE NAME = lower(?)`,
-                               genreName,
-                       )
-                       if err := row.Scan(&genre.ID, &genre.Name); err != nil {
-                               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-                               return
-                       }
-                       b, err := json.Marshal(genre)
-                       if err != nil {
-                               log.Print(err)
-                               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-                               return
-                       }
-                       w.Write(b)
-                       return
-               }
-
-               rows, err := env.Db.Db.Query(`
-SELECT ID, NAME
-FROM GENRE
-LIMIT 20`,
-               )
-               if err != nil {
-                       http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-                       return
-               }
-               defer rows.Close()
-
-               genres := make([]types.Genre, 0)
-               for rows.Next() {
-                       genre := types.Genre{}
-                       if err := rows.Scan(&genre.ID, &genre.Name); err != nil {
-                               http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-                               return
-                       }
-                       genres = append(genres, genre)
-               }
-               if err = rows.Err(); err != nil {
-                       log.Print(err)
-                       http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-                       return
-               }
-               b, err := json.Marshal(genres)
-               if err != nil {
-                       log.Print(err)
-                       http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-                       return
-               }
-               w.Write(b)
-       }
-}
diff --git a/routes/routes.go b/routes/routes.go
deleted file mode 100644 (file)
index 2df045d..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-package routes
-
-import (
-       "net/http"
-)
-
-type Handler func(http.ResponseWriter, *http.Request)
diff --git a/scrape/graph/graph.go b/scrape/graph/graph.go
deleted file mode 100644 (file)
index cc1dc1e..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-package graph
-
-import (
-       "git.jacobcasper.com/brackets/env"
-       "github.com/zmb3/spotify"
-       "log"
-       "net/http"
-       "net/url"
-       "time"
-)
-
-func Scrape(env *env.Env) {
-infinite:
-       for {
-               time.Sleep(time.Second * 5)
-               rows, err := env.Db.Db.Query(`
-SELECT ID
-FROM ARTIST
-WHERE ID NOT IN (
-  SELECT ARTIST_ID
-       FROM SCRAPED_ARTIST
-       WHERE SCRAPED == 1
-)`,
-               )
-               if err != nil {
-                       log.Print(err)
-                       continue infinite
-               }
-               defer rows.Close()
-
-               var artistId string
-               for rows.Next() {
-                       if err := rows.Scan(&artistId); err != nil {
-                               log.Print(err)
-                               continue infinite
-                       }
-
-                       artists, err := env.C.GetRelatedArtists(spotify.ID(artistId))
-                       if err != nil {
-                               log.Print(err)
-                               continue infinite
-                       }
-
-                       success := true
-               postArtists:
-                       for _, artist := range artists {
-                               row := env.Db.Db.QueryRow(`
-SELECT EXISTS (
-       SELECT 1
-       FROM ARTIST
-       WHERE ID = ?
-)
-`,
-                                       artist.ID,
-                               )
-                               var exists bool
-                               if err := row.Scan(&exists); err != nil {
-                                       // We don't care, this was a short circuit check
-                               }
-                               if exists {
-                                       continue postArtists
-                               }
-
-                               resp, err := http.PostForm("http://localhost:8080/artist/add", url.Values{"id": {string(artist.ID)}})
-                               if err != nil {
-                                       log.Print(err)
-                                       success = false
-                                       continue postArtists
-                               }
-                               if resp.StatusCode != http.StatusCreated {
-                                       success = false
-                               }
-                       }
-
-                       if success {
-                               env.Db.Mu.Lock()
-                               env.Db.Db.Exec(`
-REPLACE INTO SCRAPED_ARTIST (ARTIST_ID, SCRAPED)
-VALUES (?, 1)`,
-                                       string(artistId),
-                               )
-                               env.Db.Mu.Unlock()
-                       }
-               }
-       }
-}
diff --git a/scrape/seed/seed.go b/scrape/seed/seed.go
deleted file mode 100644 (file)
index 7425c91..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-package main
-
-import (
-       "git.jacobcasper.com/brackets/client"
-       _ "github.com/mattn/go-sqlite3"
-       "log"
-       "net/http"
-       "net/url"
-)
-
-func main() {
-
-       client, err := client.Get()
-       if err != nil {
-               log.Fatal("Could not get client: ", err.Error())
-       }
-
-       _, page, err := client.FeaturedPlaylists()
-
-       for _, playlist := range page.Playlists {
-               tracks, err := client.GetPlaylistTracks(playlist.ID)
-               if err != nil {
-                       log.Printf("Couldn't retrieve playlist %s.", string(playlist.ID))
-                       continue
-               }
-               for _, trackPage := range tracks.Tracks {
-                       for _, artist := range trackPage.Track.Artists {
-                               http.PostForm("http://localhost:8080/artist/add", url.Values{"id": {string(artist.ID)}})
-                       }
-               }
-       }
-}
diff --git a/types/artist.go b/types/artist.go
deleted file mode 100644 (file)
index 07be6a8..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-package types
-
-import "github.com/zmb3/spotify"
-
-type Artist struct {
-       ID         spotify.ID `json:"id"`
-       Name       string     `json:"name"`
-       Popularity int        `json:"popularity"`
-}
diff --git a/types/genre.go b/types/genre.go
deleted file mode 100644 (file)
index 708b48b..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-package types
-
-type Genre struct {
-       ID   int    `json:"id"`
-       Name string `json:"name"`
-}