GoDoxy/internal/homepage/list_icons.go
2025-05-02 03:38:50 +08:00

373 lines
7.7 KiB
Go

package homepage
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"sync"
"time"
"github.com/lithammer/fuzzysearch/fuzzy"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/internal/utils"
"github.com/yusing/go-proxy/internal/utils/strutils"
)
type (
IconKey string
IconMap map[IconKey]*IconMeta
IconList []string
IconMeta struct {
SVG, PNG, WebP bool
Light, Dark bool
DisplayName string
Tag string
}
IconMetaSearch struct {
Source IconSource
Ref string
SVG bool
PNG bool
WebP bool
Light bool
Dark bool
}
Cache struct {
Icons IconMap
LastUpdate time.Time
sync.RWMutex `json:"-"`
}
)
func (icon *IconMeta) Filenames(ref string) []string {
filenames := make([]string, 0)
if icon.SVG {
filenames = append(filenames, fmt.Sprintf("%s.svg", ref))
if icon.Light {
filenames = append(filenames, fmt.Sprintf("%s-light.svg", ref))
}
if icon.Dark {
filenames = append(filenames, fmt.Sprintf("%s-dark.svg", ref))
}
}
if icon.PNG {
filenames = append(filenames, fmt.Sprintf("%s.png", ref))
if icon.Light {
filenames = append(filenames, fmt.Sprintf("%s-light.png", ref))
}
if icon.Dark {
filenames = append(filenames, fmt.Sprintf("%s-dark.png", ref))
}
}
if icon.WebP {
filenames = append(filenames, fmt.Sprintf("%s.webp", ref))
if icon.Light {
filenames = append(filenames, fmt.Sprintf("%s-light.webp", ref))
}
if icon.Dark {
filenames = append(filenames, fmt.Sprintf("%s-dark.webp", ref))
}
}
return filenames
}
const updateInterval = 2 * time.Hour
var iconsCache = &Cache{
Icons: make(IconMap),
}
const (
walkxcodeIcons = "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/tree.json"
selfhstIcons = "https://cdn.selfh.st/directory/icons.json"
)
func NewIconKey(source IconSource, reference string) IconKey {
return IconKey(fmt.Sprintf("%s/%s", source, reference))
}
func (k IconKey) SourceRef() (IconSource, string) {
parts := strings.Split(string(k), "/")
return IconSource(parts[0]), parts[1]
}
func InitIconListCache() {
iconsCache.Lock()
defer iconsCache.Unlock()
err := utils.LoadJSONIfExist(common.IconListCachePath, iconsCache)
if err != nil {
logging.Error().Err(err).Msg("failed to load icons")
} else if len(iconsCache.Icons) > 0 {
logging.Info().
Int("icons", len(iconsCache.Icons)).
Msg("icons loaded")
}
if err = updateIcons(); err != nil {
logging.Error().Err(err).Msg("failed to update icons")
}
task.OnProgramExit("save_icons_cache", func() {
utils.SaveJSON(common.IconListCachePath, iconsCache, 0o644)
})
}
func ListAvailableIcons() (*Cache, error) {
if common.IsTest {
return iconsCache, nil
}
iconsCache.RLock()
if time.Since(iconsCache.LastUpdate) < updateInterval {
if len(iconsCache.Icons) == 0 {
iconsCache.RUnlock()
return iconsCache, nil
}
}
iconsCache.RUnlock()
iconsCache.Lock()
defer iconsCache.Unlock()
logging.Info().Msg("updating icon data")
if err := updateIcons(); err != nil {
return nil, err
}
logging.Info().Int("icons", len(iconsCache.Icons)).Msg("icons list updated")
iconsCache.LastUpdate = time.Now()
err := utils.SaveJSON(common.IconListCachePath, iconsCache, 0o644)
if err != nil {
logging.Warn().Err(err).Msg("failed to save icons")
}
return iconsCache, nil
}
func SearchIcons(keyword string, limit int) ([]IconMetaSearch, error) {
if keyword == "" {
return make([]IconMetaSearch, 0), nil
}
iconsCache.RLock()
defer iconsCache.RUnlock()
result := make([]IconMetaSearch, 0)
for k, icon := range iconsCache.Icons {
if fuzzy.MatchFold(keyword, string(k)) {
source, ref := k.SourceRef()
result = append(result, IconMetaSearch{
Source: source,
Ref: ref,
SVG: icon.SVG,
PNG: icon.PNG,
WebP: icon.WebP,
Light: icon.Light,
Dark: icon.Dark,
})
}
if len(result) >= limit {
break
}
}
return result, nil
}
func HasIcon(icon *IconURL) bool {
if icon.Extra == nil {
return false
}
if common.IsTest {
return true
}
iconsCache.RLock()
defer iconsCache.RUnlock()
key := NewIconKey(icon.IconSource, icon.Extra.Ref)
meta, ok := iconsCache.Icons[key]
if !ok {
return false
}
switch icon.Extra.FileType {
case "png":
return meta.PNG && (!icon.Extra.IsLight || meta.Light) && (!icon.Extra.IsDark || meta.Dark)
case "svg":
return meta.SVG && (!icon.Extra.IsLight || meta.Light) && (!icon.Extra.IsDark || meta.Dark)
case "webp":
return meta.WebP && (!icon.Extra.IsLight || meta.Light) && (!icon.Extra.IsDark || meta.Dark)
default:
return false
}
}
type HomepageMeta struct {
DisplayName string
Tag string
}
func GetHomepageMeta(ref string) (HomepageMeta, bool) {
iconsCache.RLock()
defer iconsCache.RUnlock()
meta, ok := iconsCache.Icons[NewIconKey(IconSourceSelfhSt, ref)]
if !ok {
return HomepageMeta{}, false
}
return HomepageMeta{
DisplayName: meta.DisplayName,
Tag: meta.Tag,
}, true
}
func updateIcons() error {
clear(iconsCache.Icons)
if err := UpdateWalkxCodeIcons(); err != nil {
return err
}
return UpdateSelfhstIcons()
}
var httpGet = httpGetImpl
func MockHttpGet(body []byte) {
httpGet = func(_ string) ([]byte, error) {
return body, nil
}
}
func httpGetImpl(url string) ([]byte, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, nil
}
/*
format:
{
"png": [
"*.png",
],
"svg": [
"*.svg",
],
"webp": [
"*.webp",
]
}
*/
func UpdateWalkxCodeIcons() error {
body, err := httpGet(walkxcodeIcons)
if err != nil {
return err
}
data := make(map[string][]string)
err = json.Unmarshal(body, &data)
if err != nil {
return err
}
for fileType, files := range data {
var setExt func(icon *IconMeta)
switch fileType {
case "png":
setExt = func(icon *IconMeta) { icon.PNG = true }
case "svg":
setExt = func(icon *IconMeta) { icon.SVG = true }
case "webp":
setExt = func(icon *IconMeta) { icon.WebP = true }
}
for _, f := range files {
f = strings.TrimSuffix(f, "."+fileType)
isLight := strings.HasSuffix(f, "-light")
if isLight {
f = strings.TrimSuffix(f, "-light")
}
key := NewIconKey(IconSourceWalkXCode, f)
icon, ok := iconsCache.Icons[key]
if !ok {
icon = new(IconMeta)
iconsCache.Icons[key] = icon
}
setExt(icon)
if isLight {
icon.Light = true
}
}
}
return nil
}
/*
format:
{
"Name": "2FAuth",
"Reference": "2fauth",
"SVG": "Yes",
"PNG": "Yes",
"WebP": "Yes",
"Light": "Yes",
"Dark": "Yes",
"Tag": "",
"Category": "Self-Hosted",
"CreatedAt": "2024-08-16 00:27:23+00:00"
}
*/
func UpdateSelfhstIcons() error {
type SelfhStIcon struct {
Name string
Reference string
SVG string
PNG string
WebP string
Light string
Dark string
Tags string
}
body, err := httpGet(selfhstIcons)
if err != nil {
return err
}
data := make([]SelfhStIcon, 0)
err = json.Unmarshal(body, &data)
if err != nil {
return err
}
for _, item := range data {
var tag string
if item.Tags != "" {
tag = strutils.CommaSeperatedList(item.Tags)[0]
}
icon := &IconMeta{
DisplayName: item.Name,
Tag: tag,
SVG: item.SVG == "Yes",
PNG: item.PNG == "Yes",
WebP: item.WebP == "Yes",
Light: item.Light == "Yes",
Dark: item.Dark == "Yes",
}
key := NewIconKey(IconSourceSelfhSt, item.Reference)
iconsCache.Icons[key] = icon
}
return nil
}