brackets*
-*/seed/seed
+backend/scrape/seed/seed
secrets.sh
vendor/
--- /dev/null
+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
+}
--- /dev/null
+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
+}
--- /dev/null
+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
+}
--- /dev/null
+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
+)
--- /dev/null
+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=
--- /dev/null
+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))
+}
--- /dev/null
+CREATE TABLE ARTIST (
+ ID TEXT PRIMARY KEY,
+ NAME TEXT
+);
--- /dev/null
+CREATE TABLE GENRE (
+ ID INTEGER PRIMARY KEY AUTOINCREMENT,
+ NAME TEXT NOT NULL UNIQUE
+);
--- /dev/null
+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)
+);
--- /dev/null
+CREATE TABLE SCRAPED_ARTIST (
+ ARTIST_ID TEXT UNIQUE NOT NULL,
+ SCRAPED BOOLEAN NOT NULL DEFAULT 0,
+ FOREIGN KEY(ARTIST_ID) REFERENCES ARTIST(ID)
+);
--- /dev/null
+ALTER TABLE ARTIST
+ADD COLUMN POPULARITY INT NOT NULL DEFAULT 0
+CHECK (POPULARITY >= 0 AND POPULARITY <= 100);
--- /dev/null
+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)
+ }
+ }
+}
--- /dev/null
+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)
+ }
+}
--- /dev/null
+package routes
+
+import (
+ "net/http"
+)
+
+type Handler func(http.ResponseWriter, *http.Request)
--- /dev/null
+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()
+ }
+ }
+ }
+}
--- /dev/null
+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)}})
+ }
+ }
+ }
+}
--- /dev/null
+package types
+
+import "github.com/zmb3/spotify"
+
+type Artist struct {
+ ID spotify.ID `json:"id"`
+ Name string `json:"name"`
+ Popularity int `json:"popularity"`
+}
--- /dev/null
+package types
+
+type Genre struct {
+ ID int `json:"id"`
+ Name string `json:"name"`
+}
+++ /dev/null
-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
-}
+++ /dev/null
-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
-}
+++ /dev/null
-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
-}
+++ /dev/null
-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
-)
+++ /dev/null
-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=
+++ /dev/null
-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))
-}
+++ /dev/null
-CREATE TABLE ARTIST (
- ID TEXT PRIMARY KEY,
- NAME TEXT
-);
+++ /dev/null
-CREATE TABLE GENRE (
- ID INTEGER PRIMARY KEY AUTOINCREMENT,
- NAME TEXT NOT NULL UNIQUE
-);
+++ /dev/null
-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)
-);
+++ /dev/null
-CREATE TABLE SCRAPED_ARTIST (
- ARTIST_ID TEXT UNIQUE NOT NULL,
- SCRAPED BOOLEAN NOT NULL DEFAULT 0,
- FOREIGN KEY(ARTIST_ID) REFERENCES ARTIST(ID)
-);
+++ /dev/null
-ALTER TABLE ARTIST
-ADD COLUMN POPULARITY INT NOT NULL DEFAULT 0
-CHECK (POPULARITY >= 0 AND POPULARITY <= 100);
+++ /dev/null
-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)
- }
- }
-}
+++ /dev/null
-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)
- }
-}
+++ /dev/null
-package routes
-
-import (
- "net/http"
-)
-
-type Handler func(http.ResponseWriter, *http.Request)
+++ /dev/null
-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()
- }
- }
- }
-}
+++ /dev/null
-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)}})
- }
- }
- }
-}
+++ /dev/null
-package types
-
-import "github.com/zmb3/spotify"
-
-type Artist struct {
- ID spotify.ID `json:"id"`
- Name string `json:"name"`
- Popularity int `json:"popularity"`
-}
+++ /dev/null
-package types
-
-type Genre struct {
- ID int `json:"id"`
- Name string `json:"name"`
-}