Apple Music Winamp
macOS Apple Music controls with song, artist, progress, a time counter, decorative Winamp-style visualizers, and animated lightning credits.
By Nachenko · ▲ 0 · Rate and Discuss
How It Works
Apple Music Winamp
A macOS Apple Music controller with a nostalgic 72×16 LED display. Python standard library only; no pip or Node installation required to run the app.
python3 app.py # USB bar at 10.0.4.20
python3 app.py --host 127.0.0.1:8321 # BUSY Bar Manager
BUSY_HTTP_PASSWORD=your-token python3 app.py --host 192.168.1.50
Open Music and select a song. Allow the launching terminal or host app to control Music when macOS prompts for Automation permission. Keep the Mac and controller running and put the bar in APPS mode.
| Button | Action |
|---|---|
| OK | Play / pause |
| BACK | Previous track |
| START | Next track |
White song title and gray artist scroll independently above a green progress bar. The permanent elapsed counter sits below a rotating spectrum, waveform, and dot scope. Song changes trigger a bright burst; successful button commands flash their action. Animated lightning accompanies the Winamp credit, with an occasional llama Easter egg.
The visualizers are decorative: no audio, microphone, or screen capture. Winamp is a visual homage, not a runtime dependency. Music metadata and controls use local AppleScript. Apple Music on an iPhone is not supported.
Higher-priority display owners cause the app to yield and retry. Controls are checked against a fresh successful draw and, when available, Manager's screen owner. Mode-switch input events disable controls outside APPS mode. Disable other interactive controllers to avoid multiple apps reacting to the same buttons.
Ctrl+C or SIGTERM stops the app and clears only its own display. Lost connections are retried. If Automation permission was denied, check System Settings → Privacy & Security → Automation.
Preview and checks
From the gallery root:
npm run preview -- apple-music-winamp --seconds 8 -- --demo
npm run check -- apple-music-winamp --run -- --demo
--demo uses sample track metadata and renders the actual app on any platform without controlling Music or listening to hardware buttons. It is intended for previews and review, not as a music source.
MIT License. Copyright (c) 2026 Nachenko. Unofficial and unaffiliated with Apple, Winamp, or Flipper Devices.
Source Files
These are the mirrored source bytes. The complete package also includes every asset and supporting file.
README.md
# Apple Music Winamp
A macOS Apple Music controller with a nostalgic 72×16 LED display. Python standard library only; no pip or Node installation required to run the app.
```sh
python3 app.py # USB bar at 10.0.4.20
python3 app.py --host 127.0.0.1:8321 # BUSY Bar Manager
BUSY_HTTP_PASSWORD=your-token python3 app.py --host 192.168.1.50
```
Open Music and select a song. Allow the launching terminal or host app to control Music when macOS prompts for Automation permission. Keep the Mac and controller running and put the bar in APPS mode.
| Button | Action |
| --- | --- |
| OK | Play / pause |
| BACK | Previous track |
| START | Next track |
White song title and gray artist scroll independently above a green progress bar. The permanent elapsed counter sits below a rotating spectrum, waveform, and dot scope. Song changes trigger a bright burst; successful button commands flash their action. Animated lightning accompanies the Winamp credit, with an occasional llama Easter egg.
The visualizers are decorative: no audio, microphone, or screen capture. Winamp is a visual homage, not a runtime dependency. Music metadata and controls use local AppleScript. Apple Music on an iPhone is not supported.
Higher-priority display owners cause the app to yield and retry. Controls are checked against a fresh successful draw and, when available, Manager's screen owner. Mode-switch input events disable controls outside APPS mode. Disable other interactive controllers to avoid multiple apps reacting to the same buttons.
Ctrl+C or SIGTERM stops the app and clears only its own display. Lost connections are retried. If Automation permission was denied, check System Settings → Privacy & Security → Automation.
## Preview and checks
From the gallery root:
```sh
npm run preview -- apple-music-winamp --seconds 8 -- --demo
npm run check -- apple-music-winamp --run -- --demo
```
`--demo` uses sample track metadata and renders the actual app on any platform without controlling Music or listening to hardware buttons. It is intended for previews and review, not as a music source.
MIT License. Copyright (c) 2026 Nachenko. Unofficial and unaffiliated with Apple, Winamp, or Flipper Devices.app.py
#!/usr/bin/env python3
"""Winamp-inspired Apple Music display and hardware controls (macOS).
python3 app.py --host 10.0.4.20
python3 app.py --host 127.0.0.1:8321 # BUSY Bar Manager
python3 app.py --demo --host 127.0.0.1:8080 # animated preview, no Music needed
MIT License, Copyright (c) 2026 Nachenko.
Visualizer motion is decorative. No screen, microphone, or system audio capture.
"""
import argparse
import base64
import hashlib
import json
import math
import os
import queue
import signal
import socket
import ssl
import struct
import subprocess
import sys
import threading
import time
import urllib.error
import urllib.parse
import urllib.request
import zlib
APP = 'apple-music-winamp'
SCRIPT = '''on packed(values)
set AppleScript's text item delimiters to ASCII character 30
return values as text
end packed
on run argv
if application "Music" is not running then return my packed({"closed", "Open Music", "", "0", "0"})
tell application "Music"
set operation to item 1 of argv
if operation is "toggle" then
playpause
else if operation is "previous" then
previous track
else if operation is "next" then
next track
end if
set playbackState to player state as text
if playbackState is "stopped" then return my packed({playbackState, "Apple Music", "Choose a song", "0", "0"})
return my packed({playbackState, name of current track, artist of current track, player position as text, duration of current track as text})
end tell
end run'''
SMALL_ADVANCES = [2, 2, 4, 6, 4, 5, 5, 2, 3, 3, 4, 4, 2, 3, 2, 3, 4, 3, 4, 4, 4, 4, 4, 4, 4, 4, 2, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 2, 4, 5, 4, 6, 5, 5, 5, 5, 5, 5, 4, 5, 4, 6, 4, 4, 4, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 3, 4, 4, 2, 3, 4, 2, 6, 4, 4, 4, 4, 3, 4, 3, 4, 4, 6, 4, 4, 4, 4, 2, 4, 5]
NORMAL_ADVANCES = [5, 3, 4, 6, 6, 8, 7, 2, 3, 3, 4, 6, 3, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 2, 3, 4, 6, 4, 6, 8, 6, 6, 6, 6, 6, 6, 6, 6, 4, 5, 6, 5, 8, 6, 6, 6, 6, 6, 6, 6, 6, 6, 8, 6, 6, 6, 3, 5, 3, 6, 5, 6, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 5, 4, 6, 5, 5, 5, 5, 4, 5, 5, 5, 6, 6, 6, 5, 5, 4, 3, 4, 6]
def seconds(value):
try:
result = float(str(value).replace(',', '.'))
return max(0, result) if math.isfinite(result) else 0
except ValueError:
return 0
def scroll_ms(text, width, small=False):
advances = SMALL_ADVANCES if small else NORMAL_ADVANCES
length = sum(advances[ord(c) - 32 if 32 <= ord(c) <= 126 else 31] for c in text)
return 600 + max(0, length - width) / 6 * 1000 + 1200
def rectangle(identity, x, y, width, height, color):
return dict(id=identity, type='rectangle', display='front', x=x, y=y,
width=width, height=height, radius=0, fill='solid',
fill_colors=[color], border_width=0)
def text(identity, value, x, y, width, font='normal', color='#FFFFFFFF'):
return dict(id=identity, type='text', display='front', text=value or ' ',
x=x, y=y, width=width, font=font, color=color,
scroll_rate=360, scroll_start_delay=600, scroll_repeat_delay=1500)
ICONS = {
'bolt': ['00001110','00011100','00111000','01111110','00011100','00111000','01110000','01100000','11000000','10000000'],
'apple': ['00001100','00011000','00000000','01101100','11111110','11111000','11111000','11111110','01111100','00101000'],
'blank': ['00000000'] * 10,
}
def icon_png(name):
def chunk(kind, data):
return struct.pack('!I', len(data)) + kind + data + struct.pack('!I', zlib.crc32(kind + data))
frame = int(name[-1]) if name.startswith('bolt-') else None
rows = ICONS['bolt' if frame is not None else name]
colors = [(255,176,24),(255,210,56),(255,244,150),(255,255,240),(255,218,72),(255,187,28)]
sparks = [(7,8),(1,1),(6,6),(0,4),(7,2),(3,9)]
raw = bytearray()
for y, row in enumerate(rows):
raw.append(0)
for x, pixel in enumerate(row):
rgba = (0, 0, 0, 0)
if pixel == '1':
color = colors[frame] if frame is not None else ((255,192,40) if name == 'bolt' else (255,255,255))
if frame is not None and (x + y + frame) % 5 == 0:
color = (255,255,220)
rgba = (*color,255)
elif frame is not None and (x, y) == sparks[frame]:
rgba = (255,230,130,255)
raw.extend(rgba)
return b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', struct.pack('!IIBBBBB',8,10,8,6,0,0,0)) + chunk(b'IDAT',zlib.compress(raw)) + chunk(b'IEND',b'')
# Minimal bounded protobuf reader: State.updates.input.{button_event,switch_event}.
def fields(data):
offset = 0
def varint():
nonlocal offset
value = 0
for shift in range(0, 70, 7):
if offset >= len(data):
raise ValueError('truncated protobuf')
byte = data[offset]
offset += 1
value |= (byte & 127) << shift
if byte < 128:
return value
raise ValueError('oversized varint')
result = []
while offset < len(data):
tag = varint()
number, wire = tag >> 3, tag & 7
if wire == 0:
value = varint()
elif wire == 2:
size = varint()
if size > len(data) - offset:
raise ValueError('truncated protobuf field')
value = data[offset:offset + size]
offset += size
elif wire in (1, 5):
offset += 8 if wire == 1 else 4
if offset > len(data):
raise ValueError('truncated fixed field')
continue
else:
raise ValueError('unsupported protobuf wire type')
result.append((number, wire, value))
return result
def inputs(data):
def nested(blob, number):
return [value for n, wire, value in fields(blob) if n == number and wire == 2]
events = []
for update in nested(data, 2):
for event in nested(update, 11):
for button in nested(event, 1):
values = {n: v for n, wire, v in fields(button) if wire == 0}
if values.get(2, 0) == 0 and values.get(1, 0) in (0, 1, 2):
events.append(('button', values.get(1, 0)))
for switch in nested(event, 2):
values = {n: v for n, wire, v in fields(switch) if wire == 0}
events.append(('switch', values.get(1, 0)))
return events
class StateSocket:
"""Small RFC 6455 client; handles masked sends, ping/pong and fragmentation."""
def __init__(self, url):
parsed = urllib.parse.urlsplit(url)
port = parsed.port or (443 if parsed.scheme == 'https' else 80)
self.sock = socket.create_connection((parsed.hostname, port), timeout=3)
try:
if parsed.scheme == 'https':
self.sock = ssl.create_default_context().wrap_socket(self.sock, server_hostname=parsed.hostname)
self.sock.settimeout(3)
key = base64.b64encode(os.urandom(16)).decode()
target = parsed.path + ('?' + parsed.query if parsed.query else '')
request = (f'GET {target} HTTP/1.1\r\nHost: {parsed.netloc}\r\nUpgrade: websocket\r\n'
f'Connection: Upgrade\r\nSec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n')
self.sock.sendall(request.encode())
header = bytearray()
while not header.endswith(b'\r\n\r\n'):
header.extend(self.exact(1))
if len(header) > 16384:
raise ValueError('oversized WebSocket handshake')
lines = header.decode('latin1').split('\r\n')
if len(lines[0].split()) < 2 or lines[0].split()[1] != '101':
raise ValueError('WebSocket rejected: ' + lines[0])
values = {name.lower(): value for name, value in (line.split(':', 1) for line in lines[1:] if ':' in line)}
expected = base64.b64encode(hashlib.sha1((key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').encode()).digest()).decode()
if values.get('sec-websocket-accept', '').strip() != expected:
raise ValueError('invalid WebSocket handshake')
self.send(1, b'{"enable":true}')
except Exception:
self.close()
raise
def exact(self, count):
result = bytearray()
while len(result) < count:
part = self.sock.recv(count - len(result))
if not part:
raise ConnectionError('WebSocket closed')
result.extend(part)
return bytes(result)
def send(self, opcode, payload):
mask = os.urandom(4)
size = len(payload)
header = bytes([128 | opcode, 128 | size]) if size < 126 else bytes([128 | opcode, 128 | 126]) + struct.pack('!H', size)
self.sock.sendall(header + mask + bytes(byte ^ mask[i % 4] for i, byte in enumerate(payload)))
def messages(self, stopped):
fragments = bytearray()
kind = None
while not stopped.is_set():
# Idle connections are normal. A timeout midway through a frame reconnects.
self.sock.settimeout(1)
try:
first = self.exact(1)[0]
except socket.timeout:
continue
self.sock.settimeout(3)
second = self.exact(1)[0]
size = second & 127
if size == 126:
size = struct.unpack('!H', self.exact(2))[0]
elif size == 127:
size = struct.unpack('!Q', self.exact(8))[0]
if size > 1024 * 1024:
raise ValueError('oversized WebSocket frame')
mask = self.exact(4) if second & 128 else None
payload = self.exact(size)
if mask:
payload = bytes(byte ^ mask[i % 4] for i, byte in enumerate(payload))
opcode = first & 15
if opcode == 8:
return
if opcode == 9:
self.send(10, payload)
continue
if opcode == 10:
continue
if opcode in (1, 2):
kind, fragments = opcode, bytearray()
elif opcode != 0 or kind is None:
raise ValueError('invalid WebSocket continuation')
fragments.extend(payload)
if len(fragments) > 1024 * 1024:
raise ValueError('oversized WebSocket message')
if first & 128:
if kind == 2:
yield bytes(fragments)
kind, fragments = None, bytearray()
def close(self):
self.sock.close()
class Widget:
def __init__(self, args):
self.args = args
host = args.host if '://' in args.host else 'http://' + args.host
self.base = host.rstrip('/')
self.token = args.token or os.environ.get('BUSY_HTTP_PASSWORD', '')
self.stop = threading.Event()
self.events = queue.Queue(maxsize=16)
self.owned = 0
self.apps_mode = True
self.manager = False
self.track = ('closed', 'Open Music', '', 0, 0)
self.sampled = self.started = time.monotonic()
self.cycle_start = self.started
self.changed = 0
self.feedback = ''
self.feedback_until = 0
self.levels = [0] * 8
self.peaks = [0] * 8
self.last_label = self.last_counter = self.last_icon = None
self.assets_ready = False
self.input_thread = None
self.poll_thread = None
self.results = queue.Queue(maxsize=1)
self.commands = queue.Queue(maxsize=16)
def request(self, path, data=None, method=None, binary=False):
headers = {'X-API-Token': self.token} if self.token else {}
if data is not None:
headers['Content-Type'] = 'application/octet-stream' if binary else 'application/json'
if not binary:
data = json.dumps(data).encode()
request = urllib.request.Request(self.base + path, data=data, method=method, headers=headers)
with urllib.request.urlopen(request, timeout=3) as response:
return response.read()
def clear(self):
self.request('/api/display/draw?application_name=' + APP, method='DELETE')
def state_loop(self):
while not self.stop.is_set():
stream = None
try:
query = '' if self.manager or not self.token else '?' + urllib.parse.urlencode({'x-api-token': self.token})
stream = StateSocket(self.base + '/api/status/ws' + query)
print('Hardware controls connected', flush=True)
for data in stream.messages(self.stop):
for kind, value in inputs(data):
if kind == 'switch':
self.apps_mode = value == 3
elif self.apps_mode and time.monotonic() < self.owned:
try:
self.events.put_nowait(value)
except queue.Full:
pass
except (OSError, ValueError, ConnectionError) as error:
if not self.stop.is_set():
print('Button stream: ' + str(error), file=sys.stderr)
finally:
if stream:
stream.close()
self.owned = 0
self.stop.wait(3)
def music_loop(self):
while not self.stop.is_set():
command = 'status'
try:
command = self.commands.get_nowait()
except queue.Empty:
pass
try:
if self.args.demo:
elapsed = time.monotonic() - self.started
track = ('playing', 'Karma Police', 'Radiohead', (198 + elapsed) % 264, 264)
else:
completed = subprocess.run(['/usr/bin/osascript', '-e', SCRIPT, command], capture_output=True, text=True, timeout=10, check=True)
values = completed.stdout.rstrip('\n').split('\x1e')
if len(values) != 5:
raise ValueError('unexpected Music response')
track = (*values[:3], seconds(values[3]), seconds(values[4]))
result = (track, command, time.monotonic())
# Keep the latest sample, never an unbounded backlog.
try:
self.results.get_nowait()
except queue.Empty:
pass
self.results.put_nowait(result)
except (OSError, ValueError, subprocess.SubprocessError) as error:
print('Music: ' + str(error), file=sys.stderr)
self.stop.wait(1)
def apply_sample(self):
try:
track, command, sampled = self.results.get_nowait()
except queue.Empty:
return
if track[1:3] != self.track[1:3]:
self.cycle_start = self.changed = sampled
self.track, self.sampled = track, sampled
if command != 'status':
self.feedback = ('PLAY' if track[0] == 'playing' else 'PAUSE') if command == 'toggle' else ('NEXT' if command == 'next' else 'PREV')
self.feedback_until = time.monotonic() + 1.2
def marquee(self, now):
state, title, artist, _, _ = self.track
song_ms = max(4000, scroll_ms(title, 46, True), scroll_ms(artist, 46, True))
winamp_ms = scroll_ms('Powered by Winamp', 37)
apple_ms = scroll_ms('and Apple Music', 37)
llama = "It really whips the llama's ass!"
base = song_ms + winamp_ms + apple_ms
elapsed = (now - self.cycle_start) * 1000 % (base * 3 + scroll_ms(llama, 46))
if now < self.feedback_until:
return self.feedback, 'blank', False, 46
if state not in ('playing', 'paused'):
return title, 'blank', True, 46
if elapsed >= base * 3:
return llama, 'blank', False, 46
phase = elapsed % base
if phase < song_ms:
return title, 'blank', True, 46
if phase < song_ms + winamp_ms:
return 'Powered by Winamp', 'bolt', False, 37
return 'and Apple Music', 'apple', False, 37
def draw(self, now):
state, title, artist, position, duration = self.track
playing = state == 'playing'
value, icon, two_rows, width = self.marquee(now)
label = (value, artist if two_rows else '', icon, two_rows)
icon_asset = f'bolt-{int(now * 8) % 6}' if icon == 'bolt' and playing else icon
elements = []
if self.assets_ready and icon_asset != self.last_icon:
elements.append(dict(id='credit-icon', type='image', display='front', path=f'credit-{icon_asset}.png', x=64, y=1))
if label != self.last_label:
elements.append(text('track', value, 26, 0 if two_rows else 1, width, 'small' if two_rows else 'normal'))
elements.append(text('artist', artist if two_rows else ' ', 26, 7, 46, 'small', '#C0C8D0FF'))
mode = int((now - self.started) / 12) % 3
ms = now * 1000
burst = max(0, 1 - (now - self.changed) / .8)
elements.append(rectangle('visualizer-bg', 0, 0, 24, 9, '#060C08FF'))
for x in range(8):
wave = math.sin(ms / (130 + x * 23) + x * 1.7) + math.sin(ms / 410 + x * .9) * .6
beat = max(0, math.sin(ms / 120 + x * .43)) ** 3
target = max(1, min(12, int(3 + wave * 3 + beat * 7 + .5))) if playing else 0
self.levels[x] = target if target > self.levels[x] else max(target, self.levels[x] - 1)
self.peaks[x] = max(self.levels[x], self.peaks[x] - .25) if playing else 0
def point(t):
wave = (math.sin(t / 150 + x * .82) * math.cos(t / 680 + x * .31)) if mode == 2 else (.72 * math.sin(t / 170 + x * .7) + .28 * math.sin(t / 75 - x * 1.2))
return max(0, min(3, int(1.5 + wave * 1.5 + .5)))
for row in range(4):
color = '#060C08FF'
if not playing:
color = '#25432BFF' if row == 0 else color
elif burst > 0 and row < math.ceil(burst * 4):
color = '#FFFFFFFF' if row == 3 else '#D4FF68FF'
elif mode == 0:
if row == min(3, int(self.peaks[x] / 3)):
color = '#C6FFB0FF'
elif row * 3 < self.levels[x]:
color = ['#52F552FF','#88FF48FF','#E9ED43FF','#FF5353FF'][row]
elif row == point(ms):
color = '#B0FFFFFF' if mode == 2 else '#B8FF70FF'
elif row == point(ms - 100):
color = '#39B5CBFF' if mode == 2 else '#47A64BFF'
elif row == point(ms - 220):
color = '#164557FF' if mode == 2 else '#1C542FFF'
elif mode == 1 and min(1, point(ms)) <= row <= max(1, point(ms)):
color = '#298C38FF'
elements.append(rectangle(f'cell-{x}-{row}', x * 3, 7 - row * 2, 2, 1, color))
elapsed = min(2, max(0, now - self.sampled)) if playing else 0
current = min(duration, position + elapsed) if duration > 0 else position + elapsed
total = int(current)
counter = f'{total // 60:02d}:{total % 60:02d}'
if counter != self.last_counter:
elements.append(text('counter', counter, 0, 10, 24, 'tiny', '#65FF62FF'))
filled = int(46 * max(0, min(1, current / duration))) if duration > 0 else 0
elements += [rectangle('rail',26,14,46,2,'#182518FF'),
rectangle('progress',26,14,max(1,filled),2,('#65FF62FF' if playing else '#879E87FF') if filled else '#182518FF'),
rectangle('progress-head',26+max(0,filled-1),14,1,2,'#FFFFFFFF' if filled else '#182518FF')]
try:
self.request('/api/display/draw', dict(application_name=APP, priority=20, elements=elements))
self.owned = time.monotonic() + .5
self.last_label, self.last_counter, self.last_icon = label, counter, icon_asset
return True
except urllib.error.HTTPError as error:
self.owned = 0
self.last_label = self.last_counter = self.last_icon = None
if error.code != 409:
raise
return False
def run(self):
if not self.args.demo:
try:
self.manager = isinstance(json.loads(self.request('/api/_manager/state')), dict)
except (OSError, ValueError):
pass
self.clear()
for name in ['blank','bolt','apple'] + [f'bolt-{i}' for i in range(6)]:
self.request('/api/assets/upload?' + urllib.parse.urlencode(dict(application_name=APP, file=f'credit-{name}.png')), icon_png(name), binary=True)
self.assets_ready = True
self.poll_thread = threading.Thread(target=self.music_loop, daemon=True)
self.poll_thread.start()
if not self.args.demo:
self.input_thread = threading.Thread(target=self.state_loop, daemon=True)
self.input_thread.start()
print(f'{APP} → {self.base}; OK play/pause · BACK previous · START next', flush=True)
try:
while not self.stop.is_set():
start = time.monotonic()
self.apply_sample()
try:
owned = self.draw(start)
try:
button = self.events.get_nowait()
except queue.Empty:
button = None
if button is not None and owned and self.apps_mode and not self.args.demo:
if self.manager:
owner = json.loads(self.request('/api/_manager/state')).get('screenOwner') or {}
owned = owner.get('applicationName') == APP
if owned:
try:
self.commands.put_nowait(['toggle','previous','next'][button])
except queue.Full:
pass
except (OSError, ValueError) as error:
self.owned = 0
print('Display: ' + str(error), file=sys.stderr)
self.stop.wait(1)
self.stop.wait(max(0, .125 - (time.monotonic() - start)))
finally:
self.stop.set()
for thread in (self.input_thread, self.poll_thread):
if thread:
thread.join(timeout=3)
try:
self.clear()
except OSError:
pass
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--host', default='10.0.4.20', help='BUSY Bar, emulator, or Manager host[:port]')
parser.add_argument('--token', default='', help='Wi-Fi API token (or BUSY_HTTP_PASSWORD environment variable)')
parser.add_argument('--demo', action='store_true', help='Animated demo with sample metadata; no Music or hardware controls')
args = parser.parse_args()
if sys.platform != 'darwin' and not args.demo:
parser.error('Apple Music requires macOS. Use --demo to preview on other platforms.')
widget = Widget(args)
signal.signal(signal.SIGINT, lambda *_: widget.stop.set())
signal.signal(signal.SIGTERM, lambda *_: widget.stop.set())
try:
widget.run()
except (OSError, ValueError) as error:
widget.stop.set()
print(str(error), file=sys.stderr)
return 1
return 0
if __name__ == '__main__':
sys.exit(main())manifest.yaml
name: Apple Music Winamp
author: Nachenko
description: "macOS Apple Music controls with song, artist, progress, a time counter, decorative Winamp-style visualizers, and animated lightning credits."
tags:
- music
- animation
- info
- effect
preview: ./preview.giftest_app.py
import argparse
import base64
import hashlib
import socket
import struct
import threading
import time
import unittest
import urllib.error
from app import Widget, StateSocket, inputs, seconds, scroll_ms, icon_png
class WidgetTests(unittest.TestCase):
def widget(self):
return Widget(argparse.Namespace(host='127.0.0.1:8080', token='', demo=True))
def test_localized_timestamps_and_proto_defaults(self):
self.assertAlmostEqual(seconds('198,334'), 198.334)
self.assertEqual(seconds('missing value'), 0)
self.assertEqual(inputs(bytes([18,4,90,2,10,0])), [('button',0)])
self.assertEqual(inputs(bytes([18,6,90,4,10,2,8,2])), [('button',2)])
self.assertEqual(inputs(bytes([18,6,90,4,10,2,16,1])), [])
self.assertEqual(inputs(bytes([18,6,90,4,18,2,8,3])), [('switch',3)])
with self.assertRaises(ValueError):
inputs(bytes([18,99]))
def test_frames_fit_display_and_ids_do_not_accumulate(self):
w = self.widget()
w.track = ('playing','Song','Artist',100,200)
w.assets_ready = True
seen, frames = set(), []
w.request = lambda path, data=None, **kw: frames.append(data) if data else b'{}'
for offset in range(80):
w.draw(w.started + offset)
for frame in frames:
elements = frame['elements']
self.assertEqual(len({e['id'] for e in elements}), len(elements))
for e in elements:
seen.add(e['id'])
self.assertGreaterEqual(e['x'], 0)
self.assertGreaterEqual(e['y'], 0)
self.assertLessEqual(e['x'] + e.get('width',8),72)
self.assertLessEqual(e['y'] + e.get('height',0),16)
self.assertLess(len(seen),64)
self.assertTrue(any(e['id']=='counter' for f in frames for e in f['elements']))
def test_credit_timing_waits_for_longest_row(self):
w = self.widget()
artist = 'An artist with a very long name that needs scrolling'
w.track = ('playing','Song',artist,0,200)
duration = max(4000,scroll_ms('Song',46,True),scroll_ms(artist,46,True))
self.assertTrue(w.marquee(w.cycle_start+(duration-1)/1000)[2])
self.assertEqual(w.marquee(w.cycle_start+(duration+1)/1000)[1], 'bolt')
def test_yields_on_409_and_restores_all_text(self):
w = self.widget()
def reject(*a, **kw):
raise urllib.error.HTTPError('url',409,'busy',{},None)
w.request = reject
self.assertFalse(w.draw(time.monotonic()))
self.assertEqual(w.owned,0)
self.assertIsNone(w.last_label)
def test_icon_frames_are_real_distinct_pngs(self):
frames = [icon_png(f'bolt-{i}') for i in range(6)]
self.assertEqual(len(set(frames)),6)
for image in frames:
self.assertEqual(image[:8], b'\x89PNG\r\n\x1a\n')
self.assertEqual(struct.unpack('!II',image[16:24]),(8,10))
def test_websocket_handshake_preserves_case_and_sends_masked_subscription(self):
listener=socket.socket()
listener.bind(('127.0.0.1',0))
listener.listen(1)
received=[]
def server():
client,_=listener.accept()
with client:
request=b''
while not request.endswith(b'\r\n\r\n'):
request+=client.recv(1)
key=next(line.split(b': ',1)[1] for line in request.split(b'\r\n') if line.startswith(b'Sec-WebSocket-Key:'))
accept=base64.b64encode(hashlib.sha1(key+b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest())
client.sendall(b'HTTP/1.1 101 Switching Protocols\r\nSec-WebSocket-Accept: '+accept+b'\r\n\r\n')
header=client.recv(2)
size=header[1]&127
mask=b''
while len(mask)<4:mask+=client.recv(4-len(mask))
payload=b''
while len(payload)<size:payload+=client.recv(size-len(payload))
received.append(bytes(b^mask[i%4] for i,b in enumerate(payload)))
# Fragmented binary protobuf interleaved with a ping.
client.sendall(b'\x02\x03abc\x89\x01!\x80\x03def')
time.sleep(.1)
thread=threading.Thread(target=server)
thread.start()
stream=StateSocket('http://127.0.0.1:'+str(listener.getsockname()[1])+'/api/status/ws')
try:
self.assertEqual(next(stream.messages(threading.Event())),b'abcdef')
self.assertEqual(received,[b'{"enable":true}'])
finally:
stream.close();thread.join();listener.close()
if __name__=='__main__':unittest.main()Source and Provenance
Source commit: 26b19ba9ce2ba192c45b4c0eadcfad53df42b153
Content SHA-256: 04caf2eb3881fc99387f00b967e1ef04ec3765967f0f6e5b88f4bf6349a77ee2
Rate and Discuss
Comments and reactions live in this site’s GitHub repository. Sign in with GitHub to participate.





