aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--docs/customservices.md41
-rw-r--r--src/assets/app.scss2
-rw-r--r--src/components/Message.vue2
-rw-r--r--src/components/services/Medusa.vue128
-rw-r--r--src/components/services/Radarr.vue157
-rw-r--r--src/components/services/Sonarr.vue157
6 files changed, 485 insertions, 2 deletions
diff --git a/docs/customservices.md b/docs/customservices.md
new file mode 100644
index 0000000..d2deff4
--- /dev/null
+++ b/docs/customservices.md
@@ -0,0 +1,41 @@
1# Custom Services
2
3Here is an overview of all custom services that are available within Homer.
4
5## PiHole
6
7Using the PiHole service you can display info about your local PiHole instance right on your Homer dashboard.
8
9The following configuration is available for the PiHole service.
10
11```
12 items:
13 - name: "Pi-hole"
14 logo: "assets/tools/sample.png"
15 # subtitle: "Network-wide Ad Blocking" # optional, if no subtitle is defined, PiHole statistics will be shown
16 url: "http://192.168.0.151/admin"
17 type: "PiHole"
18```
19
20## Medusa
21
22This service displays News (grey), Warning (orange) or Error (red) notifications bubbles from the Medusa application.
23Two lines are needed in the config.yml :
24```
25type: "Medusa"
26apikey: "01234deb70424befb1f4ef6a23456789"
27```
28The url must be the root url of Medusa application.
29The Medusa API key can be found in General configuration > Interface. It is needed to access Medusa API.
30
31
32## Sonarr/Radarr
33
34This service displays Activity (blue), Warning (orange) or Error (red) notifications bubbles from the Radarr/Sonarr application.
35Two lines are needed in the config.yml :
36```
37type: "Radarr" or "Sonarr"
38apikey: "01234deb70424befb1f4ef6a23456789"
39```
40The url must be the root url of Radarr/Sonarr application.
41The Radarr/Sonarr API key can be found in Settings > General. It is needed to access the API.
diff --git a/src/assets/app.scss b/src/assets/app.scss
index 48c60ae..d94a068 100644
--- a/src/assets/app.scss
+++ b/src/assets/app.scss
@@ -211,7 +211,7 @@ body {
211 color: var(--highlight-secondary); 211 color: var(--highlight-secondary);
212 background-color: var(--highlight-secondary); 212 background-color: var(--highlight-secondary);
213 position: absolute; 213 position: absolute;
214 top: 1rem; 214 bottom: 1rem;
215 right: -0.2rem; 215 right: -0.2rem;
216 width: 3px; 216 width: 3px;
217 overflow: hidden; 217 overflow: hidden;
diff --git a/src/components/Message.vue b/src/components/Message.vue
index 6cc649a..00ce158 100644
--- a/src/components/Message.vue
+++ b/src/components/Message.vue
@@ -54,7 +54,7 @@ export default {
54 54
55 // keep the original config value if no value is provided by the endpoint 55 // keep the original config value if no value is provided by the endpoint
56 const message = this.message; 56 const message = this.message;
57 for (const prop of ["title", "style", "content"]) { 57 for (const prop of ["title", "style", "content", "icon"]) {
58 if (prop in fetchedMessage && fetchedMessage[prop] !== null) { 58 if (prop in fetchedMessage && fetchedMessage[prop] !== null) {
59 message[prop] = fetchedMessage[prop]; 59 message[prop] = fetchedMessage[prop];
60 } 60 }
diff --git a/src/components/services/Medusa.vue b/src/components/services/Medusa.vue
new file mode 100644
index 0000000..5720649
--- /dev/null
+++ b/src/components/services/Medusa.vue
@@ -0,0 +1,128 @@
1<template>
2 <div>
3 <div class="card" :class="item.class">
4 <a :href="item.url" :target="item.target" rel="noreferrer">
5 <div class="card-content">
6 <div class="media">
7 <div v-if="item.logo" class="media-left">
8 <figure class="image is-48x48">
9 <img :src="item.logo" :alt="`${item.name} logo`" />
10 </figure>
11 </div>
12 <div v-if="item.icon" class="media-left">
13 <figure class="image is-48x48">
14 <i style="font-size: 35px" :class="['fa-fw', item.icon]"></i>
15 </figure>
16 </div>
17 <div class="media-content">
18 <p class="title is-4">{{ item.name }}</p>
19 <p class="subtitle is-6">{{ item.subtitle }}</p>
20 </div>
21 <div class="notifs">
22 <strong
23 v-if="config !== null && config.system.news.unread > 0"
24 class="notif news"
25 title="News"
26 >{{ config.system.news.unread }}</strong
27 >
28 <strong
29 v-if="config !== null && config.main.logs.numWarnings > 0"
30 class="notif warnings"
31 title="Warning"
32 >{{ config.main.logs.numWarnings }}</strong
33 >
34 <strong
35 v-if="config !== null && config.main.logs.numErrors > 0"
36 class="notif errors"
37 title="Error"
38 >{{ config.main.logs.numErrors }}</strong
39 >
40 <strong
41 v-if="serverError"
42 class="notif errors"
43 title="Connection error to Medusa API, check url and apikey in config.yml"
44 >?</strong
45 >
46 </div>
47 </div>
48 <div class="tag" :class="item.tagstyle" v-if="item.tag">
49 <strong class="tag-text">#{{ item.tag }}</strong>
50 </div>
51 </div>
52 </a>
53 </div>
54 </div>
55</template>
56
57<script>
58export default {
59 name: "Medusa",
60 props: {
61 item: Object,
62 },
63 data: () => {
64 return {
65 config: null,
66 serverError: false,
67 };
68 },
69 created: function () {
70 this.fetchConfig();
71 },
72 methods: {
73 fetchConfig: function () {
74 fetch(`${this.item.url}/api/v2/config`, {
75 credentials: "include",
76 headers: { "X-Api-Key": `${this.item.apikey}` },
77 })
78 .then((response) => {
79 if (response.status != 200) {
80 throw new Error(response.statusText);
81 }
82 return response.json();
83 })
84 .then((conf) => {
85 this.config = conf;
86 })
87 .catch((e) => {
88 console.log(e);
89 this.serverError = true;
90 });
91 },
92 },
93};
94</script>
95
96<style scoped lang="scss">
97.media-left img {
98 max-height: 100%;
99}
100.notifs {
101 position: absolute;
102 color: white;
103 font-family: sans-serif;
104 top: 0.3em;
105 right: 0.5em;
106}
107.notif {
108 padding-right: 0.35em;
109 padding-left: 0.35em;
110 padding-top: 0.2em;
111 padding-bottom: 0.2em;
112 border-radius: 0.25em;
113 position: relative;
114 margin-left: 0.3em;
115 font-size: 0.8em;
116}
117.news {
118 background-color: #777777;
119}
120
121.warnings {
122 background-color: #d08d2e;
123}
124
125.errors {
126 background-color: #e51111;
127}
128</style>
diff --git a/src/components/services/Radarr.vue b/src/components/services/Radarr.vue
new file mode 100644
index 0000000..93831a7
--- /dev/null
+++ b/src/components/services/Radarr.vue
@@ -0,0 +1,157 @@
1<template>
2 <div>
3 <div class="card" :class="item.class">
4 <a :href="item.url" :target="item.target" rel="noreferrer">
5 <div class="card-content">
6 <div class="media">
7 <div v-if="item.logo" class="media-left">
8 <figure class="image is-48x48">
9 <img :src="item.logo" :alt="`${item.name} logo`" />
10 </figure>
11 </div>
12 <div v-if="item.icon" class="media-left">
13 <figure class="image is-48x48">
14 <i style="font-size: 35px" :class="['fa-fw', item.icon]"></i>
15 </figure>
16 </div>
17 <div class="media-content">
18 <p class="title is-4">{{ item.name }}</p>
19 <p class="subtitle is-6">{{ item.subtitle }}</p>
20 </div>
21 <div class="notifs">
22 <strong
23 v-if="activity > 0"
24 class="notif activity"
25 title="Activity"
26 >{{ activity }}</strong
27 >
28 <strong
29 v-if="warnings > 0"
30 class="notif warnings"
31 title="Warning"
32 >{{ warnings }}</strong
33 >
34 <strong v-if="errors > 0" class="notif errors" title="Error">{{
35 errors
36 }}</strong>
37 <strong
38 v-if="serverError"
39 class="notif errors"
40 title="Connection error to Radarr API, check url and apikey in config.yml"
41 >?</strong
42 >
43 </div>
44 </div>
45 <div class="tag" :class="item.tagstyle" v-if="item.tag">
46 <strong class="tag-text">#{{ item.tag }}</strong>
47 </div>
48 </div>
49 </a>
50 </div>
51 </div>
52</template>
53
54<script>
55export default {
56 name: "Radarr",
57 props: {
58 item: Object,
59 },
60 data: () => {
61 return {
62 activity: null,
63 warnings: null,
64 errors: null,
65 serverError: false,
66 };
67 },
68 created: function () {
69 this.fetchConfig();
70 },
71 methods: {
72 fetchConfig: function () {
73 fetch(`${this.item.url}/api/health`, {
74 credentials: "include",
75 headers: { "X-Api-Key": `${this.item.apikey}` },
76 })
77 .then((response) => {
78 if (response.status != 200) {
79 throw new Error(response.statusText);
80 }
81 return response.json();
82 })
83 .then((health) => {
84 this.warnings = 0;
85 this.errors = 0;
86 for (var i = 0; i < health.length; i++) {
87 if (health[i].type == "warning") {
88 this.warnings++;
89 } else if (health[i].type == "error") {
90 this.errors++;
91 }
92 }
93 })
94 .catch((e) => {
95 console.error(e);
96 this.serverError = true;
97 });
98 fetch(`${this.item.url}/api/queue`, {
99 credentials: "include",
100 headers: { "X-Api-Key": `${this.item.apikey}` },
101 })
102 .then((response) => {
103 if (response.status != 200) {
104 throw new Error(response.statusText);
105 }
106 return response.json();
107 })
108 .then((queue) => {
109 this.activity = 0;
110 for (var i = 0; i < queue.length; i++) {
111 if (queue[i].movie) {
112 this.activity++;
113 }
114 }
115 })
116 .catch((e) => {
117 console.error(e);
118 this.serverError = true;
119 });
120 },
121 },
122};
123</script>
124
125<style scoped lang="scss">
126.media-left img {
127 max-height: 100%;
128}
129.notifs {
130 position: absolute;
131 color: white;
132 font-family: sans-serif;
133 top: 0.3em;
134 right: 0.5em;
135}
136.notif {
137 padding-right: 0.35em;
138 padding-left: 0.35em;
139 padding-top: 0.2em;
140 padding-bottom: 0.2em;
141 border-radius: 0.25em;
142 position: relative;
143 margin-left: 0.3em;
144 font-size: 0.8em;
145}
146.activity {
147 background-color: #4fb5d6;
148}
149
150.warnings {
151 background-color: #d08d2e;
152}
153
154.errors {
155 background-color: #e51111;
156}
157</style>
diff --git a/src/components/services/Sonarr.vue b/src/components/services/Sonarr.vue
new file mode 100644
index 0000000..8cebac4
--- /dev/null
+++ b/src/components/services/Sonarr.vue
@@ -0,0 +1,157 @@
1<template>
2 <div>
3 <div class="card" :class="item.class">
4 <a :href="item.url" :target="item.target" rel="noreferrer">
5 <div class="card-content">
6 <div class="media">
7 <div v-if="item.logo" class="media-left">
8 <figure class="image is-48x48">
9 <img :src="item.logo" :alt="`${item.name} logo`" />
10 </figure>
11 </div>
12 <div v-if="item.icon" class="media-left">
13 <figure class="image is-48x48">
14 <i style="font-size: 35px" :class="['fa-fw', item.icon]"></i>
15 </figure>
16 </div>
17 <div class="media-content">
18 <p class="title is-4">{{ item.name }}</p>
19 <p class="subtitle is-6">{{ item.subtitle }}</p>
20 </div>
21 <div class="notifs">
22 <strong
23 v-if="activity > 0"
24 class="notif activity"
25 title="Activity"
26 >{{ activity }}</strong
27 >
28 <strong
29 v-if="warnings > 0"
30 class="notif warnings"
31 title="Warning"
32 >{{ warnings }}</strong
33 >
34 <strong v-if="errors > 0" class="notif errors" title="Error">{{
35 errors
36 }}</strong>
37 <strong
38 v-if="serverError"
39 class="notif errors"
40 title="Connection error to Sonarr API, check url and apikey in config.yml"
41 >?</strong
42 >
43 </div>
44 </div>
45 <div class="tag" :class="item.tagstyle" v-if="item.tag">
46 <strong class="tag-text">#{{ item.tag }}</strong>
47 </div>
48 </div>
49 </a>
50 </div>
51 </div>
52</template>
53
54<script>
55export default {
56 name: "Sonarr",
57 props: {
58 item: Object,
59 },
60 data: () => {
61 return {
62 activity: null,
63 warnings: null,
64 errors: null,
65 serverError: false,
66 };
67 },
68 created: function () {
69 this.fetchConfig();
70 },
71 methods: {
72 fetchConfig: function () {
73 fetch(`${this.item.url}/api/health`, {
74 credentials: "include",
75 headers: { "X-Api-Key": `${this.item.apikey}` },
76 })
77 .then((response) => {
78 if (response.status != 200) {
79 throw new Error(response.statusText);
80 }
81 return response.json();
82 })
83 .then((health) => {
84 this.warnings = 0;
85 this.errors = 0;
86 for (var i = 0; i < health.length; i++) {
87 if (health[i].type == "warning") {
88 this.warnings++;
89 } else if (health[i].type == "error") {
90 this.errors++;
91 }
92 }
93 })
94 .catch((e) => {
95 console.error(e);
96 this.serverError = true;
97 });
98 fetch(`${this.item.url}/api/queue`, {
99 credentials: "include",
100 headers: { "X-Api-Key": `${this.item.apikey}` },
101 })
102 .then((response) => {
103 if (response.status != 200) {
104 throw new Error(response.statusText);
105 }
106 return response.json();
107 })
108 .then((queue) => {
109 this.activity = 0;
110 for (var i = 0; i < queue.length; i++) {
111 if (queue[i].series) {
112 this.activity++;
113 }
114 }
115 })
116 .catch((e) => {
117 console.error(e);
118 this.serverError = true;
119 });
120 },
121 },
122};
123</script>
124
125<style scoped lang="scss">
126.media-left img {
127 max-height: 100%;
128}
129.notifs {
130 position: absolute;
131 color: white;
132 font-family: sans-serif;
133 top: 0.3em;
134 right: 0.5em;
135}
136.notif {
137 padding-right: 0.35em;
138 padding-left: 0.35em;
139 padding-top: 0.2em;
140 padding-bottom: 0.2em;
141 border-radius: 0.25em;
142 position: relative;
143 margin-left: 0.3em;
144 font-size: 0.8em;
145}
146.activity {
147 background-color: #4fb5d6;
148}
149
150.warnings {
151 background-color: #d08d2e;
152}
153
154.errors {
155 background-color: #e51111;
156}
157</style>