diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d3a416b6..faf455d2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,7 +3,7 @@ stages: - bundle variables: - VERSION: 0.0.24 + VERSION: 0.0.25 build-go: image: golang:1.16.2 diff --git a/docs/changelog.md b/docs/changelog.md index 47bcdadc..a689004a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,7 @@ +# 0.0.25 +- Images now pull from spotify if setup! +- Show top artists/album + # 0.0.24 - Spotify will now add images on scrobble - Renamed /api/v1/track to tracks to bypass blocklists... (uBlock :() diff --git a/internal/goscrobble/artist.go b/internal/goscrobble/artist.go index e29b0a59..136befc2 100644 --- a/internal/goscrobble/artist.go +++ b/internal/goscrobble/artist.go @@ -15,6 +15,17 @@ type Artist struct { SpotifyID string `json:"spotify_id"` } +type TopArtist struct { + UUID string `json:"uuid"` + Name string `json:"name"` + Img string `json:"img"` + Plays int `json:"plays"` +} + +type TopArtists struct { + Artists map[int]TopArtist `json:"artists"` +} + // insertArtist - This will return if it exists or create it based on MBID > Name func insertArtist(name string, mbid string, spotifyId string, img string, tx *sql.Tx) (Artist, error) { artist := Artist{} @@ -114,3 +125,42 @@ func getArtistByUUID(uuid string) (Artist, error) { return artist, nil } + +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(*) "+ + "FROM `scrobbles` "+ + "JOIN `tracks` ON `tracks`.`uuid` = `scrobbles`.`track` "+ + "JOIN track_artist ON track_artist.track = tracks.uuid "+ + "JOIN artists ON track_artist.artist = artists.uuid "+ + "WHERE `scrobbles`.`user` = UUID_TO_BIN(?, true) "+ + "GROUP BY `artists`.`uuid` "+ + "ORDER BY count(*) DESC "+ + "LIMIT 14;", + userUuid) + if err != nil { + log.Printf("Failed to fetch top artist: %+v", err) + return topArtist, errors.New("Failed to fetch top artist") + } + defer rows.Close() + + i := 1 + tempArtists := make(map[int]TopArtist) + + for rows.Next() { + var artist TopArtist + err := rows.Scan(&artist.UUID, &artist.Name, &artist.Img, &artist.Plays) + if err != nil { + log.Printf("Failed to fetch artist: %+v", err) + return topArtist, errors.New("Failed to fetch artist") + } + + tempArtists[i] = artist + i++ + } + + topArtist.Artists = tempArtists + + return topArtist, nil +} diff --git a/internal/goscrobble/ingress_spotify.go b/internal/goscrobble/ingress_spotify.go index e549ee0e..9a89657d 100644 --- a/internal/goscrobble/ingress_spotify.go +++ b/internal/goscrobble/ingress_spotify.go @@ -214,6 +214,102 @@ func ParseSpotifyInput(userUUID string, data spotify.RecentlyPlayedItem, client return nil } -func (track *Track) updateImageFromSpotify() error { +// updateImageDataFromSpotify update artist/album images from spotify ;D +func (user *User) updateImageDataFromSpotify() error { + // Check that data is set before we attempt to pull + val, _ := getConfigValue("SPOTIFY_APP_SECRET") + if val == "" { + return nil + } + + // TO BE REWORKED TO NOT USE A DAMN USER ARGHHH + dbToken, err := user.getSpotifyTokens() + if err != nil { + return nil + } + + token := new(oauth2.Token) + token.AccessToken = dbToken.AccessToken + token.RefreshToken = dbToken.RefreshToken + token.Expiry = dbToken.Expiry + token.TokenType = "Bearer" + + auth := getSpotifyAuthHandler() + 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") + if err != nil { + log.Printf("Failed to fetch config: %+v", err) + return errors.New("Failed to fetch artists") + } + + toUpdate := make(map[string]string) + for rows.Next() { + var uuid string + var name string + err := rows.Scan(&uuid, &name) + if err != nil { + log.Printf("Failed to fetch artists: %+v", err) + rows.Close() + return errors.New("Failed to fetch artist") + } + res, err := client.Search(name, spotify.SearchTypeArtist) + if len(res.Artists.Artists) > 0 { + if len(res.Artists.Artists[0].Images) > 0 { + toUpdate[uuid] = res.Artists.Artists[0].Images[0].URL + } + } + + } + rows.Close() + + var artist Artist + tx, _ := db.Begin() + for uuid, img := range toUpdate { + artist, err = getArtistByUUID(uuid) + if err != nil { + continue + } + _ = artist.updateArtist("img", img, tx) + } + tx.Commit() + + rows, err = db.Query("SELECT BIN_TO_UUID(`uuid`, true), `name` FROM `albums` WHERE IFNULL(`img`,'') = '' LIMIT 50") + if err != nil { + log.Printf("Failed to fetch config: %+v", err) + return errors.New("Failed to fetch artists") + } + + toUpdate = make(map[string]string) + for rows.Next() { + var uuid string + var name string + err := rows.Scan(&uuid, &name) + if err != nil { + log.Printf("Failed to fetch albums: %+v", err) + rows.Close() + return errors.New("Failed to fetch album") + } + res, err := client.Search(name, spotify.SearchTypeAlbum) + if len(res.Albums.Albums) > 0 { + if len(res.Albums.Albums[0].Images) > 0 { + toUpdate[uuid] = res.Albums.Albums[0].Images[0].URL + } + } + + } + rows.Close() + + var album Album + tx, _ = db.Begin() + for uuid, img := range toUpdate { + album, err = getAlbumByUUID(uuid) + if err != nil { + continue + } + _ = album.updateAlbum("img", img, tx) + } + tx.Commit() return nil } diff --git a/internal/goscrobble/server.go b/internal/goscrobble/server.go index 460eca3b..02def728 100644 --- a/internal/goscrobble/server.go +++ b/internal/goscrobble/server.go @@ -550,13 +550,13 @@ func getArtists(w http.ResponseWriter, r *http.Request) { return } - artist, err := getArtistByUUID(uuid) + track, err := getTopArtists(uuid) if err != nil { throwOkError(w, err.Error()) return } - json, _ := json.Marshal(&artist) + json, _ := json.Marshal(&track) w.WriteHeader(http.StatusOK) w.Write(json) } @@ -665,7 +665,7 @@ func getServerInfo(w http.ResponseWriter, r *http.Request) { } info := ServerInfo{ - Version: "0.0.24", + Version: "0.0.25", RegistrationEnabled: cachedRegistrationEnabled, } diff --git a/internal/goscrobble/timers.go b/internal/goscrobble/timers.go index e2d74906..61c6d09b 100644 --- a/internal/goscrobble/timers.go +++ b/internal/goscrobble/timers.go @@ -8,11 +8,10 @@ import ( var endTicker chan bool func StartBackgroundWorkers() { - updateSpotifyData() endTicker := make(chan bool) - hourTicker := time.NewTicker(time.Hour) + hourTicker := time.NewTicker(time.Duration(1) * time.Hour) minuteTicker := time.NewTicker(time.Duration(60) * time.Second) go func() { @@ -25,6 +24,9 @@ func StartBackgroundWorkers() { // Clear old password reset tokens clearOldResetTokens() + // Attempt to pull missing images from spotify - hackerino version! + user, _ := getUserByUsername("idanoo") + user.updateImageDataFromSpotify() case <-minuteTicker.C: // Update playdata from spotify updateSpotifyData() diff --git a/internal/goscrobble/track.go b/internal/goscrobble/track.go index 4958fe06..d920f610 100644 --- a/internal/goscrobble/track.go +++ b/internal/goscrobble/track.go @@ -178,11 +178,11 @@ 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(`artists`.`img`,''), count(*) "+ + rows, err := db.Query("SELECT BIN_TO_UUID(`tracks`.`uuid`, true), `tracks`.`name`, IFNULL(`albums`.`img`,''), count(*) "+ "FROM `scrobbles` "+ "JOIN `tracks` ON `tracks`.`uuid` = `scrobbles`.`track` "+ - "JOIN track_artist ON track_artist.track = tracks.uuid "+ - "JOIN artists ON track_artist.artist = artists.uuid "+ + "JOIN track_album ON track_album.track = tracks.uuid "+ + "JOIN albums ON track_album.album = albums.uuid "+ "WHERE `user` = UUID_TO_BIN(?, true) "+ "GROUP BY `scrobbles`.`track` "+ "ORDER BY count(*) DESC "+ diff --git a/web/src/Api/index.js b/web/src/Api/index.js index 5e8ee6ce..64754e49 100644 --- a/web/src/Api/index.js +++ b/web/src/Api/index.js @@ -326,3 +326,12 @@ export const getTopTracks = (uuid) => { return handleErrorResp(error) }); } + +export const getTopArtists = (uuid) => { + return axios.get(process.env.REACT_APP_API_URL + "artists/top/" + uuid).then( + (data) => { + return data.data; + }).catch((error) => { + return handleErrorResp(error) + }); +} \ No newline at end of file diff --git a/web/src/Components/TopTable.js b/web/src/Components/TopTable.js index a7559ae2..4e5b378b 100644 --- a/web/src/Components/TopTable.js +++ b/web/src/Components/TopTable.js @@ -3,13 +3,13 @@ import './TopTable.css' import TopTableBox from './TopTableBox'; const TopTable = (props) => { - if (!props.items || !props.items.tracks) { + if (!props.items || Object.keys(props.items).length < 1) { return ( - No data. + Not enough data to show top {props.type}s.
) } - let tracks = props.items.tracks; + let tracks = props.items; return (
@@ -24,101 +24,105 @@ const TopTable = (props) => { img={tracks[1].img} />
-
+ { Object.keys(props.items).length > 5 && +
+ + + + +
+ } + { Object.keys(props.items).length >= 14 && +
-
-
- - - - - - - - - -
+ + + + + +
+ } ); diff --git a/web/src/Components/TopTableBox.css b/web/src/Components/TopTableBox.css index c1e1a861..31601c48 100644 --- a/web/src/Components/TopTableBox.css +++ b/web/src/Components/TopTableBox.css @@ -10,7 +10,6 @@ img { .topOverlay { position: absolute; - margin: 5px; padding: 2px 5px 5px 5px; background-color: rgba(0, 0, 0, 0.6); line-height: 0.6em; diff --git a/web/src/Components/TopTableBox.js b/web/src/Components/TopTableBox.js index a29f9059..a2b7fb76 100644 --- a/web/src/Components/TopTableBox.js +++ b/web/src/Components/TopTableBox.js @@ -20,7 +20,7 @@ const TopTableBox = (props) => { height: `${props.size}px`, float: `left`, }} > -
+
#{props.number} {props.title} diff --git a/web/src/Pages/Profile.js b/web/src/Pages/Profile.js index d9110bff..d6c8d22c 100644 --- a/web/src/Pages/Profile.js +++ b/web/src/Pages/Profile.js @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import '../App.css'; import './Profile.css'; import ScaleLoader from 'react-spinners/ScaleLoader'; -import { getProfile, getTopTracks } from '../Api/index' +import { getProfile, getTopTracks, getTopArtists } from '../Api/index' import ScrobbleTable from '../Components/ScrobbleTable' import TopTable from '../Components/TopTable' @@ -10,6 +10,7 @@ const Profile = (route) => { const [loading, setLoading] = useState(true); const [profile, setProfile] = useState({}); const [topTracks, setTopTracks] = useState({}) + const [topArtists, setTopArtists] = useState({}) let username = false; if (route && route.match && route.match.params && route.match.params.uuid) { @@ -30,9 +31,14 @@ const Profile = (route) => { // Fetch top tracks getTopTracks(data.uuid) .then(data => { - setTopTracks(data) - } - ) + setTopTracks(data.tracks) + }) + + // Fetch top artists + getTopArtists(data.uuid) + .then(data => { + setTopArtists(data.artists) + }) setLoading(false); }) @@ -63,6 +69,8 @@ const Profile = (route) => {

+ +
Last 10 scrobbles...