diff --git a/Dockerfile b/Dockerfile index b741d82..f8412da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,22 @@ -FROM --platform=$BUILDPLATFORM debian:bullseye-slim as build +FROM --platform=$BUILDPLATFORM golang:1-bullseye as build ENV SPOTIFYD_VERSION=v0.3.5 ENV URL=https://github.com/Spotifyd/spotifyd/releases/download/${SPOTIFYD_VERSION}/spotifyd-linux-armhf-full.tar.gz RUN apt-get update && \ - apt-get install -yqq --no-install-recommends ca-certificates wget + apt-get install -yqq --no-install-recommends ca-certificates wget -RUN wget ${URL} -O - | tar -xz && chmod +x /spotifyd +WORKDIR / + +RUN wget ${URL} -O - | tar -xz && chmod +x spotifyd + +COPY main.go /main.go +RUN GOARCH=arm go build -o /runner /main.go FROM --platform=$TARGETPLATFORM debian:bullseye-slim COPY --from=build /spotifyd /app/spotifyd +COPY --from=build /runner /app/runner COPY start.sh /app/start.sh RUN apt-get update && \ @@ -22,4 +28,4 @@ RUN apt-get update && \ USER spotifyd WORKDIR /app -ENTRYPOINT [ "/app/start.sh" ] +ENTRYPOINT [ "/app/runner" ] diff --git a/README.md b/README.md index 96b6f02..655d341 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Following variables are available. See the [spotifyd doc](https://spotifyd.githu `PASSWORD` Required. Spotify password. -`DEVICE` Name of the device. Default: "Spotifyd" +`DEVICE_NAME` Name of the device. Default: "Spotifyd" `VOLUME_NORMALISATION` If set to true, enables volume normalisation between songs. diff --git a/build.sh b/build.sh index b175a32..9460238 100755 --- a/build.sh +++ b/build.sh @@ -1,3 +1,3 @@ #!/usr/bin/env sh -docker buildx build --platform linux/arm/v7 . -t djeeberjr/spotifyd --push +docker buildx build --platform linux/arm/v7 . -t djeeberjr/spotifyd $@ diff --git a/docker-compose.yml b/docker-compose.yml index 40eef14..81ed276 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,4 +9,4 @@ services: environment: - USERNAME=myUsername - PASSWORD=myPassword - - DEVICE=Spotifyd + - DEVICE_NAME=Spotifyd diff --git a/main.go b/main.go new file mode 100644 index 0000000..e1da709 --- /dev/null +++ b/main.go @@ -0,0 +1,155 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "time" +) + +type headerTransport struct { + baseTransport http.RoundTripper + headers map[string]string +} + +func (t *headerTransport) RoundTrip(req *http.Request) (*http.Response, error) { + for key, value := range t.headers { + req.Header.Set(key, value) + } + + return t.baseTransport.RoundTrip(req) +} + +type DevicesResponse struct { + Devices []struct { + ID string `json:"id"` + IsActive bool `json:"is_active"` + IsPrivateSession bool `json:"is_private_session"` + IsRestricted bool `json:"is_restricted"` + Name string `json:"name"` + SupportsVolume bool `json:"supports_volume"` + Type string `json:"type"` + VolumePercent int `json:"volume_percent"` + } `json:"devices"` +} + +var API_URL string +var API_KEY string +var DEVICE_NAME string +var MAX_FAILED = 5 +var CHECK_INTERVAL = 5 * time.Second + +func main() { + API_URL = os.Getenv("API_URL") + API_KEY = os.Getenv("API_KEY") + DEVICE_NAME = os.Getenv("DEVICE_NAME") + + if API_URL == "" { + fmt.Println("API_URL not set") + os.Exit(1) + } + + if API_KEY == "" { + fmt.Println("API_KEY not set") + os.Exit(1) + } + + if DEVICE_NAME == "" { + fmt.Println("DEVICE_NAME not set") + fmt.Println("Using default: Spotifyd") + DEVICE_NAME = "Spotifyd" + } + + client := &http.Client{ + Transport: &headerTransport{ + baseTransport: http.DefaultTransport, + headers: map[string]string{ + "X-API-KEY": API_KEY, + }, + }, + } + + runLoop(client) +} + +func runLoop(client *http.Client) { + proc := runSpotifyd() + + go func() { + defer func() { + fmt.Println("Killing spotifyd") + proc.Process.Kill() + }() + + failedCheck := 0 + for { + time.Sleep(CHECK_INTERVAL) + devices, err := getDevices(client) + if err != nil { + fmt.Println("Error getting devices: " + err.Error()) + continue + } + + if containsDevice(devices, DEVICE_NAME) { + failedCheck = 0 + continue + } + + failedCheck++ + fmt.Printf("Can't find device %s. Failed check %d of %d \n", DEVICE_NAME, failedCheck, MAX_FAILED) + + if failedCheck >= MAX_FAILED { + return // kill handled by defer + } + } + }() + + proc.Run() +} + +func containsDevice(response *DevicesResponse, deviceName string) bool { + for _, device := range response.Devices { + if device.Name == DEVICE_NAME { + return true + } + } + return false +} + +func getDevices(client *http.Client) (*DevicesResponse, error) { + // https://developer.spotify.com/documentation/web-api/reference/get-a-users-available-devices + resp, err := client.Get(API_URL + "/me/player/devices") + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, errors.New("Not OK. Got " + resp.Status + " instead") + } + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var devicesResponse DevicesResponse + err = json.Unmarshal(data, &devicesResponse) + if err != nil { + return nil, err + } + + return &devicesResponse, nil +} + +func runSpotifyd() *exec.Cmd { + proc := exec.Command("/app/start.sh") + + proc.Stdout = os.Stdout + proc.Stderr = os.Stderr + + return proc +} diff --git a/start.sh b/start.sh index a3d8fc8..e0ccf8e 100755 --- a/start.sh +++ b/start.sh @@ -32,4 +32,4 @@ if [ -n "$EXTRA_ARGS" ]; then ARGS="${ARGS} ${EXTRA_ARGS}" fi -/app/spotifyd --no-daemon --username "$USERNAME" --password "$PASSWORD" --device-name "${DEVICE:=Spotifyd}" $ARGS +/app/spotifyd --no-daemon --username "$USERNAME" --password "$PASSWORD" --device-name "${DEVICE_NAME:=Spotifyd}" $ARGS