-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: support album art for image (#7)
- Loading branch information
1 parent
b9633b4
commit 51a62f6
Showing
6 changed files
with
330 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |