ahayworth / snapcast-autoconfig

Automatically re-configure snapcast groups based on playing streams
24 stars 2 forks source link

When clients are in an Idle group and one client's preferred stream starts, both clients play it #4

Open markferry opened 3 years ago

markferry commented 3 years ago

os: Arch linux snapclient: 0.25.0 snapserver: 0.25.0

First off, I realise this is a side-effect of how Snapcast works when removing clients from groups ;)

In this scenario:

To repro:

Result:

Expected:

Config:

---
server: tcp://localhost:1705
streams:
  Canard:
    clients:
      - canard
  Kitchen:
    clients:
      - kitchen
  Party 1:
    clients:
      - canard
      - kitchen

The result is the same even with more clients in g0. All clients end up in individual groups playing s1. log.txt

markferry commented 3 years ago

Shorter log:

# play Party 1 stream (canard + kitchen)
, [2021-07-30T20:06:05.794634 #676586]  INFO -- : MISCONFIGURED: Party 1
I, [2021-07-30T20:06:05.794842 #676586]  INFO -- : Going to reconfigure group 'fb8c0e01-34ee-4526-acdf-4fc1db32957d':
  Name: Canard -> Party 1
  Stream: Canard -> Party 1
  Clients: ["canard"] -> ["canard", "kitchen"]

I, [2021-07-30T20:06:05.794926 #676586]  INFO -- :     canard volume: 100 -> 100

# pause Party 1 stream

# play Canard stream ...
I, [2021-07-30T20:06:35.813048 #676586]  INFO -- : MISCONFIGURED: Canard
I, [2021-07-30T20:06:35.813282 #676586]  INFO -- : Going to reconfigure group 'fb8c0e01-34ee-4526-acdf-4fc1db32957d':
  Name: Party 1 -> Canard
  Stream: Party 1 -> Canard
  Clients: ["canard", "kitchen"] -> ["canard"]

I, [2021-07-30T20:06:35.813451 #676586]  INFO -- :     canard volume: 100 -> 100
I, [2021-07-30T20:06:35.813592 #676586]  INFO -- :     kitchen volume: 100 -> 100
# pause Canard stream
markferry commented 3 years ago

snapserver control stream (concatenated into a json array):

[
  {
    "jsonrpc": "2.0",
    "method": "Stream.OnUpdate",
    "params": {
      "id": "Party 1",
      "stream": {
        "id": "Party 1",
        "meta": { "STREAM": "Party 1" },
        "status": "playing",
        "uri": {
          "fragment": "",
          "host": "",
          "path": "/librespot",
          "query": {
            "bitrate": "160",
            "chunk_ms": "20",
            "codec": "flac",
            "devicename": "Party 1",
            "name": "Party 1",
            "sampleformat": "44100:16:2"
          },
          "raw": "librespot:////librespot?bitrate=160&chunk_ms=20&codec=flac&devicename=Party 1&name=Party 1&sampleformat=44100:16:2",
          "scheme": "librespot"
        }
      }
    }
  },

  { "jsonrpc": "2.0", "method": "Group.OnStreamChanged", "params": { "id": "130666ad-cbb1-a433-6a64-3e5d97bde579", "stream_id": "Party 1" } },

  {
    "jsonrpc": "2.0",
    "method": "Server.OnUpdate",
    "params": {
      "server": {
        "groups": [
          {
            "clients": [
              {
                "config": {
                  "instance": 1,
                  "latency": 0,
                  "name": "",
                  "volume": { "muted": false, "percent": 100 }
                },
                "connected": false,
                "host": {
                  "arch": "web",
                  "ip": "::ffff:192.168.0.666",
                  "mac": "66:66:66:66:66:66",
                  "name": "Snapweb client",
                  "os": "iPhone"
                },
                "id": "528e4925-d033-415f-95ea-ad852fd0ebcb",
                "lastSeen": { "sec": 1627595355, "usec": 76512 },
                "snapclient": {
                  "name": "Snapweb",
                  "protocolVersion": 2,
                  "version": "0.1.0"
                }
              }
            ],
            "id": "6f8b43d4-d82e-d607-88f5-4daeef5d0c89",
            "muted": false,
            "name": "",
            "stream_id": "Canard"
          },
          {
            "clients": [
              {
                "config": {
                  "instance": 1,
                  "latency": 0,
                  "name": "",
                  "volume": { "muted": false, "percent": 100 }
                },
                "connected": true,
                "host": {
                  "arch": "x86_64",
                  "ip": "127.0.0.1",
                  "mac": "66:66:66:66:66:66",
                  "name": "canard",
                  "os": "Arch Linux"
                },
                "id": "canard",
                "lastSeen": { "sec": 1627679619, "usec": 995448 },
                "snapclient": {
                  "name": "Snapclient",
                  "protocolVersion": 2,
                  "version": "0.25.0"
                }
              },
              {
                "config": {
                  "instance": 1,
                  "latency": 0,
                  "name": "kitchen",
                  "volume": { "muted": false, "percent": 100 }
                },
                "connected": true,
                "host": {
                  "arch": "x86_64",
                  "ip": "127.0.0.1",
                  "mac": "66:66:66:66:66:66",
                  "name": "canard",
                  "os": "Arch Linux"
                },
                "id": "kitchen",
                "lastSeen": { "sec": 1627679619, "usec": 995492 },
                "snapclient": {
                  "name": "Snapclient",
                  "protocolVersion": 2,
                  "version": "0.25.0"
                }
              }
            ],
            "id": "130666ad-cbb1-a433-6a64-3e5d97bde579",
            "muted": false,
            "name": "Canard",
            "stream_id": "Party 1"
          },
          {
            "clients": [
              {
                "config": {
                  "instance": 1,
                  "latency": 0,
                  "name": "bedroom-mark",
                  "volume": { "muted": false, "percent": 100 }
                },
                "connected": false,
                "host": {
                  "arch": "x86_64",
                  "ip": "127.0.0.1",
                  "mac": "66:66:66:66:66:66",
                  "name": "canard",
                  "os": "Arch Linux"
                },
                "id": "bedroom-mark",
                "lastSeen": { "sec": 1627679600, "usec": 207626 },
                "snapclient": {
                  "name": "Snapclient",
                  "protocolVersion": 2,
                  "version": "0.25.0"
                }
              }
            ],
            "id": "8a9f1bf6-313b-4510-b238-5f3587ba9447",
            "muted": false,
            "name": "",
            "stream_id": "Canard"
          }
        ],
        "server": {
          "host": {
            "arch": "x86_64",
            "ip": "",
            "mac": "",
            "name": "canard",
            "os": "Arch Linux"
          },
          "snapserver": {
            "controlProtocolVersion": 1,
            "name": "Snapserver",
            "protocolVersion": 1,
            "version": "0.25.0"
          }
        },
        "streams": [
          {
            "id": "Canard",
            "meta": { "STREAM": "Canard" },
            "status": "idle",
            "uri": {
              "fragment": "",
              "host": "",
              "path": "/tmp/mopidy.canard",
              "query": {
                "chunk_ms": "20",
                "codec": "flac",
                "name": "Canard",
                "sampleformat": "48000:16:2"
              },
              "raw": "pipe:////tmp/mopidy.canard?chunk_ms=20&codec=flac&name=Canard&sampleformat=48000:16:2",
              "scheme": "pipe"
            }
          },
          {
            "id": "Kitchen",
            "meta": { "STREAM": "Kitchen" },
            "status": "idle",
            "uri": {
              "fragment": "",
              "host": "",
              "path": "/tmp/mopidy.kitchen",
              "query": {
                "chunk_ms": "20",
                "codec": "flac",
                "name": "Kitchen",
                "sampleformat": "48000:16:2"
              },
              "raw": "pipe:////tmp/mopidy.kitchen?chunk_ms=20&codec=flac&name=Kitchen&sampleformat=48000:16:2",
              "scheme": "pipe"
            }
          },
          {
            "id": "Party 1",
            "meta": { "STREAM": "Party 1" },
            "status": "playing",
            "uri": {
              "fragment": "",
              "host": "",
              "path": "/librespot",
              "query": {
                "bitrate": "160",
                "chunk_ms": "20",
                "codec": "flac",
                "devicename": "Party 1",
                "name": "Party 1",
                "sampleformat": "44100:16:2"
              },
              "raw": "librespot:////librespot?bitrate=160&chunk_ms=20&codec=flac&devicename=Party 1&name=Party 1&sampleformat=44100:16:2",
              "scheme": "librespot"
            }
          }
        ]
      }
    }
  },

  { "jsonrpc": "2.0", "method": "Group.OnNameChanged", "params": { "id": "130666ad-cbb1-a433-6a64-3e5d97bde579", "name": "Party 1" } },

  {
    "jsonrpc": "2.0",
    "method": "Stream.OnUpdate",
    "params": {
      "id": "Party 1",
      "stream": {
        "id": "Party 1",
        "meta": { "STREAM": "Party 1" },
        "status": "idle",
        "uri": {
          "fragment": "",
          "host": "",
          "path": "/librespot",
          "query": {
            "bitrate": "160",
            "chunk_ms": "20",
            "codec": "flac",
            "devicename": "Party 1",
            "name": "Party 1",
            "sampleformat": "44100:16:2"
          },
          "raw": "librespot:////librespot?bitrate=160&chunk_ms=20&codec=flac&devicename=Party 1&name=Party 1&sampleformat=44100:16:2",
          "scheme": "librespot"
        }
      }
    }
  },

  {
    "jsonrpc": "2.0",
    "method": "Stream.OnUpdate",
    "params": {
      "id": "Canard",
      "stream": {
        "id": "Canard",
        "meta": { "STREAM": "Canard" },
        "status": "playing",
        "uri": {
          "fragment": "",
          "host": "",
          "path": "/tmp/mopidy.canard",
          "query": {
            "chunk_ms": "20",
            "codec": "flac",
            "name": "Canard",
            "sampleformat": "48000:16:2"
          },
          "raw": "pipe:////tmp/mopidy.canard?chunk_ms=20&codec=flac&name=Canard&sampleformat=48000:16:2",
          "scheme": "pipe"
        }
      }
    }
  },

  { "jsonrpc": "2.0", "method": "Group.OnStreamChanged", "params": { "id": "130666ad-cbb1-a433-6a64-3e5d97bde579", "stream_id": "Canard" } },

  {
    "jsonrpc": "2.0",
    "method": "Server.OnUpdate",
    "params": {
      "server": {
        "groups": [
          {
            "clients": [
              {
                "config": {
                  "instance": 1,
                  "latency": 0,
                  "name": "",
                  "volume": { "muted": false, "percent": 100 }
                },
                "connected": false,
                "host": {
                  "arch": "web",
                  "ip": "::ffff:192.168.0.666",
                  "mac": "66:66:66:66:66:66",
                  "name": "Snapweb client",
                  "os": "iPhone"
                },
                "id": "528e4925-d033-415f-95ea-ad852fd0ebcb",
                "lastSeen": { "sec": 1627595355, "usec": 76512 },
                "snapclient": {
                  "name": "Snapweb",
                  "protocolVersion": 2,
                  "version": "0.1.0"
                }
              }
            ],
            "id": "6f8b43d4-d82e-d607-88f5-4daeef5d0c89",
            "muted": false,
            "name": "",
            "stream_id": "Canard"
          },
          {
            "clients": [
              {
                "config": {
                  "instance": 1,
                  "latency": 0,
                  "name": "",
                  "volume": { "muted": false, "percent": 100 }
                },
                "connected": true,
                "host": {
                  "arch": "x86_64",
                  "ip": "127.0.0.1",
                  "mac": "66:66:66:66:66:66",
                  "name": "canard",
                  "os": "Arch Linux"
                },
                "id": "canard",
                "lastSeen": { "sec": 1627679648, "usec": 14893 },
                "snapclient": {
                  "name": "Snapclient",
                  "protocolVersion": 2,
                  "version": "0.25.0"
                }
              }
            ],
            "id": "130666ad-cbb1-a433-6a64-3e5d97bde579",
            "muted": false,
            "name": "Party 1",
            "stream_id": "Canard"
          },
          {
            "clients": [
              {
                "config": {
                  "instance": 1,
                  "latency": 0,
                  "name": "bedroom-mark",
                  "volume": { "muted": false, "percent": 100 }
                },
                "connected": false,
                "host": {
                  "arch": "x86_64",
                  "ip": "127.0.0.1",
                  "mac": "66:66:66:66:66:66",
                  "name": "canard",
                  "os": "Arch Linux"
                },
                "id": "bedroom-mark",
                "lastSeen": { "sec": 1627679600, "usec": 207626 },
                "snapclient": {
                  "name": "Snapclient",
                  "protocolVersion": 2,
                  "version": "0.25.0"
                }
              }
            ],
            "id": "8a9f1bf6-313b-4510-b238-5f3587ba9447",
            "muted": false,
            "name": "",
            "stream_id": "Canard"
          },
          {
            "clients": [
              {
                "config": {
                  "instance": 1,
                  "latency": 0,
                  "name": "kitchen",
                  "volume": { "muted": false, "percent": 100 }
                },
                "connected": true,
                "host": {
                  "arch": "x86_64",
                  "ip": "127.0.0.1",
                  "mac": "66:66:66:66:66:66",
                  "name": "canard",
                  "os": "Arch Linux"
                },
                "id": "kitchen",
                "lastSeen": { "sec": 1627679648, "usec": 14684 },
                "snapclient": {
                  "name": "Snapclient",
                  "protocolVersion": 2,
                  "version": "0.25.0"
                }
              }
            ],
            "id": "fd017fd6-37c4-a8b9-8b41-e983445598aa",
            "muted": false,
            "name": "",
            "stream_id": "Canard"
          }
        ],
        "server": {
          "host": {
            "arch": "x86_64",
            "ip": "",
            "mac": "",
            "name": "canard",
            "os": "Arch Linux"
          },
          "snapserver": {
            "controlProtocolVersion": 1,
            "name": "Snapserver",
            "protocolVersion": 1,
            "version": "0.25.0"
          }
        },
        "streams": [
          {
            "id": "Canard",
            "meta": { "STREAM": "Canard" },
            "status": "playing",
            "uri": {
              "fragment": "",
              "host": "",
              "path": "/tmp/mopidy.canard",
              "query": {
                "chunk_ms": "20",
                "codec": "flac",
                "name": "Canard",
                "sampleformat": "48000:16:2"
              },
              "raw": "pipe:////tmp/mopidy.canard?chunk_ms=20&codec=flac&name=Canard&sampleformat=48000:16:2",
              "scheme": "pipe"
            }
          },
          {
            "id": "Kitchen",
            "meta": { "STREAM": "Kitchen" },
            "status": "idle",
            "uri": {
              "fragment": "",
              "host": "",
              "path": "/tmp/mopidy.kitchen",
              "query": {
                "chunk_ms": "20",
                "codec": "flac",
                "name": "Kitchen",
                "sampleformat": "48000:16:2"
              },
              "raw": "pipe:////tmp/mopidy.kitchen?chunk_ms=20&codec=flac&name=Kitchen&sampleformat=48000:16:2",
              "scheme": "pipe"
            }
          },
          {
            "id": "Party 1",
            "meta": { "STREAM": "Party 1" },
            "status": "idle",
            "uri": {
              "fragment": "",
              "host": "",
              "path": "/librespot",
              "query": {
                "bitrate": "160",
                "chunk_ms": "20",
                "codec": "flac",
                "devicename": "Party 1",
                "name": "Party 1",
                "sampleformat": "44100:16:2"
              },
              "raw": "librespot:////librespot?bitrate=160&chunk_ms=20&codec=flac&devicename=Party 1&name=Party 1&sampleformat=44100:16:2",
              "scheme": "librespot"
            }
          }
        ]
      }
    }
  },

  { "jsonrpc": "2.0", "method": "Group.OnNameChanged", "params": { "id": "130666ad-cbb1-a433-6a64-3e5d97bde579", "name": "Canard" } }
]
markferry commented 3 years ago

I wonder, would it be possible to catch unnamed groups that have only one client and update its stream based on config? (and name it at the same time...)

ahayworth commented 3 years ago

Hi @markferry - glad to see someone else finds this tool useful!

I hadn't noticed your issue yet, but I actually saw this behavior myself the other day. I added some behavior that actually just muted groups like this, to work around it - see this commit range: https://github.com/ahayworth/snapcast-autoconfig/compare/5a424a70a112552860f5cd41f5b7b8af68ff74c9...main

I wonder if that solves the problem well enough for you? If not, I can look into trying to handle it more robustly.

markferry commented 3 years ago

Thanks Andrew, just back at this now.

The commits on main seem less reliable than before.

I'll dig deeper tomorrow, but in the meantime I've noticed:

f5340600-8e42-3a38-e2d1-f82e306976a0 (name: 'party1', stream: 'party1', muted: 'false' / playing) ["outside"]
----------
I, [2021-08-12T03:42:35.893530 #1054108]  INFO -- : MISCONFIGURED: f5340600-8e42-3a38-e2d1-f82e306976a0
I, [2021-08-12T03:42:35.893565 #1054108]  INFO -- : Going to mute group 'f5340600-8e42-3a38-e2d1-f82e306976a0' / 'party1' / 'party1'!

D, [2021-08-12T03:42:35.893757 #1054108] DEBUG -- : -> Group.SetMute ({:id=>"f5340600-8e42-3a38-e2d1-f82e306976a0", :mute=>true})
D, [2021-08-12T03:42:36.135021 #1054108] DEBUG -- : -> Server.GetStatus ([])
D, [2021-08-12T03:42:36.152818 #1054108] DEBUG -- : <- Server.GetStatus
D, [2021-08-12T03:42:37.894629 #1054108] DEBUG -- : 
----------
cfb48486-a149-7b17-5a7e-2efd041d101c (name: 'kitchen', stream: 'kitchen', muted: 'false' / playing) ["kitchen"]
4733822b-8420-c805-71c2-c2c196835896 (name: 'ballroom', stream: 'outside', muted: 'true' / idle) ["ballroom"]
63eddb4a-cb3e-6df6-6552-383a0ebe4fb1 (name: '', stream: 'Iris', muted: 'false' / idle) ["canard"]
08429d46-fa38-3867-3c82-98b1afc0dde8 (name: '', stream: 'Iris', muted: 'false' / idle) ["33692697-bccc-43b2-bc9b-e91fca21749a"]
f5340600-8e42-3a38-e2d1-f82e306976a0 (name: 'party1', stream: 'party1', muted: 'true' / playing) ["outside"]
----------
I, [2021-08-12T03:42:37.895957 #1054108]  INFO -- : MISCONFIGURED: party1
I, [2021-08-12T03:42:37.896190 #1054108]  INFO -- : Going to reconfigure group 'f5340600-8e42-3a38-e2d1-f82e306976a0':
  Name: party1 -> party1
  Stream: party1 -> party1
  Clients: ["outside"] -> ["outside"]
  Muted: true -> false

I, [2021-08-12T03:42:37.896311 #1054108]  INFO -- :     outside volume: 100 -> 100
D, [2021-08-12T03:42:37.896806 #1054108] DEBUG -- : -> Group.SetMute ({:id=>"f5340600-8e42-3a38-e2d1-f82e306976a0", :mute=>false})
D, [2021-08-12T03:42:38.135936 #1054108] DEBUG -- : -> Server.GetStatus ([])
D, [2021-08-12T03:42:38.211380 #1054108] DEBUG -- : <- Server.GetStatus
D, [2021-08-12T03:42:39.898487 #1054108] DEBUG -- : 
----------
cfb48486-a149-7b17-5a7e-2efd041d101c (name: 'kitchen', stream: 'kitchen', muted: 'false' / playing) ["kitchen"]
4733822b-8420-c805-71c2-c2c196835896 (name: 'ballroom', stream: 'outside', muted: 'true' / idle) ["ballroom"]
63eddb4a-cb3e-6df6-6552-383a0ebe4fb1 (name: '', stream: 'Iris', muted: 'false' / idle) ["canard"]
08429d46-fa38-3867-3c82-98b1afc0dde8 (name: '', stream: 'Iris', muted: 'false' / idle) ["33692697-bccc-43b2-bc9b-e91fca21749a"]
f5340600-8e42-3a38-e2d1-f82e306976a0 (name: 'party1', stream: 'party1', muted: 'false' / playing) ["outside"]
----------
I, [2021-08-12T03:42:39.899627 #1054108]  INFO -- : MISCONFIGURED: f5340600-8e42-3a38-e2d1-f82e306976a0
I, [2021-08-12T03:42:39.899757 #1054108]  INFO -- : Going to mute group 'f5340600-8e42-3a38-e2d1-f82e306976a0' / 'party1' / 'party1'!

D, [2021-08-12T03:42:39.900162 #1054108] DEBUG -- : -> Group.SetMute ({:id=>"f5340600-8e42-3a38-e2d1-f82e306976a0", :mute=>true})
D, [2021-08-12T03:42:40.137322 #1054108] DEBUG -- : -> Server.GetStatus ([])
D, [2021-08-12T03:42:40.212560 #1054108] DEBUG -- : <- Server.GetStatus
D, [2021-08-12T03:42:41.901234 #1054108] DEBUG -- : 
----------
cfb48486-a149-7b17-5a7e-2efd041d101c (name: 'kitchen', stream: 'kitchen', muted: 'false' / playing) ["kitchen"]
4733822b-8420-c805-71c2-c2c196835896 (name: 'ballroom', stream: 'outside', muted: 'true' / idle) ["ballroom"]
63eddb4a-cb3e-6df6-6552-383a0ebe4fb1 (name: '', stream: 'Iris', muted: 'false' / idle) ["canard"]
08429d46-fa38-3867-3c82-98b1afc0dde8 (name: '', stream: 'Iris', muted: 'false' / idle) ["33692697-bccc-43b2-bc9b-e91fca21749a"]
f5340600-8e42-3a38-e2d1-f82e306976a0 (name: 'party1', stream: 'party1', muted: 'true' / playing) ["outside"]
----------
I, [2021-08-12T03:42:41.901492 #1054108]  INFO -- : MISCONFIGURED: party1
I, [2021-08-12T03:42:41.901533 #1054108]  INFO -- : Going to reconfigure group 'f5340600-8e42-3a38-e2d1-f82e306976a0':
  Name: party1 -> party1
  Stream: party1 -> party1
  Clients: ["outside"] -> ["outside"]
  Muted: true -> false

I, [2021-08-12T03:42:41.901561 #1054108]  INFO -- :     outside volume: 100 -> 100
D, [2021-08-12T03:42:41.901721 #1054108] DEBUG -- : -> Group.SetMute ({:id=>"f5340600-8e42-3a38-e2d1-f82e306976a0", :mute=>false})
D, [2021-08-12T03:42:42.139069 #1054108] DEBUG -- : -> Server.GetStatus ([])
D, [2021-08-12T03:42:42.156790 #1054108] DEBUG -- : <- Server.GetStatus
D, [2021-08-12T03:42:43.902333 #1054108] DEBUG -- : 
----------
cfb48486-a149-7b17-5a7e-2efd041d101c (name: 'kitchen', stream: 'kitchen', muted: 'false' / playing) ["kitchen"]
4733822b-8420-c805-71c2-c2c196835896 (name: 'ballroom', stream: 'outside', muted: 'true' / idle) ["ballroom"]
63eddb4a-cb3e-6df6-6552-383a0ebe4fb1 (name: '', stream: 'Iris', muted: 'false' / idle) ["canard"]
08429d46-fa38-3867-3c82-98b1afc0dde8 (name: '', stream: 'Iris', muted: 'false' / idle) ["33692697-bccc-43b2-bc9b-e91fca21749a"]
f5340600-8e42-3a38-e2d1-f82e306976a0 (name: 'party1', stream: 'party1', muted: 'false' / playing) ["outside"]
----------
I, [2021-08-12T03:42:43.902885 #1054108]  INFO -- : MISCONFIGURED: f5340600-8e42-3a38-e2d1-f82e306976a0
I, [2021-08-12T03:42:43.902950 #1054108]  INFO -- : Going to mute group 'f5340600-8e42-3a38-e2d1-f82e306976a0' / 'party1' / 'party1'!

D, [2021-08-12T03:42:43.903239 #1054108] DEBUG -- : -> Group.SetMute ({:id=>"f5340600-8e42-3a38-e2d1-f82e306976a0", :mute=>true})
D, [2021-08-12T03:42:44.139655 #1054108] DEBUG -- : -> Server.GetStatus ([])
D, [2021-08-12T03:42:44.157686 #1054108] DEBUG -- : <- Server.GetStatus
ahayworth commented 3 years ago

@markferry I'm surprised, honestly - this fixed the similar problems I was seeing at my house. But, clearly, it's not the right answer. I'll dig into this a little bit tonight and see if I can't figure something out. I wrote this whole thing on some fancy new ruby async libraries and didn't write tests or anything, so there's probably horrible bugs to work out. I appreciate the report, and the patience!

(Of course, if you think you know how to fix it already, PRs are always welcome - but otherwise I'll work on this a bit tonight. 😄 )