Skip to content

Commit e627329

Browse files
committed
feat: add full kodi / xmbc support
1 parent 8e07e67 commit e627329

11 files changed

Lines changed: 333 additions & 8 deletions

File tree

README.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,35 +69,38 @@ go build -o apprise-go ./cmd/apprise
6969

7070
```bash
7171
# Send a simple notification
72-
apprise-go -u "discord://webhook_id/webhook_token" -m "Hello from apprise-go!"
72+
apprise -b "Hello from apprise!" "discord://webhook_id/webhook_token"
7373
```
7474

7575
### Multiple Services
7676

7777
```bash
7878
# Send to multiple services at once
79-
apprise-go -u "discord://..." -u "slack://..." -m "Multi-platform notification"
79+
apprise -b "Multi-platform notification" "discord://..." "slack://..."
8080
```
8181

8282
### With Title
8383

8484
```bash
8585
# Add a title to your notification
86-
apprise-go -u "discord://..." -t "Alert" -m "Something happened!"
86+
apprise -t "Alert" -b "Something happened!" "discord://..."
8787
```
8888

89-
### Schema Information
89+
### Schema and Service Information
9090

9191
```bash
92-
# Get configuration details for a service
93-
apprise-go --schema discord
92+
# Get full Apprise schema as JSON
93+
apprise --schema
94+
95+
# Print details about all supported services
96+
apprise --details
9497
```
9598

9699
### Storage Support
97100

98101
```bash
99102
# Store and reuse configurations
100-
apprise-go --storage /path/to/config -m "Using saved config"
103+
apprise --storage-path /path/to/config -b "Using saved config"
101104
```
102105

103106
## Releases

