Skip to content

Commit

Permalink
feat: support album art for image (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
TorchedSammy authored Oct 29, 2023
1 parent b9633b4 commit 51a62f6
Show file tree
Hide file tree
Showing 6 changed files with 330 additions and 4 deletions.
7 changes: 7 additions & 0 deletions artfetcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package main

import "github.com/godbus/dbus/v5"

type artFetcher interface{
getAlbumArt(artist, album, title string, metadata map[string]dbus.Variant) string
}
13 changes: 13 additions & 0 deletions assets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package main

type Asset struct {
ID string `json:"id"`
Name string `json:"name"`
}

type AssetToUpload struct {
Name string `json:"name"`
Type string `json:"type"` // always 1
Image string `json:"image"` // base64 encoded
}

2 changes: 2 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ type config struct {
PlayerPresence map[string]presenceConfig `json:"playerPresence"`
Vars []string `json:"vars"`
LogLevel string `json:"logLevel"`
ShowAlbumArt bool `json:"showAlbumArt"`
ArtFetchMethod string `json:"artFetchMethod"`
}

type presenceConfig struct {
Expand Down
175 changes: 175 additions & 0 deletions discord.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package main

import (
"bytes"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io/fs"
"net/http"
"os"
"regexp"
"sort"
"strings"
"net/url"

"github.com/godbus/dbus/v5"
)

var tokenRegex = regexp.MustCompile(`mfa\.[\w-]{84}`)
var receivedAssets []Asset

type discordFetcher struct{}

func (discordFetcher) getAlbumArt(artist, album, title string, mdata map[string]dbus.Variant) string {
artFile := ""
if artUrl, ok := mdata["mpris:artUrl"].Value().(string); ok {
artFile, _ = url.PathUnescape(artUrl)
// remove file:// from the beginning
artFile = artFile[7:]
}

albumAsset, err := checkForAsset(album)
fmt.Println(err, albumAsset)
if err != nil {
fmt.Println("Uploading " + artFile + " to discord")
albumAsset, err = uploadAsset(artFile, album)
fmt.Println(err)
}

return albumAsset
}

// function to get token from local discord db
func getDiscordToken() string {
discordDir := os.Getenv("HOME") + "/.config/discord"
dbDir := discordDir + "/Local Storage/leveldb/"
// get files in dbDir
files, err := os.ReadDir(dbDir)
dbs := []fs.DirEntry{}
if err != nil {
fmt.Println(err)
return ""
}
// add file to dbs if it ends with .ldb
for _, file := range files {
if strings.HasSuffix(file.Name(), ".ldb") {
dbs = append(dbs, file)
}
}

// sort by modification time
sort.Slice(files, func(i, j int) bool {
firstFileInfo, _ := files[i].Info()
secondFileInfo, _ := files[j].Info()
return firstFileInfo.ModTime().After(secondFileInfo.ModTime())
})

// go through all leveldbs to find the one with the token
for _, dbFile := range dbs {
dbContents, err := os.ReadFile(dbDir + dbFile.Name())
if err != nil {
fmt.Println(err)
return ""
}

// return single regex match
token := tokenRegex.FindString(string(dbContents))
if token != "" {
return token
}
}

return ""
}

func getAssets() []Asset {
// make get request to get assets
if len(receivedAssets) != 0 {
return receivedAssets
}
resp, err := http.Get("https://discord.com/api/v9/oauth2/applications/902662551119224852/assets")
if err != nil {
fmt.Println(err)
return []Asset{}
}

var assets []Asset
json.NewDecoder(resp.Body).Decode(&assets)

receivedAssets = assets
return receivedAssets
}

// upload asset to discord
// assetName will be the album name, or song name if no album
// "music" is returned as the assetName when an error occurs since that's the name of just a music icon
func uploadAsset(fileName, assetName string) (string, error) {
image, err := os.ReadFile(fileName)
if err != nil {
return "music", err
}

base64Encoding := ""

// determine the content type of the image file
imageType := http.DetectContentType(image)

// album art is only going to be jpg or png, right?
// if not i hate you
switch imageType {
case "image/jpeg":
base64Encoding += "data:image/jpeg;base64,"
case "image/png":
base64Encoding += "data:image/png;base64,"
}

base64Encoding += base64.StdEncoding.EncodeToString(image)

// turn assetName into a hex encoded string
assetNameEncoded := hex.EncodeToString([]byte(assetName))

asset := AssetToUpload{
Type: "1",
Name: assetNameEncoded,
Image: base64Encoding,
}

// make post request to upload asset, using token in Authentication header
jsonBytes, err := json.Marshal(asset)
req, _ := http.NewRequest("POST", "https://discord.com/api/v9/oauth2/applications/902662551119224852/assets", bytes.NewBuffer(jsonBytes))

req.Header.Set("Authorization", getDiscordToken())
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "music", err
}
var receivedAssetInfo Asset
json.NewDecoder(resp.Body).Decode(&receivedAssetInfo)
resp.Body.Close()
fmt.Println(receivedAssetInfo)

receivedAssets = append(receivedAssets, receivedAssetInfo)

return assetName, nil
}

func checkForAsset(albumName string) (string, error) {
// get assets from discord
assets := getAssets()

albumNameEncoded := hex.EncodeToString([]byte(albumName))

// go through assets to see if one matches fileName
for _, asset := range assets {
if asset.Name == albumNameEncoded {
fmt.Println("found asset")
return asset.Name, nil
}
}

return "", fmt.Errorf("No asset found for %s", albumName)
}
21 changes: 17 additions & 4 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@ var conf = config{
State: "{artist} {album}",
},
LogLevel: "info",
ShowAlbumArt: true,
ArtFetchMethod: "spotify",
}

