From: Jacob Casper Date: Fri, 17 Apr 2020 02:18:49 +0000 (-0500) Subject: Move backend to subdir X-Git-Url: https://git.jacobcasper.com/?p=brackets.git;a=commitdiff_plain;h=06f9ce8382c67ccf3b39214377f59f45102e54b1 Move backend to subdir --- diff --git a/.gitignore b/.gitignore index f166cd5..f6cbb54 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ brackets* -*/seed/seed +backend/scrape/seed/seed secrets.sh vendor/ diff --git a/backend/client/client.go b/backend/client/client.go new file mode 100644 index 0000000..394ee5f --- /dev/null +++ b/backend/client/client.go @@ -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 index 0000000..3c04e68 --- /dev/null +++ b/backend/db/db.go @@ -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 index 0000000..9efbf2e --- /dev/null +++ b/backend/env/env.go @@ -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 index 0000000..a51656d --- /dev/null +++ b/backend/go.mod @@ -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 index 0000000..5c58929 --- /dev/null +++ b/backend/go.sum @@ -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 index 0000000..702fd48 --- /dev/null +++ b/backend/main.go @@ -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 index 0000000..7910b80 --- /dev/null +++ b/backend/migrations/001-create_artist.sql @@ -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 index 0000000..6224609 --- /dev/null +++ b/backend/migrations/002-create_genre.sql @@ -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 index 0000000..25b9f42 --- /dev/null +++ b/backend/migrations/003-create_artist_genre_xref.sql @@ -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 index 0000000..bc51487 --- /dev/null +++ b/backend/migrations/004-create_scraped_artist.sql @@ -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 index 0000000..fdd4e49 --- /dev/null +++ b/backend/migrations/005-alter_artist_add_popularity.sql @@ -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 index 0000000..d26f66d --- /dev/null +++ b/backend/routes/artist/artist.go @@ -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 index 0000000..06dfa8e --- /dev/null +++ b/backend/routes/genre/genre.go @@ -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 index 0000000..2df045d --- /dev/null +++ b/backend/routes/routes.go @@ -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 index 0000000..cc1dc1e --- /dev/null +++ b/backend/scrape/graph/graph.go @@ -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 index 0000000..7425c91 --- /dev/null +++ b/backend/scrape/seed/seed.go @@ -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 index 0000000..07be6a8 --- /dev/null +++ b/backend/types/artist.go @@ -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 index 0000000..708b48b --- /dev/null +++ b/backend/types/genre.go @@ -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 index 394ee5f..0000000 --- a/client/client.go +++ /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 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 index 9efbf2e..0000000 --- a/env/env.go +++ /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 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 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 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 index 7910b80..0000000 --- a/migrations/001-create_artist.sql +++ /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 index 6224609..0000000 --- a/migrations/002-create_genre.sql +++ /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 index 25b9f42..0000000 --- a/migrations/003-create_artist_genre_xref.sql +++ /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 index bc51487..0000000 --- a/migrations/004-create_scraped_artist.sql +++ /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 index fdd4e49..0000000 --- a/migrations/005-alter_artist_add_popularity.sql +++ /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 index d26f66d..0000000 --- a/routes/artist/artist.go +++ /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 index 06dfa8e..0000000 --- a/routes/genre/genre.go +++ /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 index 2df045d..0000000 --- a/routes/routes.go +++ /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 index cc1dc1e..0000000 --- a/scrape/graph/graph.go +++ /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 index 7425c91..0000000 --- a/scrape/seed/seed.go +++ /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 index 07be6a8..0000000 --- a/types/artist.go +++ /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 index 708b48b..0000000 --- a/types/genre.go +++ /dev/null @@ -1,6 +0,0 @@ -package types - -type Genre struct { - ID int `json:"id"` - Name string `json:"name"` -}