From 8df05d57c77e0dee00bb8dc98a710a190b684d38 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 9 Oct 2024 09:06:15 +0200 Subject: [PATCH 1/7] Able to edit settings in interface --- api/routes.go | 8 +- api/settings.go | 64 ++++++++ app.go | 19 ++- sources/fields.go | 53 ++++++ ui/routes.go | 1 + ui/src/lib/components/SettingInput.svelte | 58 +++++++ .../lib/components/SettingInputBasic.svelte | 31 ++++ ui/src/lib/components/SettingsButton.svelte | 20 +++ ui/src/lib/custom_source.js | 26 +++ ui/src/lib/settings.js | 54 +++++++ ui/src/lib/stores/loadable-sources.js | 24 +++ ui/src/routes/+layout.svelte | 5 + ui/src/routes/settings/+page.js | 24 +++ ui/src/routes/settings/+page.svelte | 153 ++++++++++++++++++ 14 files changed, 538 insertions(+), 2 deletions(-) create mode 100644 api/settings.go create mode 100644 sources/fields.go create mode 100644 ui/src/lib/components/SettingInput.svelte create mode 100644 ui/src/lib/components/SettingInputBasic.svelte create mode 100644 ui/src/lib/components/SettingsButton.svelte create mode 100644 ui/src/lib/custom_source.js create mode 100644 ui/src/lib/settings.js create mode 100644 ui/src/lib/stores/loadable-sources.js create mode 100644 ui/src/routes/settings/+page.js create mode 100644 ui/src/routes/settings/+page.svelte diff --git a/api/routes.go b/api/routes.go index 7473389..1677bb1 100644 --- a/api/routes.go +++ b/api/routes.go @@ -4,12 +4,18 @@ import ( "github.com/gin-gonic/gin" "git.nemunai.re/nemunaire/hathoris/config" + "git.nemunai.re/nemunaire/hathoris/settings" ) -func DeclareRoutes(router *gin.Engine, cfg *config.Config) { +type SettingsGetter func() *settings.Settings + +type SettingsReloader func() error + +func DeclareRoutes(router *gin.Engine, cfg *config.Config, getsettings SettingsGetter, reloadsettings SettingsReloader) { apiRoutes := router.Group("/api") declareInputsRoutes(cfg, apiRoutes) + declareSettingsRoutes(cfg, getsettings, reloadsettings, apiRoutes) declareSourcesRoutes(cfg, apiRoutes) declareVolumeRoutes(cfg, apiRoutes) } diff --git a/api/settings.go b/api/settings.go new file mode 100644 index 0000000..4fc5abe --- /dev/null +++ b/api/settings.go @@ -0,0 +1,64 @@ +package api + +import ( + "fmt" + "log" + "net/http" + + "github.com/gin-gonic/gin" + + "git.nemunai.re/nemunaire/hathoris/config" + "git.nemunai.re/nemunaire/hathoris/settings" + "git.nemunai.re/nemunaire/hathoris/sources" +) + +type loadableSourceExposed struct { + Description string `json:"description"` + Fields []*sources.SourceField `json:"fields"` +} + +func declareSettingsRoutes(cfg *config.Config, getsettings SettingsGetter, reloadsettings SettingsReloader, router *gin.RouterGroup) { + router.GET("/settings", func(c *gin.Context) { + c.JSON(http.StatusOK, getsettings()) + }) + + router.POST("/settings", func(c *gin.Context) { + var params settings.Settings + + // Parse settings + err := c.ShouldBindJSON(¶ms) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Something is wrong in received settings: %s", err.Error())}) + return + } + + // Reload current settings + *getsettings() = params + + // Save settings + getsettings().Save(cfg.SettingsPath) + + err = reloadsettings() + if err != nil { + log.Println("Unable to reload settings:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to reload settings: %s", err.Error())}) + return + } + + c.JSON(http.StatusOK, getsettings()) + }) + + router.GET("/settings/loadable_sources", func(c *gin.Context) { + ret := map[string]loadableSourceExposed{} + + for k, ls := range sources.LoadableSources { + ret[k] = loadableSourceExposed{ + Description: ls.Description, + Fields: sources.GenFields(ls.SourceDefinition), + } + } + + c.JSON(http.StatusOK, ret) + }) + +} diff --git a/app.go b/app.go index c0d2d61..5474672 100644 --- a/app.go +++ b/app.go @@ -49,7 +49,9 @@ func NewApp(cfg *config.Config) *App { // Register routes ui.DeclareRoutes(router, cfg) - api.DeclareRoutes(router, cfg) + api.DeclareRoutes(router, cfg, func() *settings.Settings { + return app.settings + }, app.loadCustomSources) router.GET("/api/version", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"version": Version}) @@ -82,6 +84,21 @@ func (app *App) loadCustomSources() error { } } + // Delete old custom sources, no more in config + for id1 := range sources.SoundSources { + found := false + for id2, csrc := range app.settings.CustomSources { + if id1 == fmt.Sprintf("%s-%d", csrc.Source, id2) { + found = true + break + } + } + + if !found { + delete(sources.SoundSources, id1) + } + } + return nil } diff --git a/sources/fields.go b/sources/fields.go new file mode 100644 index 0000000..69a8cfc --- /dev/null +++ b/sources/fields.go @@ -0,0 +1,53 @@ +package sources + +import ( + "reflect" + "strings" +) + +type SourceField struct { + Id string `json:"id"` + Type string `json:"type"` + Label string `json:"label,omitempty"` + Placeholder string `json:"placeholder,omitempty"` + Default interface{} `json:"default,omitempty"` + Required bool `json:"required,omitempty"` + Description string `json:"description,omitempty"` +} + +func GenFields(data interface{}) (fields []*SourceField) { + if data != nil { + dataMeta := reflect.Indirect(reflect.ValueOf(data)).Type() + + for i := 0; i < dataMeta.NumField(); i += 1 { + field := dataMeta.Field(i) + if field.IsExported() { + fields = append(fields, GenField(field)) + } + } + } + return +} + +func GenField(field reflect.StructField) (f *SourceField) { + f = &SourceField{ + Id: field.Name, + Type: field.Type.String(), + Label: field.Tag.Get("label"), + Placeholder: field.Tag.Get("placeholder"), + Default: field.Tag.Get("default"), + Description: field.Tag.Get("description"), + } + + jsonTag := field.Tag.Get("json") + jsonTuples := strings.Split(jsonTag, ",") + if len(jsonTuples) > 0 && len(jsonTuples[0]) > 0 { + f.Id = jsonTuples[0] + } + + if f.Label == "" { + f.Label = field.Name + } + + return +} diff --git a/ui/routes.go b/ui/routes.go index ae7bc9f..b04b498 100644 --- a/ui/routes.go +++ b/ui/routes.go @@ -64,6 +64,7 @@ func DeclareRoutes(router *gin.Engine, cfg *config.Config) { } router.GET("/", serveOrReverse("", cfg)) + router.GET("/settings", serveOrReverse("", cfg)) router.GET("/_app/*_", serveOrReverse("", cfg)) router.GET("/img/*_", serveOrReverse("", cfg)) diff --git a/ui/src/lib/components/SettingInput.svelte b/ui/src/lib/components/SettingInput.svelte new file mode 100644 index 0000000..78d7041 --- /dev/null +++ b/ui/src/lib/components/SettingInput.svelte @@ -0,0 +1,58 @@ + + +{#if field.type.startsWith("[]")} + {#each value as val, k} + {#if k !== 0}
{/if} + dispatch('change', e)} + on:input={(e) => dispatch('input', e)} + /> + {/each} + +{:else} + dispatch('change', e)} + on:input={(e) => dispatch('input', e)} + /> +{/if} diff --git a/ui/src/lib/components/SettingInputBasic.svelte b/ui/src/lib/components/SettingInputBasic.svelte new file mode 100644 index 0000000..4c0a002 --- /dev/null +++ b/ui/src/lib/components/SettingInputBasic.svelte @@ -0,0 +1,31 @@ + + +{#if field.type.endsWith('int')} + dispatch('change', e)} + on:input={(e) => dispatch('input', e)} + > +{:else} + dispatch('change', e)} + on:input={(e) => dispatch('input', e)} + > +{/if} diff --git a/ui/src/lib/components/SettingsButton.svelte b/ui/src/lib/components/SettingsButton.svelte new file mode 100644 index 0000000..eacf3f6 --- /dev/null +++ b/ui/src/lib/components/SettingsButton.svelte @@ -0,0 +1,20 @@ + + + diff --git a/ui/src/lib/custom_source.js b/ui/src/lib/custom_source.js new file mode 100644 index 0000000..737bd8c --- /dev/null +++ b/ui/src/lib/custom_source.js @@ -0,0 +1,26 @@ +export class CustomSource { + constructor(res) { + if (res) { + this.update(res); + } + } + + update({ src, kv }) { + this.src = src; + this.kv = kv; + } +} + +export async function retrieveLoadableSources() { + const res = await fetch(`api/settings/loadable_sources`, {headers: {'Accept': 'application/json'}}) + if (res.status == 200) { + const data = await res.json(); + if (data == null) { + return {} + } else { + return data; + } + } else { + throw new Error((await res.json()).errmsg); + } +} diff --git a/ui/src/lib/settings.js b/ui/src/lib/settings.js new file mode 100644 index 0000000..c562f88 --- /dev/null +++ b/ui/src/lib/settings.js @@ -0,0 +1,54 @@ +import { CustomSource } from '$lib/custom_source.js'; + +export class Settings { + constructor(res) { + if (res) { + this.update(res); + } + } + + update({ custom_sources }) { + if (custom_sources) { + this.custom_sources = custom_sources.map((e) => new CustomSource(e)); + } else { + this.custom_sources = []; + } + } + + async save(avoidStructUpdate) { + const res = await fetch('api/settings', { + headers: {'Accept': 'application/json'}, + method: 'POST', + body: JSON.stringify(this), + }); + if (res.status == 200) { + const data = await res.json(); + if (!avoidStructUpdate) { + if (data == null) { + this.update({}); + } else { + this.update(data); + } + return this; + } else { + return new Settings(data); + } + } else { + throw new Error((await res.json()).errmsg); + } + } +} + +export async function getSettings() { + const res = await fetch(`api/settings`, {headers: {'Accept': 'application/json'}}) + if (res.status == 200) { + const data = await res.json(); + if (data == null) { + return {} + } else { + return new Settings(data); + } + } else { + throw new Error((await res.json()).errmsg); + } +} diff --git a/ui/src/lib/stores/loadable-sources.js b/ui/src/lib/stores/loadable-sources.js new file mode 100644 index 0000000..a883b97 --- /dev/null +++ b/ui/src/lib/stores/loadable-sources.js @@ -0,0 +1,24 @@ +import { derived, writable } from 'svelte/store'; + +import { retrieveLoadableSources } from '$lib/custom_source' + +function createLoadableSourcesStore() { + const { subscribe, set, update } = writable(null); + + return { + subscribe, + + set: (v) => { + update((m) => v); + }, + + refresh: async () => { + const map = await retrieveLoadableSources(); + update((m) => map); + return map; + }, + }; + +} + +export const loadablesSources = createLoadableSourcesStore(); diff --git a/ui/src/routes/+layout.svelte b/ui/src/routes/+layout.svelte index 4490a5c..ade3fe5 100644 --- a/ui/src/routes/+layout.svelte +++ b/ui/src/routes/+layout.svelte @@ -10,6 +10,7 @@ inputs.refresh(); setInterval(inputs.refresh, 4500); + import SettingsButton from '$lib/components/SettingsButton.svelte'; import SourceSelection from '$lib/components/SourceSelection.svelte'; const version = fetch('api/version', {headers: {'Accept': 'application/json'}}).then((res) => res.json()) @@ -21,6 +22,10 @@
+
+ +
+
diff --git a/ui/src/routes/settings/+page.js b/ui/src/routes/settings/+page.js new file mode 100644 index 0000000..252e476 --- /dev/null +++ b/ui/src/routes/settings/+page.js @@ -0,0 +1,24 @@ +import { error } from '@sveltejs/kit'; +import { getSettings } from '$lib/settings'; +import { loadablesSources } from '$lib/stores/loadable-sources.js'; + +export const load = async({ parent, fetch }) => { + const data = await parent(); + + await loadablesSources.refresh(); + + let settings; + try { + settings = await getSettings(); + } catch (err) { + throw error(err.status, err.statusText); + } + + const version = fetch('/api/version').then((res) => res.json()); + + return { + ...data, + settings, + version, + }; +} diff --git a/ui/src/routes/settings/+page.svelte b/ui/src/routes/settings/+page.svelte new file mode 100644 index 0000000..9db3f0d --- /dev/null +++ b/ui/src/routes/settings/+page.svelte @@ -0,0 +1,153 @@ + + +
+
+

+
+
+ + General Settings +
+
+

+
+ Version: + {#await data.version then version} + {version.version} + {/await} +
+
+ +
+

+
+
+ + Custom Sources +
+ +
+

+
+ {#each data.settings.custom_sources as source, isrc} +
+
+
+
source.edit_name = true} + > + {#if source.kv && source.kv.name !== undefined} + {#if !source.edit_name} + {source.kv.name} + {:else} + + {/if} + {:else} + {source.src} #{isrc} + {/if} +
+ {#if source.src} + {source.src} + {/if} +
+ +
+ {#if source.src && source.kv} + {#if $loadablesSources[source.src].fields} + {#each $loadablesSources[source.src].fields.filter((e) => e.id !== "name") as field (field.id)} +
+ + +
+ {/each} + {:else} + {#each Object.keys(source.kv).filter((e) => e !== "name") as k} +
+ + +
+ {/each} + {/if} + {:else} + Choose a new custom source: +
+ {#if $loadablesSources} + {#each Object.keys($loadablesSources) as kls} + + {/each} + {/if} +
+ {/if} +
+ {/each} +
+
+
From a592b310b15a74cec9dd08314391bb590b662f51 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 26 Apr 2025 10:56:02 +0200 Subject: [PATCH 2/7] Also build tag for amd64 and arm64 --- .drone.yml | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/.drone.yml b/.drone.yml index 3f7d64e..a4d1cec 100644 --- a/.drone.yml +++ b/.drone.yml @@ -90,6 +90,120 @@ trigger: - push - tag +--- +kind: pipeline +type: docker +name: build-amd64 + +platform: + os: linux + arch: amd64 + +workspace: + base: /go + path: src/git.nemunai.re/nemunaire/hathoris + +steps: +- name: build front + image: node:21 + commands: + - mkdir deploy + - cd ui + - npm install --network-timeout=100000 + - npm run build + +- name: build + image: golang:1-alpine + commands: + - apk --no-cache add alsa-lib-dev build-base git pkgconf + - go get -v -d + - go vet -v + - go build -v -ldflags '-w -X main.Version=${DRONE_BRANCH}-${DRONE_COMMIT} -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/hathoris-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} + - ln deploy/hathoris-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} hathoris + +- name: gitea release + image: plugins/gitea-release + settings: + api_key: + from_secret: gitea_api_key + base_url: https://git.nemunai.re/ + files: + - deploy/hathoris-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} + +- name: docker + image: plugins/docker + settings: + registry: registry.nemunai.re + repo: registry.nemunai.re/hathoris + auto_tag: true + auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} + dockerfile: Dockerfile-norebuild + username: + from_secret: docker_username + password: + from_secret: docker_password + +trigger: + event: + - tag + +--- +kind: pipeline +type: docker +name: build-arm64 + +platform: + os: linux + arch: arm64 + +workspace: + base: /go + path: src/git.nemunai.re/nemunaire/hathoris + +steps: +- name: build front + image: node:21 + commands: + - mkdir deploy + - cd ui + - npm install --network-timeout=100000 + - npm run build + +- name: build + image: golang:1-alpine + commands: + - apk --no-cache add alsa-lib-dev build-base git pkgconf + - go get -v -d + - go vet -v + - go build -v -ldflags '-w -X main.Version=${DRONE_BRANCH}-${DRONE_COMMIT} -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/hathoris-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} + - ln deploy/hathoris-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} hathoris + +- name: gitea release + image: plugins/gitea-release + settings: + api_key: + from_secret: gitea_api_key + base_url: https://git.nemunai.re/ + files: + - deploy/hathoris-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} + +- name: docker + image: plugins/docker + settings: + registry: registry.nemunai.re + repo: registry.nemunai.re/hathoris + auto_tag: true + auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} + dockerfile: Dockerfile-norebuild + username: + from_secret: docker_username + password: + from_secret: docker_password + +trigger: + event: + - tag + --- kind: pipeline name: docker-manifest @@ -113,4 +227,6 @@ trigger: - tag depends_on: +- build-amd64 +- build-arm64 - build-arm From b82316752d17ee2dc892dc1fa60e5556b24148ce Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 26 Apr 2025 12:24:59 +0200 Subject: [PATCH 3/7] Deploy containers to Docker Hub --- .drone-manifest.yml | 8 ++++---- .drone.yml | 9 +++------ 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.drone-manifest.yml b/.drone-manifest.yml index 70818c7..c87ef06 100644 --- a/.drone-manifest.yml +++ b/.drone-manifest.yml @@ -1,4 +1,4 @@ -image: registry.nemunai.re/hathoris:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}} +image: nemunaire/hathoris:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}} {{#if build.tags}} tags: {{#each build.tags}} @@ -6,16 +6,16 @@ tags: {{/each}} {{/if}} manifests: - - image: registry.nemunai.re/hathoris:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64 + - image: nemunaire/hathoris:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64 platform: architecture: amd64 os: linux - - image: registry.nemunai.re/hathoris:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64 + - image: nemunaire/hathoris:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64 platform: architecture: arm64 os: linux variant: v8 - - image: registry.nemunai.re/hathoris:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm + - image: nemunaire/hathoris:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm platform: architecture: arm os: linux diff --git a/.drone.yml b/.drone.yml index a4d1cec..19abf21 100644 --- a/.drone.yml +++ b/.drone.yml @@ -74,8 +74,7 @@ steps: - name: docker image: plugins/docker:linux-arm settings: - registry: registry.nemunai.re - repo: registry.nemunai.re/hathoris + repo: nemunaire/hathoris auto_tag: true auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} dockerfile: Dockerfile-norebuild @@ -133,8 +132,7 @@ steps: - name: docker image: plugins/docker settings: - registry: registry.nemunai.re - repo: registry.nemunai.re/hathoris + repo: nemunaire/hathoris auto_tag: true auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} dockerfile: Dockerfile-norebuild @@ -190,8 +188,7 @@ steps: - name: docker image: plugins/docker settings: - registry: registry.nemunai.re - repo: registry.nemunai.re/hathoris + repo: nemunaire/hathoris auto_tag: true auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} dockerfile: Dockerfile-norebuild From 2b38d0d4b20e89104e66a07b5ffb2d584ac4b928 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 26 Apr 2025 12:33:32 +0200 Subject: [PATCH 4/7] Deploy binaries on Github --- .drone.yml | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/.drone.yml b/.drone.yml index 19abf21..272d2ac 100644 --- a/.drone.yml +++ b/.drone.yml @@ -71,6 +71,19 @@ steps: event: - tag +- name: github release + image: plugins/github-release:linux-arm + settings: + api_key: + from_secret: github_api_token + github_url: https://github.com + files: + - deploy/hathoris-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}hf + - deploy/hathoris-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}v7 + when: + event: + - tag + - name: docker image: plugins/docker:linux-arm settings: @@ -129,6 +142,18 @@ steps: files: - deploy/hathoris-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} +- name: github release + image: plugins/github-release + settings: + api_key: + from_secret: github_api_token + github_url: https://github.com + files: + - deploy/hathoris-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} + when: + event: + - tag + - name: docker image: plugins/docker settings: @@ -185,6 +210,18 @@ steps: files: - deploy/hathoris-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} +- name: github release + image: plugins/github-release + settings: + api_key: + from_secret: github_api_token + github_url: https://github.com + files: + - deploy/hathoris-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} + when: + event: + - tag + - name: docker image: plugins/docker settings: From 42b795ae352ba16f43aa6db8db23181c45ddbedd Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 26 Apr 2025 11:19:36 +0200 Subject: [PATCH 5/7] Add a quick start guide --- README.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e3ba621..f5387fc 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,27 @@ Hathoris is compatible with any Linux distribution that has PulseAudio. It also ### Quick Installation Guide -TODO: Add a quick installation guide. +1. Install dependancies. + On Debian/Ubuntu/Raspbian/armbian/...: `sudo apt install alsa-utils pulseaudio-utils mpv yt-dlp` + On Alpine: `sudo apk add alsa-utils pulseaudio-utils mpv yt-dlp` + On ArchLinux/Manjaro: `sudo pacman -S alsa-utils pulseaudio mpv yt-dlp` + +2. Download the latest release binary for your architecture; choose between ARMv6 (Raspberry Pi Zero), ARMv7 (Voltastreams, Raspberry Pi 2+), ARM64 (Raspberry Pi Zero 2 and 3+ **with 64 bits OS**): https://git.nemunai.re/nemunaire/hathoris/releases/latest + +3. Give execution permissions: `chmod +x hathoris-linux-armv7` + +4. (optional) [Prepare a configuration for optional virtual inputs](#audio-sources) (like radios, streaming sources) + + The file is called `settings.json`, it is expected to be in the directory where you execute `hathoris`. It can be overwrited by adding a command line argument like `-settings-file /etc/hathoris/settings.json`. + +5. Launch the binary: `./hathoris -bind :8081` + +6. The interface will be available on the port 8081 from anywhere on your local network. + + From your local machine, it'll be on + +Enjoy! ### Build the Project From d47393fa486c7e9b74c89f35e4cc9ea27672cb73 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 26 Apr 2025 18:24:56 +0200 Subject: [PATCH 6/7] Docker image ready --- Dockerfile | 6 +++++- Dockerfile-norebuild | 6 +++++- README.md | 34 +++++++++++++++++++++++++++------- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2456062..3d32176 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,11 @@ RUN go get && go generate && go build -ldflags="-s -w" FROM alpine:3.18 +ENV HATHORIS_BIND=:8080 EXPOSE 8080 -CMD ["/srv/hathoris"] +ENTRYPOINT ["/srv/hathoris"] +WORKDIR /var/lib/hathoris + +RUN mkdir /var/lib/hathoris; apk --no-cache add alsa-utils pulseaudio-utils mpv yt-dlp COPY --from=build /go/src/git.nemunai.re/nemunaire/hathoris/hathoris /srv/hathoris diff --git a/Dockerfile-norebuild b/Dockerfile-norebuild index e083f4f..34d05fa 100644 --- a/Dockerfile-norebuild +++ b/Dockerfile-norebuild @@ -1,6 +1,10 @@ FROM alpine:3.18 +ENV HATHORIS_BIND=:8080 EXPOSE 8080 -CMD ["/srv/hathoris"] +ENTRYPOINT ["/srv/hathoris"] +WORKDIR /var/lib/hathoris + +RUN mkdir /var/lib/hathoris; apk --no-cache add alsa-utils pulseaudio-utils mpv yt-dlp COPY hathoris /srv/hathoris diff --git a/README.md b/README.md index f5387fc..bd8680b 100644 --- a/README.md +++ b/README.md @@ -47,13 +47,31 @@ Hathoris is compatible with any Linux distribution that has PulseAudio. It also ### Quick Installation Guide +#### With Docker + +[Prepare a configuration for optional virtual inputs](#audio-sources) (like radios, streaming sources), create the file at `~/.config/hathoris/settings.json`. + +``` +docker run -p 8080:8080 \ + --device /dev/snd \ + -e PULSE_SERVER=unix:/run/pulse/native \ + -v ${XDG_RUNTIME_DIR}/pulse/native:/run/pulse/native \ + -v ~/.config/pulse/cookie:/root/.config/pulse/cookie \ + -v ~/.config/hathoris:/var/lib/hathoris \ + nemunaire/hathoris:1 +``` + +⚠️ Please note that if your host is directly reachable on the Internet, it will be accessible to anyone who can reach this port. It is recommended to use a reverse proxy and/or configure proper firewall rules to secure your setup. + +#### Without Docker + 1. Install dependancies. - On Debian/Ubuntu/Raspbian/armbian/...: `sudo apt install alsa-utils pulseaudio-utils mpv yt-dlp` - On Alpine: `sudo apk add alsa-utils pulseaudio-utils mpv yt-dlp` - On ArchLinux/Manjaro: `sudo pacman -S alsa-utils pulseaudio mpv yt-dlp` + - On Debian/Ubuntu/Raspbian/armbian/...: `sudo apt install alsa-utils pulseaudio-utils mpv yt-dlp` + - On Alpine: `sudo apk add alsa-utils pulseaudio-utils mpv yt-dlp` + - On ArchLinux/Manjaro: `sudo pacman -S alsa-utils pulseaudio mpv yt-dlp` -2. Download the latest release binary for your architecture; choose between ARMv6 (Raspberry Pi Zero), ARMv7 (Voltastreams, Raspberry Pi 2+), ARM64 (Raspberry Pi Zero 2 and 3+ **with 64 bits OS**): https://git.nemunai.re/nemunaire/hathoris/releases/latest +2. Download the [latest release binary for your architecture](https://github.com/nemunaire/hathoris/releases/latest); choose between ARMv6 (Raspberry Pi Zero), ARMv7 (Voltastreams, Raspberry Pi 2+), ARM64 (Raspberry Pi Zero 2 and 3+ **with 64 bits OS**). 3. Give execution permissions: `chmod +x hathoris-linux-armv7` @@ -61,11 +79,13 @@ Hathoris is compatible with any Linux distribution that has PulseAudio. It also The file is called `settings.json`, it is expected to be in the directory where you execute `hathoris`. It can be overwrited by adding a command line argument like `-settings-file /etc/hathoris/settings.json`. -5. Launch the binary: `./hathoris -bind :8081` +5. Launch the binary: `./hathoris -bind :8080` -6. The interface will be available on the port 8081 from anywhere on your local network. +6. The interface will be available on the port 8080 from anywhere on your local network. - From your local machine, it'll be on + From your local machine, it'll be on + +⚠️ Please note that if your host is directly reachable on the Internet, it will be accessible to anyone who can reach this port. It is recommended to use a reverse proxy and/or configure proper firewall rules to secure your setup. Enjoy! From 02fdc0ed79dd39536e41679e813c92e6b86d84cc Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 26 Apr 2025 22:40:39 +0200 Subject: [PATCH 7/7] Speak about kodi.script.hathoris and LibreELEC autostart --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index bd8680b..37c31b3 100644 --- a/README.md +++ b/README.md @@ -281,6 +281,17 @@ curl http://127.0.0.1:8080/api/mixer' ] ``` +#### With Kodi + +An companion script is available to control hathoris directly from Kodi: + + +You can also create a script to automaticaly enable your Kodi input when it lauches. +Eg. for LibreELEC, append in `~/.config/autostart.sh`: + +``` +curl 'http://192.168.0.42:8080/api/sources/spdif/enable' -X POST +``` ## Compatible Hardware