182 lines
5.5 KiB
Go
182 lines
5.5 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/navidrome/navidrome/plugins/pdk/go/host"
|
|
"github.com/navidrome/navidrome/plugins/pdk/go/pdk"
|
|
)
|
|
|
|
// Configuration keys for image hosting
|
|
const (
|
|
imageHostKey = "imagehost"
|
|
imgbbApiKeyKey = "imgbbapikey"
|
|
)
|
|
|
|
// imgbb API response
|
|
type imgbbResponse struct {
|
|
Data struct {
|
|
DisplayURL string `json:"display_url"`
|
|
} `json:"data"`
|
|
Success bool `json:"success"`
|
|
}
|
|
|
|
// uguu.se API response
|
|
type uguuResponse struct {
|
|
Success bool `json:"success"`
|
|
Files []struct {
|
|
URL string `json:"url"`
|
|
} `json:"files"`
|
|
}
|
|
|
|
// getImageURL retrieves the track artwork URL, optionally uploading to a public image host.
|
|
func getImageURL(username, trackID string) string {
|
|
imageHost, _ := pdk.GetConfig(imageHostKey)
|
|
|
|
switch imageHost {
|
|
case "imgbb":
|
|
return getImageViaImgbb(username, trackID)
|
|
case "uguu":
|
|
return getImageViaUguu(username, trackID)
|
|
default:
|
|
return getImageDirect(trackID)
|
|
}
|
|
}
|
|
|
|
// getImageDirect returns the artwork URL directly from Navidrome (current behavior).
|
|
func getImageDirect(trackID string) string {
|
|
artworkURL, err := host.ArtworkGetTrackUrl(trackID, 300)
|
|
if err != nil {
|
|
pdk.Log(pdk.LogWarn, fmt.Sprintf("Failed to get artwork URL: %v", err))
|
|
return ""
|
|
}
|
|
|
|
// Don't use localhost URLs
|
|
if strings.HasPrefix(artworkURL, "http://localhost") {
|
|
return ""
|
|
}
|
|
return artworkURL
|
|
}
|
|
|
|
// uploadFunc uploads image data to an external host and returns the public URL.
|
|
type uploadFunc func(contentType string, data []byte) (string, error)
|
|
|
|
// getImageViaHost fetches artwork and uploads it using the provided upload function.
|
|
func getImageViaHost(provider, username, trackID string, cacheTTL int64, upload uploadFunc) string {
|
|
// Check cache first
|
|
cacheKey := fmt.Sprintf("%s.artwork.%s", provider, trackID)
|
|
cachedURL, exists, err := host.CacheGetString(cacheKey)
|
|
if err == nil && exists {
|
|
pdk.Log(pdk.LogDebug, fmt.Sprintf("Cache hit for %s artwork: %s", provider, trackID))
|
|
return cachedURL
|
|
}
|
|
|
|
// Fetch artwork data from Navidrome
|
|
contentType, data, err := host.SubsonicAPICallRaw(fmt.Sprintf("/getCoverArt?u=%s&id=%s&size=300", username, trackID))
|
|
if err != nil {
|
|
pdk.Log(pdk.LogWarn, fmt.Sprintf("Failed to fetch artwork data: %v", err))
|
|
return ""
|
|
}
|
|
|
|
// Upload to external host
|
|
url, err := upload(contentType, data)
|
|
if err != nil {
|
|
pdk.Log(pdk.LogWarn, fmt.Sprintf("Failed to upload to %s: %v", provider, err))
|
|
return ""
|
|
}
|
|
|
|
_ = host.CacheSetString(cacheKey, url, cacheTTL)
|
|
return url
|
|
}
|
|
|
|
// getImageViaImgbb fetches artwork and uploads it to imgbb.
|
|
func getImageViaImgbb(username, trackID string) string {
|
|
apiKey, ok := pdk.GetConfig(imgbbApiKeyKey)
|
|
if !ok || apiKey == "" {
|
|
pdk.Log(pdk.LogWarn, "imgbb image host selected but no API key configured")
|
|
return ""
|
|
}
|
|
|
|
return getImageViaHost("imgbb", username, trackID, 82800, func(_ string, data []byte) (string, error) {
|
|
return uploadToImgbb(apiKey, data)
|
|
})
|
|
}
|
|
|
|
// getImageViaUguu fetches artwork and uploads it to uguu.se.
|
|
func getImageViaUguu(username, trackID string) string {
|
|
return getImageViaHost("uguu", username, trackID, 9000, func(contentType string, data []byte) (string, error) {
|
|
return uploadToUguu(data, contentType)
|
|
})
|
|
}
|
|
|
|
// uploadToImgbb uploads image data to imgbb and returns the display URL.
|
|
func uploadToImgbb(apiKey string, imageData []byte) (string, error) {
|
|
encoded := base64.StdEncoding.EncodeToString(imageData)
|
|
body := fmt.Sprintf("key=%s&image=%s&expiration=86400", url.QueryEscape(apiKey), url.QueryEscape(encoded))
|
|
|
|
req := pdk.NewHTTPRequest(pdk.MethodPost, "https://api.imgbb.com/1/upload")
|
|
req.SetHeader("Content-Type", "application/x-www-form-urlencoded")
|
|
req.SetBody([]byte(body))
|
|
|
|
resp := req.Send()
|
|
if resp.Status() >= 400 {
|
|
return "", fmt.Errorf("imgbb upload failed: HTTP %d", resp.Status())
|
|
}
|
|
|
|
var result imgbbResponse
|
|
if err := json.Unmarshal(resp.Body(), &result); err != nil {
|
|
return "", fmt.Errorf("failed to parse imgbb response: %w", err)
|
|
}
|
|
|
|
if !result.Success {
|
|
return "", fmt.Errorf("imgbb upload was not successful")
|
|
}
|
|
|
|
if result.Data.DisplayURL == "" {
|
|
return "", fmt.Errorf("imgbb returned empty display URL")
|
|
}
|
|
|
|
return result.Data.DisplayURL, nil
|
|
}
|
|
|
|
// uploadToUguu uploads image data to uguu.se and returns the file URL.
|
|
func uploadToUguu(imageData []byte, contentType string) (string, error) {
|
|
// Build multipart/form-data body manually (TinyGo-compatible)
|
|
boundary := "----NavidromeCoverArt"
|
|
var body []byte
|
|
body = append(body, []byte(fmt.Sprintf("--%s\r\n", boundary))...)
|
|
body = append(body, []byte(fmt.Sprintf("Content-Disposition: form-data; name=\"files[]\"; filename=\"cover.jpg\"\r\n"))...)
|
|
body = append(body, []byte(fmt.Sprintf("Content-Type: %s\r\n", contentType))...)
|
|
body = append(body, []byte("\r\n")...)
|
|
body = append(body, imageData...)
|
|
body = append(body, []byte(fmt.Sprintf("\r\n--%s--\r\n", boundary))...)
|
|
|
|
req := pdk.NewHTTPRequest(pdk.MethodPost, "https://uguu.se/upload")
|
|
req.SetHeader("Content-Type", fmt.Sprintf("multipart/form-data; boundary=%s", boundary))
|
|
req.SetBody(body)
|
|
|
|
resp := req.Send()
|
|
if resp.Status() >= 400 {
|
|
return "", fmt.Errorf("uguu.se upload failed: HTTP %d", resp.Status())
|
|
}
|
|
|
|
var result uguuResponse
|
|
if err := json.Unmarshal(resp.Body(), &result); err != nil {
|
|
return "", fmt.Errorf("failed to parse uguu.se response: %w", err)
|
|
}
|
|
|
|
if !result.Success || len(result.Files) == 0 {
|
|
return "", fmt.Errorf("uguu.se upload was not successful")
|
|
}
|
|
|
|
if result.Files[0].URL == "" {
|
|
return "", fmt.Errorf("uguu.se returned empty URL")
|
|
}
|
|
|
|
return result.Files[0].URL, nil
|
|
}
|