mirror of
https://github.com/idanoo/GoScrobble.git
synced 2024-11-25 01:45:15 +00:00
0.0.32 Flesh out track page
This commit is contained in:
parent
d7b7ceb122
commit
9074149925
@ -20,5 +20,6 @@ SENDGRID_API_KEY=
|
|||||||
MAIL_FROM_ADDRESS=
|
MAIL_FROM_ADDRESS=
|
||||||
MAIL_FROM_NAME=
|
MAIL_FROM_NAME=
|
||||||
|
|
||||||
|
DEV_MODE=false
|
||||||
GOSCROBBLE_DOMAIN=""
|
GOSCROBBLE_DOMAIN=""
|
||||||
STATIC_DIR="web"
|
STATIC_DIR="web"
|
||||||
|
@ -3,10 +3,10 @@ stages:
|
|||||||
- bundle
|
- bundle
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
VERSION: 0.0.31
|
VERSION: 0.0.32
|
||||||
|
|
||||||
build-go:
|
build-go:
|
||||||
image: golang:1.16.2
|
image: golang:1.16.7
|
||||||
stage: build
|
stage: build
|
||||||
only:
|
only:
|
||||||
- master
|
- master
|
||||||
|
@ -60,6 +60,12 @@ func main() {
|
|||||||
// Ignore reverse proxies
|
// Ignore reverse proxies
|
||||||
goscrobble.ReverseProxies = strings.Split(os.Getenv("REVERSE_PROXIES"), ",")
|
goscrobble.ReverseProxies = strings.Split(os.Getenv("REVERSE_PROXIES"), ",")
|
||||||
|
|
||||||
|
goscrobble.DevMode = false
|
||||||
|
devModeString := os.Getenv("DEV_MODE")
|
||||||
|
if strings.ToLower(devModeString) == "true" {
|
||||||
|
goscrobble.DevMode = true
|
||||||
|
}
|
||||||
|
|
||||||
// Store port
|
// Store port
|
||||||
port := os.Getenv("PORT")
|
port := os.Getenv("PORT")
|
||||||
if port == "" {
|
if port == "" {
|
||||||
@ -74,9 +80,14 @@ func main() {
|
|||||||
goscrobble.InitRedis()
|
goscrobble.InitRedis()
|
||||||
defer goscrobble.CloseRedisConn()
|
defer goscrobble.CloseRedisConn()
|
||||||
|
|
||||||
// Start background workers
|
// Start background workers if not DevMode
|
||||||
|
if !goscrobble.DevMode {
|
||||||
go goscrobble.StartBackgroundWorkers()
|
go goscrobble.StartBackgroundWorkers()
|
||||||
defer goscrobble.EndBackgroundWorkers()
|
defer goscrobble.EndBackgroundWorkers()
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Running in DevMode. No background workers running")
|
||||||
|
fmt.Println("")
|
||||||
|
}
|
||||||
|
|
||||||
// Boot up API webserver \o/
|
// Boot up API webserver \o/
|
||||||
goscrobble.HandleRequests(port)
|
goscrobble.HandleRequests(port)
|
||||||
|
@ -1,3 +1,10 @@
|
|||||||
|
# 0.0.32
|
||||||
|
- Add related records into track API
|
||||||
|
- Build out track page to show links to related records
|
||||||
|
- Tidy UI *even more*
|
||||||
|
- Bump golang build to 1.16.7
|
||||||
|
- Added DevMode env var. This prevents the background workers running on local machines
|
||||||
|
|
||||||
# 0.0.31
|
# 0.0.31
|
||||||
- Added newlines for flamerohr
|
- Added newlines for flamerohr
|
||||||
- Tidied pages
|
- Tidied pages
|
||||||
|
@ -30,5 +30,6 @@ These are stored in `web/.env.production` and `web/.env.development`
|
|||||||
MAIL_FROM_ADDRESS= // FROM email
|
MAIL_FROM_ADDRESS= // FROM email
|
||||||
MAIL_FROM_NAME= // FROM name
|
MAIL_FROM_NAME= // FROM name
|
||||||
|
|
||||||
|
DEV_MODE=false // true|false - Defaults false
|
||||||
GOSCROBBLE_DOMAIN="" // Full domain for email links (https://goscrobble.com))
|
GOSCROBBLE_DOMAIN="" // Full domain for email links (https://goscrobble.com))
|
||||||
STATIC_DIR="web" // Location to store images (This will serve from web/static)
|
STATIC_DIR="web" // Location to store images (This will serve from web/static)
|
||||||
|
@ -71,12 +71,16 @@ func HandleRequests(port string) {
|
|||||||
// No Auth
|
// No Auth
|
||||||
v1.HandleFunc("/stats", limitMiddleware(handleStats, lightLimiter)).Methods("GET")
|
v1.HandleFunc("/stats", limitMiddleware(handleStats, lightLimiter)).Methods("GET")
|
||||||
v1.HandleFunc("/profile/{username}", limitMiddleware(getProfile, lightLimiter)).Methods("GET")
|
v1.HandleFunc("/profile/{username}", limitMiddleware(getProfile, lightLimiter)).Methods("GET")
|
||||||
|
|
||||||
v1.HandleFunc("/artists/top/{uuid}", limitMiddleware(getArtists, lightLimiter)).Methods("GET")
|
v1.HandleFunc("/artists/top/{uuid}", limitMiddleware(getArtists, lightLimiter)).Methods("GET")
|
||||||
v1.HandleFunc("/artists/{uuid}", limitMiddleware(getArtist, lightLimiter)).Methods("GET")
|
v1.HandleFunc("/artists/{uuid}", limitMiddleware(getArtist, lightLimiter)).Methods("GET")
|
||||||
|
|
||||||
v1.HandleFunc("/albums/top/{uuid}", limitMiddleware(getArtists, lightLimiter)).Methods("GET")
|
v1.HandleFunc("/albums/top/{uuid}", limitMiddleware(getArtists, lightLimiter)).Methods("GET")
|
||||||
v1.HandleFunc("/albums/{uuid}", limitMiddleware(getAlbum, lightLimiter)).Methods("GET")
|
v1.HandleFunc("/albums/{uuid}", limitMiddleware(getAlbum, lightLimiter)).Methods("GET")
|
||||||
v1.HandleFunc("/tracks/top/{uuid}", limitMiddleware(getTracks, lightLimiter)).Methods("GET")
|
|
||||||
v1.HandleFunc("/tracks/{uuid}", limitMiddleware(getTrack, lightLimiter)).Methods("GET")
|
v1.HandleFunc("/tracks/top/{uuid}", limitMiddleware(getTracks, lightLimiter)).Methods("GET") // User UUID - Top Tracks
|
||||||
|
v1.HandleFunc("/tracks/{uuid}", limitMiddleware(getTrack, lightLimiter)).Methods("GET") // Track UUID
|
||||||
|
v1.HandleFunc("/tracks/{uuid}/top", limitMiddleware(getTopUsersForTrack, lightLimiter)).Methods("GET") // TrackUUID - Top Listeners
|
||||||
|
|
||||||
v1.HandleFunc("/register", limitMiddleware(handleRegister, heavyLimiter)).Methods("POST")
|
v1.HandleFunc("/register", limitMiddleware(handleRegister, heavyLimiter)).Methods("POST")
|
||||||
v1.HandleFunc("/login", limitMiddleware(handleLogin, standardLimiter)).Methods("POST")
|
v1.HandleFunc("/login", limitMiddleware(handleLogin, standardLimiter)).Methods("POST")
|
||||||
@ -648,6 +652,31 @@ func getTracks(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write(json)
|
w.Write(json)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getTopUsersForTrack - I suck at naming. Returns top users that have scrobbled this track.
|
||||||
|
func getTopUsersForTrack(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var uuid string
|
||||||
|
for k, v := range mux.Vars(r) {
|
||||||
|
if k == "uuid" {
|
||||||
|
uuid = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if uuid == "" {
|
||||||
|
throwOkError(w, "Invalid UUID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userList, err := getTopUsersForTrackUUID(uuid, 10, 1)
|
||||||
|
if err != nil {
|
||||||
|
throwOkError(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json, _ := json.Marshal(&userList)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write(json)
|
||||||
|
}
|
||||||
|
|
||||||
// postSpotifyResponse - Oauth Response from Spotify
|
// postSpotifyResponse - Oauth Response from Spotify
|
||||||
func postSpotifyReponse(w http.ResponseWriter, r *http.Request) {
|
func postSpotifyReponse(w http.ResponseWriter, r *http.Request) {
|
||||||
err := connectSpotifyResponse(r)
|
err := connectSpotifyResponse(r)
|
||||||
@ -748,7 +777,7 @@ func getServerInfo(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
info := ServerInfo{
|
info := ServerInfo{
|
||||||
Version: "0.0.31",
|
Version: "0.0.32",
|
||||||
RegistrationEnabled: cachedRegistrationEnabled,
|
RegistrationEnabled: cachedRegistrationEnabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,6 +30,23 @@ type TopTracks struct {
|
|||||||
Tracks map[int]TopTrack `json:"tracks"`
|
Tracks map[int]TopTrack `json:"tracks"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TopUserTrackResponse struct {
|
||||||
|
Meta TopUserTrackResponseMeta `json:"meta"`
|
||||||
|
Items []TopUserTrackResponseItem `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TopUserTrackResponseMeta struct {
|
||||||
|
Count int `json:"count"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TopUserTrackResponseItem struct {
|
||||||
|
UserUUID string `json:"user_uuid"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
UserName string `json:"user_name"`
|
||||||
|
}
|
||||||
|
|
||||||
// insertTrack - This will return if it exists or create it based on MBID > Name
|
// insertTrack - This will return if it exists or create it based on MBID > Name
|
||||||
func insertTrack(name string, legnth int, mbid string, spotifyId string, album string, artists []string, tx *sql.Tx) (Track, error) {
|
func insertTrack(name string, legnth int, mbid string, spotifyId string, album string, artists []string, tx *sql.Tx) (Track, error) {
|
||||||
track := Track{}
|
track := Track{}
|
||||||
@ -294,3 +311,57 @@ func (track *Track) getAlbumsForTrack() error {
|
|||||||
track.Albums = albums
|
track.Albums = albums
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getTopUsersForTrackUUID - Returns list of top users for a track
|
||||||
|
func getTopUsersForTrackUUID(trackUUID string, limit int, page int) (TopUserTrackResponse, error) {
|
||||||
|
response := TopUserTrackResponse{}
|
||||||
|
var count int
|
||||||
|
|
||||||
|
// Yeah this isn't great. But for now.. it works! Cache later
|
||||||
|
// TODO: This is counting total scrobbles, not unique users
|
||||||
|
total, err := getDbCount(
|
||||||
|
"SELECT COUNT(*) FROM `scrobbles` WHERE `track` = UUID_TO_BIN(?, true) GROUP BY `user`", trackUUID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to fetch scrobble count: %+v", err)
|
||||||
|
return response, errors.New("Failed to fetch combined scrobbles")
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := db.Query(
|
||||||
|
"SELECT BIN_TO_UUID(`scrobbles`.`user`, true), `users`.`username`, COUNT(*) "+
|
||||||
|
"FROM `scrobbles` "+
|
||||||
|
"JOIN `users` ON `scrobbles`.`user` = `users`.`uuid` "+
|
||||||
|
"WHERE `track` = UUID_TO_BIN(?, true) "+
|
||||||
|
"GROUP BY `scrobbles`.`user` "+
|
||||||
|
"ORDER BY COUNT(*) DESC LIMIT ?",
|
||||||
|
trackUUID, limit)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to fetch scrobbles: %+v", err)
|
||||||
|
return response, errors.New("Failed to fetch combined scrobbles")
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
item := TopUserTrackResponseItem{}
|
||||||
|
err := rows.Scan(&item.UserUUID, &item.UserName, &item.Count)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to fetch scrobbles: %+v", err)
|
||||||
|
return response, errors.New("Failed to fetch combined scrobbles")
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
response.Items = append(response.Items, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to fetch scrobbles: %+v", err)
|
||||||
|
return response, errors.New("Failed to fetch scrobbles")
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Meta.Count = count
|
||||||
|
response.Meta.Total = total
|
||||||
|
response.Meta.Page = page
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
@ -16,6 +16,9 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DevMode - Controls background workers and probably more
|
||||||
|
var DevMode bool
|
||||||
|
|
||||||
var emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
|
var emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
|
||||||
var usernameRegex = regexp.MustCompile("^[a-zA-Z0-9_\\.]+$")
|
var usernameRegex = regexp.MustCompile("^[a-zA-Z0-9_\\.]+$")
|
||||||
|
|
||||||
|
@ -355,3 +355,12 @@ export const getTopArtists = (uuid) => {
|
|||||||
return handleErrorResp(error)
|
return handleErrorResp(error)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getTopUsersForTrack = (uuid) => {
|
||||||
|
return axios.get(process.env.REACT_APP_API_URL + "/api/v1/tracks/" + uuid + "/top").then(
|
||||||
|
(data) => {
|
||||||
|
return data.data;
|
||||||
|
}).catch((error) => {
|
||||||
|
return handleErrorResp(error)
|
||||||
|
});
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
html, body {
|
html, body {
|
||||||
background-color: #282c34;
|
background-color: #282c34;
|
||||||
|
/** WHY DOES THIS DEFAULT TO 1.5 */
|
||||||
|
line-height: 1.3!important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.App {
|
.App {
|
||||||
|
@ -8,7 +8,7 @@ import AuthContext from '../Contexts/AuthContext';
|
|||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
'Home',
|
'Home',
|
||||||
'About',
|
// 'About',
|
||||||
];
|
];
|
||||||
|
|
||||||
const loggedInMenuItems = [
|
const loggedInMenuItems = [
|
||||||
|
0
web/src/Components/TopUserTable.css
Normal file
0
web/src/Components/TopUserTable.css
Normal file
56
web/src/Components/TopUserTable.js
Normal file
56
web/src/Components/TopUserTable.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import './TopUserTable.css'
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import ScaleLoader from 'react-spinners/ScaleLoader';
|
||||||
|
import { getTopUsersForTrack } from '../Api/index'
|
||||||
|
|
||||||
|
const TopUserTable = (props) => {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [data, setData] = useState({});
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!props.uuid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTopUsersForTrack(props.uuid)
|
||||||
|
.then(data => {
|
||||||
|
setData(data);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
}, [props.uuid])
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="pageWrapper">
|
||||||
|
<ScaleLoader color="#6AD7E5" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
width: `100%`,
|
||||||
|
display: `flex`,
|
||||||
|
flexWrap: `wrap`,
|
||||||
|
marginLeft: `20px`,
|
||||||
|
textAlign: `left`,
|
||||||
|
}}>
|
||||||
|
{
|
||||||
|
data.items &&
|
||||||
|
data.items.map(function (element) {
|
||||||
|
return <div style={{width: `100%`, padding: `2px`}} key={"box" + props.uuid}>
|
||||||
|
<Link
|
||||||
|
key={"user" + element.user_uuid}
|
||||||
|
to={"/u/"+element.user_name}
|
||||||
|
>{element.user_name}</Link> ({element.count})
|
||||||
|
</div>;
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TopUserTable;
|
@ -1 +0,0 @@
|
|||||||
|
|
@ -1,25 +0,0 @@
|
|||||||
import '../App.css';
|
|
||||||
import './Docs.css';
|
|
||||||
|
|
||||||
const Docs = () => {
|
|
||||||
return (
|
|
||||||
<div className="pageWrapper">
|
|
||||||
<h1>
|
|
||||||
Documentation
|
|
||||||
</h1>
|
|
||||||
<p className="aboutBody">
|
|
||||||
Go-Scrobble is an open source music scorbbling service written in Go and React.<br/>
|
|
||||||
Used to track your listening history and build a profile to discover new music.
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
className="pageBody"
|
|
||||||
href="https://gitlab.com/idanoo/go-scrobble"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>gitlab.com/idanoo/go-scrobble
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Docs;
|
|
@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import '../App.css';
|
import '../App.css';
|
||||||
import './Track.css';
|
import './Track.css';
|
||||||
|
import TopUserTable from '../Components/TopUserTable';
|
||||||
import ScaleLoader from 'react-spinners/ScaleLoader';
|
import ScaleLoader from 'react-spinners/ScaleLoader';
|
||||||
import { getTrack } from '../Api/index'
|
import { getTrack } from '../Api/index'
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
@ -80,22 +81,24 @@ const Track = (route) => {
|
|||||||
<div className="pageBody">
|
<div className="pageBody">
|
||||||
<div style={{display: `flex`, flexWrap: `wrap`, textAlign: `center`}}>
|
<div style={{display: `flex`, flexWrap: `wrap`, textAlign: `center`}}>
|
||||||
<div style={{width: `300px`, padding: `0 10px 10px 10px`, textAlign: `left`}}>
|
<div style={{width: `300px`, padding: `0 10px 10px 10px`, textAlign: `left`}}>
|
||||||
|
<img src={process.env.REACT_APP_API_URL + "/img/" + track.img + "_full.jpg"} alt={track.name} style={{maxWidth: `300px`, maxHeight: `300px`}}/>
|
||||||
|
</div>
|
||||||
|
<div style={{width: `290px`, padding: `0 10px 10px 10px`, margin: `0 5px 0 5px`, textAlign: `left`}}>
|
||||||
<span style={{fontSize: '14pt'}}>
|
<span style={{fontSize: '14pt'}}>
|
||||||
{artists}
|
{artists}
|
||||||
</span>
|
</span>
|
||||||
<br/>
|
<br/>
|
||||||
<span style={{fontSize: '12pt'}}>
|
<span style={{fontSize: '14pt', textDecoration: 'none'}}>
|
||||||
{albums}
|
{albums}
|
||||||
</span>
|
</span>
|
||||||
<img src={process.env.REACT_APP_API_URL + "/img/" + track.img + "_full.jpg"} alt={track.name} style={{maxWidth: `300px`, maxHeight: `300px`}}/><br/><br/>
|
<br/><br/>
|
||||||
{track.mbid && <a rel="noreferrer" target="_blank" href={"https://musicbrainz.org/track/" + track.mbid}>Open on MusicBrainz<br/></a>}
|
{track.mbid && <a rel="noreferrer" target="_blank" href={"https://musicbrainz.org/track/" + track.mbid}>Open on MusicBrainz<br/></a>}
|
||||||
{track.spotify_id && <a rel="noreferrer" target="_blank" href={"https://open.spotify.com/track/" + track.spotify_id}>Open on Spotify<br/></a>}
|
{track.spotify_id && <a rel="noreferrer" target="_blank" href={"https://open.spotify.com/track/" + track.spotify_id}>Open on Spotify<br/></a>}
|
||||||
Track Length: {length && length}
|
{length && <span>Track Length: {length}</span>}
|
||||||
</div>
|
</div>
|
||||||
<div style={{width: `600px`, padding: `0 10px 10px 10px`}}>
|
<div style={{width: `290px`, padding: `0 10px 10px 10px`}}>
|
||||||
<h3>Top Users</h3>
|
<h3>Top 10 Scrobblers</h3>
|
||||||
<br/>
|
<TopUserTable uuid={track.uuid}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
.modal {
|
.modal {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal > .header {
|
.modal > .header {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-bottom: 1px solid gray;
|
border-bottom: 1px solid gray;
|
||||||
|
Loading…
Reference in New Issue
Block a user