diff --git a/.env.example b/.env.example
index 653ef4d4..aa3cbdb0 100644
--- a/.env.example
+++ b/.env.example
@@ -21,3 +21,4 @@ MAIL_FROM_ADDRESS=
MAIL_FROM_NAME=
GOSCROBBLE_DOMAIN=""
+STATIC_DIR="web"
diff --git a/.gitignore b/.gitignore
index eda2b7ed..88083e5e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,8 @@
web/.env.production
web/.env.development
+web/img/*
+
# Test binary, built with `go test -c`
*.test
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 5ed95d39..3300a85c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -3,7 +3,7 @@ stages:
- bundle
variables:
- VERSION: 0.0.28
+ VERSION: 0.0.29
build-go:
image: golang:1.16.2
diff --git a/cmd/go-scrobble/main.go b/cmd/go-scrobble/main.go
index 801c98fd..d7dfcf0b 100644
--- a/cmd/go-scrobble/main.go
+++ b/cmd/go-scrobble/main.go
@@ -51,6 +51,12 @@ func main() {
goscrobble.RefereshExpiry = time.Duration(i) * time.Second
}
+ goscrobble.StaticDirectory = "web"
+ staticDirectoryStr := os.Getenv("STATIC_DIR")
+ if staticDirectoryStr != "" {
+ goscrobble.StaticDirectory = staticDirectoryStr
+ }
+
// Ignore reverse proxies
goscrobble.ReverseProxies = strings.Split(os.Getenv("REVERSE_PROXIES"), ",")
diff --git a/docs/changelog.md b/docs/changelog.md
index 6d498664..c0c967a9 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,3 +1,7 @@
+# 0.0.29
+- Add image handler
+- Store images locally
+
# 0.0.28
- Fix mobile view on user pages
- Fix favicon issue
diff --git a/docs/config.md b/docs/config.md
index d61d7670..ac9b09cc 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -31,3 +31,4 @@ These are stored in `web/.env.production` and `web/.env.development`
MAIL_FROM_NAME= // FROM name
GOSCROBBLE_DOMAIN="" // Full domain for email links (https://goscrobble.com))
+ STATIC_DIR="web" // Location to store images (This will serve from web/static)
diff --git a/internal/goscrobble/artist.go b/internal/goscrobble/artist.go
index 136befc2..9b82feb4 100644
--- a/internal/goscrobble/artist.go
+++ b/internal/goscrobble/artist.go
@@ -129,7 +129,7 @@ func getArtistByUUID(uuid string) (Artist, error) {
func getTopArtists(userUuid string) (TopArtists, error) {
var topArtist TopArtists
- rows, err := db.Query("SELECT BIN_TO_UUID(`artists`.`uuid`, true), `artists`.`name`, IFNULL(`artists`.`img`,''), count(*) "+
+ rows, err := db.Query("SELECT BIN_TO_UUID(`artists`.`uuid`, true), `artists`.`name`, IFNULL(BIN_TO_UUID(`artists`.`uuid`, true),''), count(*) "+
"FROM `scrobbles` "+
"JOIN `tracks` ON `tracks`.`uuid` = `scrobbles`.`track` "+
"JOIN track_artist ON track_artist.track = tracks.uuid "+
diff --git a/internal/goscrobble/image.go b/internal/goscrobble/image.go
new file mode 100644
index 00000000..369cb9eb
--- /dev/null
+++ b/internal/goscrobble/image.go
@@ -0,0 +1,46 @@
+package goscrobble
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+)
+
+func importImage(uuid string, url string) error {
+ // Create the file
+ path, err := os.Getwd()
+ if err != nil {
+ return err
+ }
+
+ out, err := os.Create(path + string(os.PathSeparator) + StaticDirectory + string(os.PathSeparator) + "img" + string(os.PathSeparator) + uuid + "_full.jpg")
+ if err != nil {
+ return err
+ }
+ defer out.Close()
+
+ // Get the data
+ resp, err := http.Get(url)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ // Check server response
+ if resp.StatusCode != http.StatusOK {
+ return fmt.Errorf("Bad response status: %s", resp.Status)
+ }
+
+ // Writer the body to file
+ _, err = io.Copy(out, resp.Body)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func resizeImage(uuid string) error {
+ return nil
+}
diff --git a/internal/goscrobble/ingress_navidrome.go b/internal/goscrobble/ingress_navidrome.go
index 26583c8d..f928ef30 100644
--- a/internal/goscrobble/ingress_navidrome.go
+++ b/internal/goscrobble/ingress_navidrome.go
@@ -84,7 +84,6 @@ func (user *User) updateNavidromePlaydata(tx *sql.Tx) error {
}
response, err := getNavidromeNowPlaying(&tokens)
- fmt.Println(response)
if err != nil {
return errors.New(fmt.Sprintf("Failed to fetch Navidrome Tokens %+v", err))
}
diff --git a/internal/goscrobble/ingress_spotify.go b/internal/goscrobble/ingress_spotify.go
index ec190635..0f938d41 100644
--- a/internal/goscrobble/ingress_spotify.go
+++ b/internal/goscrobble/ingress_spotify.go
@@ -238,7 +238,7 @@ func (user *User) updateImageDataFromSpotify() error {
client := auth.NewClient(token)
client.AutoRetry = true
- rows, err := db.Query("SELECT BIN_TO_UUID(`uuid`, true), `name` FROM `artists` WHERE IFNULL(`img`,'') = '' LIMIT 50")
+ rows, err := db.Query("SELECT BIN_TO_UUID(`uuid`, true), `name` FROM `artists` WHERE IFNULL(`img`,'') NOT IN ('pending', 'complete') LIMIT 50")
if err != nil {
log.Printf("Failed to fetch config: %+v", err)
return errors.New("Failed to fetch artists")
@@ -271,11 +271,18 @@ func (user *User) updateImageDataFromSpotify() error {
if err != nil {
continue
}
- _ = artist.updateArtist("img", img, tx)
+
+ err = importImage(uuid, img)
+ if err != nil {
+ fmt.Printf("Failed to import image: %+v", err)
+ continue
+ }
+
+ _ = artist.updateArtist("img", "pending", tx)
}
tx.Commit()
- rows, err = db.Query("SELECT BIN_TO_UUID(`uuid`, true), `name` FROM `albums` WHERE IFNULL(`img`,'') = '' LIMIT 50")
+ rows, err = db.Query("SELECT BIN_TO_UUID(`uuid`, true), `name` FROM `albums` WHERE IFNULL(`img`,'') NOT IN ('pending', 'complete') LIMIT 50")
if err != nil {
log.Printf("Failed to fetch config: %+v", err)
return errors.New("Failed to fetch artists")
@@ -305,10 +312,12 @@ func (user *User) updateImageDataFromSpotify() error {
tx, _ = db.Begin()
for uuid, img := range toUpdate {
album, err = getAlbumByUUID(uuid)
+ err = importImage(uuid, img)
+
if err != nil {
continue
}
- _ = album.updateAlbum("img", img, tx)
+ _ = album.updateAlbum("img", "pending", tx)
}
tx.Commit()
return nil
diff --git a/internal/goscrobble/server.go b/internal/goscrobble/server.go
index 61c64d44..4716e04b 100644
--- a/internal/goscrobble/server.go
+++ b/internal/goscrobble/server.go
@@ -22,6 +22,9 @@ type jsonResponse struct {
// List of Reverse proxies
var ReverseProxies []string
+// Static image directory
+var StaticDirectory string
+
// RequestRequest - Incoming JSON!
type RequestRequest struct {
URL string `json:"url"`
@@ -88,6 +91,10 @@ func HandleRequests(port string) {
// This just prevents it serving frontend stuff over /api
r.PathPrefix("/api")
+ // SERVE STATIC FILES - NO AUTH
+ spaStatic := spaStaticHandler{staticPath: StaticDirectory}
+ r.PathPrefix("/img").Handler(spaStatic)
+
// SERVE FRONTEND - NO AUTH
spa := spaHandler{staticPath: "web/build", indexPath: "index.html"}
r.PathPrefix("/").Handler(spa)
@@ -733,7 +740,7 @@ func getServerInfo(w http.ResponseWriter, r *http.Request) {
}
info := ServerInfo{
- Version: "0.0.28",
+ Version: "0.0.29",
RegistrationEnabled: cachedRegistrationEnabled,
}
diff --git a/internal/goscrobble/server_static.go b/internal/goscrobble/server_static.go
index 7e404244..f420d03f 100644
--- a/internal/goscrobble/server_static.go
+++ b/internal/goscrobble/server_static.go
@@ -6,12 +6,43 @@ import (
"path/filepath"
)
+// spaStaticHandler - Handles static imges
+type spaStaticHandler struct {
+ staticPath string
+ indexPath string
+}
+
// spaHandler - Handles Single Page Applications (React)
type spaHandler struct {
staticPath string
indexPath string
}
+// ServerHTTP - Frontend React server
+func (h spaStaticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ // Get the absolute path to prevent directory traversal
+ path, err := filepath.Abs(r.URL.Path)
+ if err != nil {
+ http.ServeFile(w, r, filepath.Join(h.staticPath, "img/placeholder.jpg"))
+ return
+ }
+
+ path = filepath.Join(h.staticPath, path)
+
+ _, err = os.Stat(path)
+ if os.IsNotExist(err) {
+ // file does not exist, serve placeholder
+ http.ServeFile(w, r, filepath.Join(h.staticPath, "img/placeholder.jpg"))
+ return
+ } else if err != nil {
+ http.ServeFile(w, r, filepath.Join(h.staticPath, "img/placeholder.jpg"))
+ return
+ }
+
+ // otherwise, use http.FileServer to serve the static images
+ http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r)
+}
+
// ServerHTTP - Frontend React server
func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Get the absolute path to prevent directory traversal
diff --git a/internal/goscrobble/timers.go b/internal/goscrobble/timers.go
index 070e2bcb..90b9c267 100644
--- a/internal/goscrobble/timers.go
+++ b/internal/goscrobble/timers.go
@@ -8,6 +8,9 @@ import (
var endTicker chan bool
func StartBackgroundWorkers() {
+ user, _ := getUserByUsername("idanoo")
+ go user.updateImageDataFromSpotify()
+
endTicker := make(chan bool)
hourTicker := time.NewTicker(time.Duration(1) * time.Hour)
diff --git a/internal/goscrobble/track.go b/internal/goscrobble/track.go
index 75487891..85a12838 100644
--- a/internal/goscrobble/track.go
+++ b/internal/goscrobble/track.go
@@ -166,7 +166,7 @@ func (track *Track) updateTrack(col string, val string, tx *sql.Tx) error {
func getTrackByUUID(uuid string) (Track, error) {
var track Track
- err := db.QueryRow("SELECT BIN_TO_UUID(`tracks`.`uuid`, true), `tracks`.`name`, IFNULL(`albums`.`desc`,''), IFNULL(`albums`.`img`,''), `tracks`.`length`, `tracks`.`mbid`, `tracks`.`spotify_id` "+
+ err := db.QueryRow("SELECT BIN_TO_UUID(`tracks`.`uuid`, true), `tracks`.`name`, IFNULL(`albums`.`desc`,''), IFNULL(BIN_TO_UUID(`albums`.`uuid`, true),''), `tracks`.`length`, `tracks`.`mbid`, `tracks`.`spotify_id` "+
"FROM `tracks` "+
"LEFT JOIN track_album ON track_album.track = tracks.uuid "+
"LEFT JOIN albums ON track_album.album = albums.uuid "+
@@ -184,7 +184,7 @@ func getTrackByUUID(uuid string) (Track, error) {
func getTopTracks(userUuid string) (TopTracks, error) {
var topTracks TopTracks
- rows, err := db.Query("SELECT BIN_TO_UUID(`tracks`.`uuid`, true), `tracks`.`name`, IFNULL(`albums`.`img`,''), count(*) "+
+ rows, err := db.Query("SELECT BIN_TO_UUID(`tracks`.`uuid`, true), `tracks`.`name`, IFNULL(BIN_TO_UUID(`albums`.`uuid`, true),''), count(*) "+
"FROM `scrobbles` "+
"JOIN `tracks` ON `tracks`.`uuid` = `scrobbles`.`track` "+
"JOIN track_album ON track_album.track = tracks.uuid "+
diff --git a/web/.env.example b/web/.env.example
index fbedc8f2..3150c75a 100644
--- a/web/.env.example
+++ b/web/.env.example
@@ -1 +1 @@
-REACT_APP_API_URL=http://127.0.0.1:42069/api/v1/
+REACT_APP_API_URL=http://127.0.0.1:42069
diff --git a/web/img/placeholder.jpg b/web/img/placeholder.jpg
new file mode 100644
index 00000000..ff8ff816
Binary files /dev/null and b/web/img/placeholder.jpg differ
diff --git a/web/src/Api/index.js b/web/src/Api/index.js
index c4c85169..3114a660 100644
--- a/web/src/Api/index.js
+++ b/web/src/Api/index.js
@@ -49,7 +49,7 @@ function handleErrorResp(error) {
}
export const PostLogin = (formValues) => {
- return axios.post(process.env.REACT_APP_API_URL + "login", formValues)
+ return axios.post(process.env.REACT_APP_API_URL + "/api/v1/login", formValues)
.then((response) => {
if (response.data.token) {
let expandedUser = jwt(response.data.token)
@@ -82,7 +82,7 @@ export const PostLogin = (formValues) => {
};
export const PostRefreshToken = (refreshToken) => {
- return axios.post(process.env.REACT_APP_API_URL + "refresh", {token: refreshToken})
+ return axios.post(process.env.REACT_APP_API_URL + "/api/v1/refresh", {token: refreshToken})
.then((response) => {
if (response.data.token) {
let expandedUser = jwt(response.data.token)
@@ -115,7 +115,7 @@ export const PostRefreshToken = (refreshToken) => {
export const PostRegister = (formValues) => {
- return axios.post(process.env.REACT_APP_API_URL + "register", formValues)
+ return axios.post(process.env.REACT_APP_API_URL + "/api/v1/register", formValues)
.then((response) => {
if (response.data.message) {
toast.success(response.data.message);
@@ -133,7 +133,7 @@ export const PostRegister = (formValues) => {
};
export const PostResetPassword = (formValues) => {
- return axios.post(process.env.REACT_APP_API_URL + "resetpassword", formValues)
+ return axios.post(process.env.REACT_APP_API_URL + "/api/v1/resetpassword", formValues)
.then((response) => {
if (response.data.message) {
toast.success(response.data.message);
@@ -151,7 +151,7 @@ export const PostResetPassword = (formValues) => {
};
export const sendPasswordReset = (values) => {
- return axios.post(process.env.REACT_APP_API_URL + "sendreset", values).then(
+ return axios.post(process.env.REACT_APP_API_URL + "/api/v1/sendreset", values).then(
(data) => {
return data.data;
}).catch((error) => {
@@ -160,7 +160,7 @@ export const sendPasswordReset = (values) => {
};
export const getStats = () => {
- return axios.get(process.env.REACT_APP_API_URL + "stats").then(
+ return axios.get(process.env.REACT_APP_API_URL + "/api/v1/stats").then(
(data) => {
return data.data;
}).catch((error) => {
@@ -169,7 +169,7 @@ export const getStats = () => {
};
export const getRecentScrobbles = (id) => {
- return axios.get(process.env.REACT_APP_API_URL + "user/" + id + "/scrobbles", { headers: getHeaders() })
+ return axios.get(process.env.REACT_APP_API_URL + "/api/v1/user/" + id + "/scrobbles", { headers: getHeaders() })
.then((data) => {
return data.data;
}).catch((error) => {
@@ -178,7 +178,7 @@ export const getRecentScrobbles = (id) => {
};
export const getConfigs = () => {
- return axios.get(process.env.REACT_APP_API_URL + "config", { headers: getHeaders() })
+ return axios.get(process.env.REACT_APP_API_URL + "/api/v1/config", { headers: getHeaders() })
.then((data) => {
return data.data;
}).catch((error) => {
@@ -193,7 +193,7 @@ export const postConfigs = (values, toggle) => {
values.REGISTRATION_ENABLED = "0"
}
- return axios.post(process.env.REACT_APP_API_URL + "config", values, { headers: getHeaders() })
+ return axios.post(process.env.REACT_APP_API_URL + "/api/v1/config", values, { headers: getHeaders() })
.then((data) => {
if (data.data && data.data.message) {
toast.success(data.data.message);
@@ -206,7 +206,7 @@ export const postConfigs = (values, toggle) => {
};
export const getProfile = (userName) => {
- return axios.get(process.env.REACT_APP_API_URL + "profile/" + userName, { headers: getHeaders() })
+ return axios.get(process.env.REACT_APP_API_URL + "/api/v1/profile/" + userName, { headers: getHeaders() })
.then((data) => {
return data.data;
}).catch((error) => {
@@ -215,7 +215,7 @@ export const getProfile = (userName) => {
};
export const getUser = () => {
- return axios.get(process.env.REACT_APP_API_URL + "user", { headers: getHeaders() })
+ return axios.get(process.env.REACT_APP_API_URL + "/api/v1/user", { headers: getHeaders() })
.then((data) => {
return data.data;
}).catch((error) => {
@@ -224,7 +224,7 @@ export const getUser = () => {
};
export const patchUser = (values) => {
- return axios.patch(process.env.REACT_APP_API_URL + "user", values, { headers: getHeaders() })
+ return axios.patch(process.env.REACT_APP_API_URL + "/api/v1/user", values, { headers: getHeaders() })
.then((data) => {
return data.data;
}).catch((error) => {
@@ -233,7 +233,7 @@ export const patchUser = (values) => {
};
export const validateResetPassword = (tokenStr) => {
- return axios.get(process.env.REACT_APP_API_URL + "user/", { headers: getHeaders() })
+ return axios.get(process.env.REACT_APP_API_URL + "/api/v1/user/", { headers: getHeaders() })
.then((data) => {
return data.data;
}).catch((error) => {
@@ -242,7 +242,7 @@ export const validateResetPassword = (tokenStr) => {
};
export const getSpotifyClientId = () => {
- return axios.get(process.env.REACT_APP_API_URL + "user/spotify", { headers: getHeaders() })
+ return axios.get(process.env.REACT_APP_API_URL + "/api/v1/user/spotify", { headers: getHeaders() })
.then((data) => {
return data.data
}).catch((error) => {
@@ -272,7 +272,7 @@ export const spotifyConnectionRequest = () => {
};
export const spotifyDisonnectionRequest = () => {
- return axios.delete(process.env.REACT_APP_API_URL + "user/spotify", { headers: getHeaders() })
+ return axios.delete(process.env.REACT_APP_API_URL + "/api/v1/user/spotify", { headers: getHeaders() })
.then((data) => {
toast.success(data.data.message);
return true
@@ -282,7 +282,7 @@ export const spotifyDisonnectionRequest = () => {
}
export const navidromeConnectionRequest = (values) => {
- return axios.post(process.env.REACT_APP_API_URL + "user/navidrome", values, { headers: getHeaders() })
+ return axios.post(process.env.REACT_APP_API_URL + "/api/v1/user/navidrome", values, { headers: getHeaders() })
.then((data) => {
toast.success(data.data.message);
return true
@@ -292,7 +292,7 @@ export const navidromeConnectionRequest = (values) => {
};
export const navidromeDisonnectionRequest = () => {
- return axios.delete(process.env.REACT_APP_API_URL + "user/navidrome", { headers: getHeaders() })
+ return axios.delete(process.env.REACT_APP_API_URL + "/api/v1/user/navidrome", { headers: getHeaders() })
.then((data) => {
toast.success(data.data.message);
return true
@@ -303,7 +303,7 @@ export const navidromeDisonnectionRequest = () => {
export const getServerInfo = () => {
- return axios.get(process.env.REACT_APP_API_URL + "serverinfo")
+ return axios.get(process.env.REACT_APP_API_URL + "/api/v1/serverinfo")
.then((data) => {
return data.data
}).catch((error) => {
@@ -312,7 +312,7 @@ export const getServerInfo = () => {
}
export const getArtist = (uuid) => {
- return axios.get(process.env.REACT_APP_API_URL + "artists/" + uuid).then(
+ return axios.get(process.env.REACT_APP_API_URL + "/api/v1/artists/" + uuid).then(
(data) => {
return data.data;
}).catch((error) => {
@@ -321,7 +321,7 @@ export const getArtist = (uuid) => {
};
export const getAlbum = (uuid) => {
- return axios.get(process.env.REACT_APP_API_URL + "albums/" + uuid).then(
+ return axios.get(process.env.REACT_APP_API_URL + "/api/v1/albums/" + uuid).then(
(data) => {
return data.data;
}).catch((error) => {
@@ -330,7 +330,7 @@ export const getAlbum = (uuid) => {
};
export const getTrack = (uuid) => {
- return axios.get(process.env.REACT_APP_API_URL + "tracks/" + uuid).then(
+ return axios.get(process.env.REACT_APP_API_URL + "/api/v1/tracks/" + uuid).then(
(data) => {
return data.data;
}).catch((error) => {
@@ -339,7 +339,7 @@ export const getTrack = (uuid) => {
};
export const getTopTracks = (uuid) => {
- return axios.get(process.env.REACT_APP_API_URL + "tracks/top/" + uuid).then(
+ return axios.get(process.env.REACT_APP_API_URL + "/api/v1/tracks/top/" + uuid).then(
(data) => {
return data.data;
}).catch((error) => {
@@ -348,7 +348,7 @@ export const getTopTracks = (uuid) => {
}
export const getTopArtists = (uuid) => {
- return axios.get(process.env.REACT_APP_API_URL + "artists/top/" + uuid).then(
+ return axios.get(process.env.REACT_APP_API_URL + "/api/v1/artists/top/" + uuid).then(
(data) => {
return data.data;
}).catch((error) => {
diff --git a/web/src/Components/TopTable.js b/web/src/Components/TopTable.js
index b44a2891..abc64f1a 100644
--- a/web/src/Components/TopTable.js
+++ b/web/src/Components/TopTable.js
@@ -21,7 +21,7 @@ const TopTable = (props) => {
number="1"
title={tracks[1].name}
link={"/" + props.type + "/" + tracks[1].uuid}
- img={tracks[1].img}
+ uuid={tracks[1].img}
/>
{ Object.keys(props.items).length > 5 &&
@@ -31,28 +31,28 @@ const TopTable = (props) => {
number="2"
title={tracks[2].name}
link={"/" + props.type + "/" + tracks[2].uuid}
- img={tracks[2].img}
+ uuid={tracks[2].img}
/>