var logger = slogx.NewLogger("Clematis")
var fetcher artFetcher

func main() {
logger.SetFormat("${time} ${level} > ${message}")
Expand Down Expand Up @@ -60,6 +64,11 @@ func main() {
logger.Error("Error reading config file: ", err)
}
json.Unmarshal(confFile, &conf)
switch conf.ArtFetchMethod {
case "spotify": fetcher = spotifyFetcher{}
case "discord": fetcher = discordFetcher{}
default: panic(fmt.Sprintf("Invalid album art fetcher %s. The valid options are: spotify, discord", conf.ArtFetchMethod))
}

logger.SetLevel(slogx.ParseLevel(conf.LogLevel))

Expand Down Expand Up @@ -212,20 +221,24 @@ func setPresence(metadata map[string]dbus.Variant, songstamp time.Time, player *
}
}
album := ""
albumName := ""
if abm, ok := metadata["xesam:album"].Value().(string); ok {
album = "on " + abm
albumName = abm
album = "on " + albumName
}
if pbStat != "Playing" {
startstamp, endstamp = nil, nil
}

artistsStr := ""
if artistsArr, ok := metadata["xesam:artist"].Value().([]string); ok {
artistsStr = "by " + strings.Join(artistsArr, ", ")
artistsStr = strings.Join(artistsArr, ", ")
}

url := fetcher.getAlbumArt(artistsStr, albumName, title, metadata)

args := []string{
"{artist}", artistsStr,
"{artist}", "by " + artistsStr,
"{title}", title,
"{album}", album,
}
Expand All @@ -243,7 +256,7 @@ func setPresence(metadata map[string]dbus.Variant, songstamp time.Time, player *
client.SetActivity(client.Activity{
Details: replacer.Replace(p.Details),
State: replacer.Replace(p.State),
LargeImage: "music",
LargeImage: url,
LargeText: playerIdentity,
SmallImage: strings.ToLower(string(pbStat)),
SmallText: string(pbStat),
Expand Down
116 changes: 116 additions & 0 deletions spotify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package main

// credit to https://github.com/lacymorrow/album-art
// thanks for the token too :)

import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"os"
"net/http"
"net/url"
// "sort"
"strings"

"github.com/godbus/dbus/v5"
)

const (
art_endpoint = "https://api.spotify.com/v1"
auth_endpoint = "https://accounts.spotify.com/api/token"
art_api_id = "3f974573800a4ff5b325de9795b8e603"
art_api_secret = "ff188d2860ff44baa57acc79c121a3b9"
art_api_auth = art_api_id + ":" + art_api_secret
)

type spotifyFetcher struct{}

type spotifyAccess struct{
AccessToken string `json:"access_token"`
}

type spotifySeach struct{
Tracks spotifyTrack `json:"tracks"`
}

type spotifyTrack struct{
Items []spotifyTrackObject `json:"items"`
}

type spotifyTrackObject struct{
Album spotifyAlbum `json:"album"`
}

type spotifyAlbum struct{
Images []spotifyArt
}

type spotifyArt struct {
Width int
Height int
URL string
}

func handleSpotErr(err error) string {
fmt.Fprintln(os.Stderr, err)
return "music"
}

func (spotifyFetcher) getAlbumArt(artist, album, title string, mdata map[string]dbus.Variant) string {
spotSearchQuery := url.PathEscape(url.QueryEscape(fmt.Sprintf("track:%s artist:%s", title, artist)))
artUrl, _ := url.Parse(fmt.Sprintf("%s/search?q=%s&type=track&limit=1", art_endpoint, spotSearchQuery))
authUrl, _ := url.Parse(auth_endpoint)

req, err := http.NewRequest("POST", authUrl.String(), strings.NewReader("grant_type=client_credentials"))
if err != nil {
return handleSpotErr(err)
}
req.Header.Add("Authorization", "Basic " + base64.StdEncoding.EncodeToString([]byte(art_api_auth)))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

resp, err := http.DefaultClient.Do(req)
if err != nil {
return handleSpotErr(err)
}

body, err := io.ReadAll(resp.Body)
spot := &spotifyAccess{}
if err := json.Unmarshal(body, &spot); err != nil {
return handleSpotErr(err)
}

req, err = http.NewRequest("GET", artUrl.String(), strings.NewReader(""))
req.Header.Add("Authorization", "Bearer " + spot.AccessToken)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

resp, err = http.DefaultClient.Do(req)
if err != nil {
return handleSpotErr(err)
}

body, err = io.ReadAll(resp.Body)
logger.Debug(string(body))

spotifyData := &spotifySeach{}
if err := json.Unmarshal(body, &spotifyData); err != nil {
return handleSpotErr(err)
}

if len(spotifyData.Tracks.Items) == 0 {
// nothing found
fmt.Fprintln(os.Stderr, "Nothing found on spotify for %s", album)
return "music"
}

images := spotifyData.Tracks.Items[0].Album.Images
// may or may not be needed (will see)
/*
sort.Slice(images, func(i, j int) bool {
return images[i].Width > images[j].Width
})
*/

return images[0].URL
}

0 comments on commit 51a62f6

Please sign in to comment.