mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-24 09:18:31 +02:00
refactor: improve error handling and response formatting in API
This commit is contained in:
@@ -2,9 +2,8 @@ package homepage
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
"github.com/yusing/go-proxy/internal/homepage/widgets"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
@@ -13,20 +12,21 @@ type (
|
||||
Category []*Item
|
||||
|
||||
ItemConfig struct {
|
||||
Show bool `json:"show"`
|
||||
Name string `json:"name"` // display name
|
||||
Icon *IconURL `json:"icon"`
|
||||
Category string `json:"category"`
|
||||
Description string `json:"description" aliases:"desc"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
WidgetConfig map[string]any `json:"widget_config" aliases:"widget"`
|
||||
Show bool `json:"show"`
|
||||
Name string `json:"name"` // display name
|
||||
Icon *IconURL `json:"icon"`
|
||||
Category string `json:"category"`
|
||||
Description string `json:"description" aliases:"desc"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
|
||||
Item struct {
|
||||
*ItemConfig
|
||||
WidgetConfig *widgets.Config `json:"widget_config,omitempty" aliases:"widget"`
|
||||
|
||||
Alias string
|
||||
Provider string
|
||||
Alias string
|
||||
Provider string
|
||||
OriginURL string
|
||||
}
|
||||
)
|
||||
|
||||
@@ -43,23 +43,10 @@ func (cfg *ItemConfig) GetOverride(alias string) *ItemConfig {
|
||||
}
|
||||
|
||||
func (item *Item) MarshalJSON() ([]byte, error) {
|
||||
var url *string
|
||||
if !strings.ContainsRune(item.Alias, '.') {
|
||||
godoxyCfg := config.GetInstance().Value()
|
||||
// use first domain as base domain
|
||||
domains := godoxyCfg.MatchDomains
|
||||
if len(domains) > 0 {
|
||||
url = new(string)
|
||||
*url = item.Alias + domains[0]
|
||||
}
|
||||
} else {
|
||||
url = &item.Alias
|
||||
}
|
||||
return json.Marshal(map[string]any{
|
||||
"show": item.Show,
|
||||
"alias": item.Alias,
|
||||
"provider": item.Provider,
|
||||
"url": url,
|
||||
"name": item.Name,
|
||||
"icon": item.Icon,
|
||||
"category": item.Category,
|
||||
|
||||
68
internal/homepage/integrations/qbittorrent/client.go
Normal file
68
internal/homepage/integrations/qbittorrent/client.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/homepage/widgets"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
URL string
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (c *Client) Initialize(ctx context.Context, url string, cfg map[string]any) error {
|
||||
c.URL = url
|
||||
c.Username = cfg["username"].(string)
|
||||
c.Password = cfg["password"].(string)
|
||||
|
||||
_, err := c.Version(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) doRequest(ctx context.Context, method, endpoint string, query url.Values, body io.Reader) (*http.Response, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, method, c.URL+endpoint+query.Encode(), body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.Username != "" && c.Password != "" {
|
||||
req.SetBasicAuth(c.Username, c.Password)
|
||||
}
|
||||
|
||||
resp, err := widgets.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, gperr.Errorf("%w: %d %s", widgets.ErrHTTPStatus, resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func jsonRequest[T any](ctx context.Context, client *Client, endpoint string, query url.Values) (result T, err error) {
|
||||
resp, err := client.doRequest(ctx, http.MethodGet, endpoint, query, nil)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
102
internal/homepage/integrations/qbittorrent/logs.go
Normal file
102
internal/homepage/integrations/qbittorrent/logs.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const endpointLogs = "/api/v2/log/main"
|
||||
|
||||
type LogEntry struct {
|
||||
ID int `json:"id"`
|
||||
Timestamp int `json:"timestamp"`
|
||||
Type int `json:"type"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
const (
|
||||
LogSeverityNormal = 1 << iota
|
||||
LogSeverityInfo
|
||||
LogSeverityWarning
|
||||
LogSeverityCritical
|
||||
)
|
||||
|
||||
func (l *LogEntry) Time() time.Time {
|
||||
return time.Unix(int64(l.Timestamp), 0)
|
||||
}
|
||||
|
||||
func (l *LogEntry) Level() string {
|
||||
switch l.Type {
|
||||
case LogSeverityNormal:
|
||||
return "Normal"
|
||||
case LogSeverityInfo:
|
||||
return "Info"
|
||||
case LogSeverityWarning:
|
||||
return "Warning"
|
||||
case LogSeverityCritical:
|
||||
return "Critical"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LogEntry) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"id": l.ID,
|
||||
"timestamp": l.Timestamp,
|
||||
"level": l.Level(),
|
||||
"message": l.Message,
|
||||
})
|
||||
}
|
||||
|
||||
// params:
|
||||
//
|
||||
// normal: bool
|
||||
// info: bool
|
||||
// warning: bool
|
||||
// critical: bool
|
||||
// last_known_id: int
|
||||
func (c *Client) GetLogs(ctx context.Context, lastKnownID int) ([]*LogEntry, error) {
|
||||
return jsonRequest[[]*LogEntry](ctx, c, endpointLogs, url.Values{
|
||||
"last_known_id": {strconv.Itoa(lastKnownID)},
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) WatchLogs(ctx context.Context) (<-chan *LogEntry, <-chan error) {
|
||||
ch := make(chan *LogEntry)
|
||||
errCh := make(chan error)
|
||||
|
||||
lastKnownID := -1
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
defer close(errCh)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
logs, err := c.GetLogs(ctx, lastKnownID)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
}
|
||||
|
||||
if len(logs) == 0 {
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, log := range logs {
|
||||
ch <- log
|
||||
}
|
||||
lastKnownID = logs[len(logs)-1].ID
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch, errCh
|
||||
}
|
||||
32
internal/homepage/integrations/qbittorrent/transfer_info.go
Normal file
32
internal/homepage/integrations/qbittorrent/transfer_info.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/homepage/widgets"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
const endpointTransferInfo = "/api/v2/transfer/info"
|
||||
|
||||
type TransferInfo struct {
|
||||
ConnectionStatus string `json:"connection_status"`
|
||||
SessionDownloads uint64 `json:"dl_info_data"`
|
||||
SessionUploads uint64 `json:"up_info_data"`
|
||||
DownloadSpeed uint64 `json:"dl_info_speed"`
|
||||
UploadSpeed uint64 `json:"up_info_speed"`
|
||||
}
|
||||
|
||||
func (c *Client) Data(ctx context.Context) ([]widgets.NameValue, error) {
|
||||
info, err := jsonRequest[TransferInfo](ctx, c, endpointTransferInfo, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []widgets.NameValue{
|
||||
{Name: "Status", Value: info.ConnectionStatus},
|
||||
{Name: "Download", Value: strutils.FormatByteSize(info.SessionDownloads)},
|
||||
{Name: "Upload", Value: strutils.FormatByteSize(info.SessionUploads)},
|
||||
{Name: "Download Speed", Value: strutils.FormatByteSize(info.DownloadSpeed) + "/s"},
|
||||
{Name: "Upload Speed", Value: strutils.FormatByteSize(info.UploadSpeed) + "/s"},
|
||||
}, nil
|
||||
}
|
||||
21
internal/homepage/integrations/qbittorrent/version.go
Normal file
21
internal/homepage/integrations/qbittorrent/version.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
)
|
||||
|
||||
func (c *Client) Version(ctx context.Context) (string, error) {
|
||||
resp, err := c.doRequest(ctx, "GET", "/api/v2/app/version", nil, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(body), nil
|
||||
}
|
||||
14
internal/homepage/widgets/http.go
Normal file
14
internal/homepage/widgets/http.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package widgets
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
)
|
||||
|
||||
var HTTPClient = &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
var ErrHTTPStatus = gperr.New("http status")
|
||||
49
internal/homepage/widgets/widgets.go
Normal file
49
internal/homepage/widgets/widgets.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package widgets
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
type (
|
||||
Config struct {
|
||||
Provider string `json:"provider"`
|
||||
Config Widget `json:"config"`
|
||||
}
|
||||
Widget interface {
|
||||
Initialize(ctx context.Context, url string, cfg map[string]any) error
|
||||
Data(ctx context.Context) ([]NameValue, error)
|
||||
}
|
||||
NameValue struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
WidgetProviderQbittorrent = "qbittorrent"
|
||||
)
|
||||
|
||||
var widgetProviders = map[string]struct{}{
|
||||
WidgetProviderQbittorrent: {},
|
||||
}
|
||||
|
||||
var ErrInvalidProvider = gperr.New("invalid provider")
|
||||
|
||||
func (cfg *Config) UnmarshalMap(m map[string]any) error {
|
||||
cfg.Provider = m["provider"].(string)
|
||||
if _, ok := widgetProviders[cfg.Provider]; !ok {
|
||||
return ErrInvalidProvider.Subject(cfg.Provider)
|
||||
}
|
||||
delete(m, "provider")
|
||||
m, ok := m["config"].(map[string]any)
|
||||
if !ok {
|
||||
return gperr.New("invalid config")
|
||||
}
|
||||
if err := utils.MapUnmarshalValidate(m, &cfg.Config); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user