internal/cli/cli.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1387,6 +1387,17 @@ func Run(args []string, stdout, stderr io.Writer) int {
13871387
fmt.Fprintf(stderr, "emby notify error: %s\n", err)
13881388
failed = true
13891389
}
1390+
case "xbmc", "xbmcs", "kodi", "kodis":
1391+
xbmcTarget, err := notify.NewXBMCTarget(parsed)
1392+
if err != nil {
1393+
fmt.Fprintf(stderr, "xbmc target error: %s\n", err)
1394+
failed = true
1395+
continue
1396+
}
1397+
if err := xbmcTarget.Send(body, title, nt); err != nil {
1398+
fmt.Fprintf(stderr, "xbmc notify error: %s\n", err)
1399+
failed = true
1400+
}
13901401
case "hassio", "hassios":
13911402
hassTarget, err := notify.NewHomeAssistantTarget(parsed)
13921403
if err != nil {

internal/notify/registry_push.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ var pushSchemas = map[string]struct{}{
1616
"fcm": {},
1717
"hassio": {},
1818
"hassios": {},
19+
"kodi": {},
20+
"kodis": {},
1921
"kumulos": {},
2022
"lametric": {},
2123
"lametrics": {},
@@ -71,4 +73,6 @@ var pushSchemas = map[string]struct{}{
7173
"workflow": {},
7274
"workflows": {},
7375
"wxpusher": {},
76+
"xbmc": {},
77+
"xbmcs": {},
7478
}

internal/notify/schema_inputs.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,10 @@ func adjustSchemaValues(specs schemaSpecs, target *ParsedURL, values map[string]
224224
if _, ok := target.Query["from"]; !ok {
225225
delete(values, "from_addr")
226226
}
227+
case "xbmc", "xbmcs":
228+
if portValue, ok := values["port"]; !ok || portValue.Value == nil {
229+
values["port"] = schemaValueInt(8080)
230+
}
227231
case "seven":
228232
if _, ok := values["label"]; !ok {
229233
values["label"] = schemaValueAny(nil)

internal/notify/xbmc_target.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package notify
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/url"
7+
"strconv"
8+
"strings"
9+
)
10+
11+
const (
12+
xbmcDefaultPort = 8080
13+
xbmcRemoteProtocol = 2
14+
kodiRemoteProtocol = 6
15+
xbmcImageSize = "128x128"
16+
xbmcJSONRPCPath = "/jsonrpc"
17+
xbmcNotifyTypeInfo = "info"
18+
xbmcNotifyTypeWarn = "warning"
19+
xbmcNotifyTypeError = "error"
20+
)
21+
22+
type XBMCTarget struct {
23+
host string
24+
port int
25+
secure bool
26+
user string
27+
password string
28+
protocol int
29+
includeImage bool
30+
duration int
31+
}
32+
33+
func NewXBMCTarget(target *ParsedURL) (*XBMCTarget, error) {
34+
if target == nil {
35+
return nil, fmt.Errorf("missing target")
36+
}
37+
38+
host := strings.TrimSpace(target.Host)
39+
if host == "" {
40+
return nil, fmt.Errorf("missing host")
41+
}
42+
43+
schema := strings.ToLower(strings.TrimSpace(target.Scheme))
44+
protocol := kodiRemoteProtocol
45+
if strings.HasPrefix(schema, "xbmc") {
46+
protocol = xbmcRemoteProtocol
47+
}
48+
49+
secure := strings.HasSuffix(schema, "s")
50+
port := target.Port
51+
if !target.HasPort && protocol == xbmcRemoteProtocol {
52+
port = xbmcDefaultPort
53+
}
54+
55+
includeImage := parseBoolWithDefault(target.Query["image"], true)
56+
duration := 12
57+
if raw := strings.TrimSpace(target.Query["duration"]); raw != "" {
58+
if parsed, err := strconv.Atoi(raw); err == nil {
59+
if parsed < 0 {
60+
parsed = -parsed
61+
}
62+
duration = parsed
63+
}
64+
}
65+
66+
return &XBMCTarget{
67+
host: host,
68+
port: port,
69+
secure: secure,
70+
user: strings.TrimSpace(target.User),
71+
password: target.Password,
72+
protocol: protocol,
73+
includeImage: includeImage,
74+
duration: duration,
75+
}, nil
76+
}
77+
78+
func (x *XBMCTarget) BuildRequest(body, title string, notifyType NotifyType) (RequestSpec, error) {
79+
payload := map[string]any{
80+
"jsonrpc": "2.0",
81+
"method": "GUI.ShowNotification",
82+
"params": map[string]any{
83+
"title": title,
84+
"message": body,
85+
"displaytime": int(x.duration * 1000),
86+
},
87+
"id": 1,
88+
}
89+
90+
if x.includeImage {
91+
imageURL := appriseImageURL(notifyType, xbmcImageSize)
92+
if imageURL != "" {
93+
payload["params"].(map[string]any)["image"] = imageURL
94+
if x.protocol != xbmcRemoteProtocol {
95+
switch notifyType {
96+
case NotifyFailure:
97+
payload["type"] = xbmcNotifyTypeError
98+
case NotifyWarning:
99+
payload["type"] = xbmcNotifyTypeWarn
100+
default:
101+
payload["type"] = xbmcNotifyTypeInfo
102+
}
103+
}
104+
}
105+
}
106+
107+
data, err := json.Marshal(payload)
108+
if err != nil {
109+
return RequestSpec{}, err
110+
}
111+
112+
headers := map[string]string{
113+
"User-Agent": "Apprise",
114+
"Content-Type": "application/json",
115+
}
116+
if x.user != "" {
117+
headers["Authorization"] = basicAuthHeader(x.user, x.password)
118+
}
119+
120+
return RequestSpec{
121+
Method: "POST",
122+
URL: x.notifyURL(),
123+
Headers: headers,
124+
Body: string(data),
125+
}, nil
126+
}
127+
128+
func (x *XBMCTarget) Send(body, title string, notifyType NotifyType) error {
129+
spec, err := x.BuildRequest(body, title, notifyType)
130+
if err != nil {
131+
return err
132+
}
133+
134+
return SendRequest(spec)
135+
}
136+
137+
func (x *XBMCTarget) notifyURL() string {
138+
scheme := "http"
139+
if x.secure {
140+
scheme = "https"
141+
}
142+
143+
host := x.host
144+
if x.port > 0 {
145+
host = host + ":" + strconv.Itoa(x.port)
146+
}
147+
148+
u := url.URL{
149+
Scheme: scheme,
150+
Host: host,
151+
Path: xbmcJSONRPCPath,
152+
}
153+
154+
return u.String()
155+
}

internal/parity/provider_parity_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ func TestProviderRequestParity(t *testing.T) {
1313

1414
for _, name := range sortedProviderNames(defs) {
1515
def := defs[name]
16+
golden := loadProviderGolden(t, def.Dir)
17+
goldenByName := map[string]goldenCase{}
18+
for _, g := range golden {
19+
goldenByName[g.Name] = g
20+
}
1621
builder, ok := providerBuilders[name]
1722
if !ok {
1823
t.Fatalf("missing provider builder for %s", name)
@@ -33,6 +38,11 @@ func TestProviderRequestParity(t *testing.T) {
3338
}
3439

3540
pythonSpecs, pythonSuccess := testutil.CapturePythonRequestsWithTypeResult(t, c.URL, c.Body, c.Title, notifyType)
41+
if expected, ok := goldenByName[c.Name]; ok {
42+
assertRequestSpecSequenceMatches(t, pythonSpecs, expected.Requests)
43+
} else {
44+
t.Fatalf("missing golden case for %s/%s", name, c.Name)
45+
}
3646
parsedURL, err := notify.ParseURL(c.URL)
3747
if err != nil {
3848
t.Fatalf("parse url: %v", err)

internal/parity/provider_registry_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ var providerBuilders = map[string]buildTargetFunc{
5757
"emby": func(parsed *notify.ParsedURL) (requestSender, error) {
5858
return notify.NewEmbyTarget(parsed)
5959
},
60+
"kodi": func(parsed *notify.ParsedURL) (requestSender, error) {
61+
return notify.NewXBMCTarget(parsed)
62+
},
6063
"kumulos": func(parsed *notify.ParsedURL) (requestSender, error) {
6164
return notify.NewKumulosTarget(parsed)
6265
},
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[
2+
{
3+
"name": "xbmc-default",
4+
"url": "xbmc://user:pass@kodi.local",
5+
"body": "hello from python",
6+
"title": "apprise parity"
7+
},
8+
{
9+
"name": "xbmcs-secure",
10+
"url": "xbmcs://user:pass@kodi.local",
11+
"body": "hello from python",
12+
"title": "apprise parity"
13+
},
14+
{
15+
"name": "kodi-default",
16+
"url": "kodi://user:pass@kodi.local",
17+
"body": "hello from python",
18+
"title": "apprise parity"
19+
},
20+
{
21+
"name": "kodis-secure",
22+
"url": "kodis://user:pass@kodi.local",
23+
"body": "hello from python",
24+
"title": "apprise parity"
25+
}
26+
]
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
[
2+
{
3+
"name": "xbmc-default",
4+
"requests": [
5+
{
6+
"body": "{\"jsonrpc\": \"2.0\", \"method\": \"GUI.ShowNotification\", \"params\": {\"title\": \"apprise parity\", \"message\": \"hello from python\", \"displaytime\": 12000, \"image\": \"https://raw.githubusercontent.com/unraid/apprise-go/main/assets/themes/default/apprise-info-128x128.png\"}, \"id\": 1}",
7+
"headers": {
8+
"accept": "*/*",
9+
"authorization": "Basic dXNlcjpwYXNz",
10+
"content-type": "application/json",
11+
"user-agent": "Apprise"
12+
},
13+
"method": "POST",
14+
"url": "http://kodi.local:8080/jsonrpc"
15+
}
16+
]
17+
},
18+
{
19+
"name": "xbmcs-secure",
20+
"requests": [
21+
{
22+
"body": "{\"jsonrpc\": \"2.0\", \"method\": \"GUI.ShowNotification\", \"params\": {\"title\": \"apprise parity\", \"message\": \"hello from python\", \"displaytime\": 12000, \"image\": \"https://raw.githubusercontent.com/unraid/apprise-go/main/assets/themes/default/apprise-info-128x128.png\"}, \"id\": 1}",
23+
"headers": {
24+
"accept": "*/*",
25+
"authorization": "Basic dXNlcjpwYXNz",
26+
"content-type": "application/json",
27+
"user-agent": "Apprise"
28+
},
29+
"method": "POST",
30+
"url": "https://kodi.local:8080/jsonrpc"
31+
}
32+
]
33+
},
34+
{
35+
"name": "kodi-default",
36+
"requests": [
37+
{
38+
"body": "{\"jsonrpc\": \"2.0\", \"method\": \"GUI.ShowNotification\", \"params\": {\"title\": \"apprise parity\", \"message\": \"hello from python\", \"displaytime\": 12000, \"image\": \"https://raw.githubusercontent.com/unraid/apprise-go/main/assets/themes/default/apprise-info-128x128.png\"}, \"id\": 1, \"type\": \"info\"}",
39+
"headers": {
40+
"accept": "*/*",
41+
"authorization": "Basic dXNlcjpwYXNz",
42+
"content-type": "application/json",
43+
"user-agent": "Apprise"
44+
},
45+
"method": "POST",
46+
"url": "http://kodi.local/jsonrpc"
47+
}
48+
]
49+
},
50+
{
51+
"name": "kodis-secure",
52+
"requests": [
53+
{
54+
"body": "{\"jsonrpc\": \"2.0\", \"method\": \"GUI.ShowNotification\", \"params\": {\"title\": \"apprise parity\", \"message\": \"hello from python\", \"displaytime\": 12000, \"image\": \"https://raw.githubusercontent.com/unraid/apprise-go/main/assets/themes/default/apprise-info-128x128.png\"}, \"id\": 1, \"type\": \"info\"}",
55+
"headers": {
56+
"accept": "*/*",
57+
"authorization": "Basic dXNlcjpwYXNz",
58+
"content-type": "application/json",
59+
"user-agent": "Apprise"
60+
},
61+
"method": "POST",
62+
"url": "https://kodi.local/jsonrpc"
63+
}
64+
]
65+
}
66+
]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "kodi",
3+
"schemas": ["xbmc", "xbmcs", "kodi", "kodis"]
4+
}

0 commit comments

Comments
 (0)