Audio überall. Mopidy und Snapcast

source:

Musik hören geht alle Zeiten wieder durch mehrere Iterationen. Von wild, ungetaggten, Files die ich bin XMMS abspielte, über die ersten Versuche mit einer Verwaltung unter Amarok und eine sehr düstere Zeit mit, dem vom Herzen tiefst gehassten, iTunes. Das erste Licht am Ende des Tunnels sah ich mit MPD und dem Feature von ncmpcpp Tags und Files umzubenennen und richtig zu taggen. Später dann mit Picard vom MusicBrainz Projekt. Dann kam ich schnell auf ein Kommandozeilen-Tool was dies auch ganz wunderbar kann: beets. Ab da wollte ich mir nicht mehr von anderen Programmen in meine Files schreiben lassen und erst recht nicht sortieren. Nun gibt es Clouds oder ähnliche Marketingkonstrukte und man installiert sich Software auf dem Heimserver wie Plex oder seine Open-Source-Alternative Emby. Mit diesen Diensten kann man seine Mediensammlung theoretisch über das Internet konsumieren. Klappt in den besten Fällen ziemlich gut. Gerade die UI von Emby kommt mir ziemlich behäbig vor (kann natürlich auch an meinem Server liegen). Ich erinnerte mich an meine unbeschwerte Zeit mit MPD. Alles flutschte und funktionierte mit den verschiedensten Clients. Nun gibt es ein kleines Projekt mit dem Namen Mopidy. Ein in Python geschriebener Audio Server der so tut als ob er MPD ist aber noch viel mehr zu bieten hat. Es gibt Plugins um auf die verschiedensten Bibliotheken zuzugreifen. Unter anderem Spotify, Youtube, Google Music, usw. Also habe ich mich mal ran gesetzt und ein Emby Plugin geschrieben. Nun kann ich das langsame UI umgehen und viele weitere Möglichkeiten machen sich auf.

Ich lasse Mopidy und Snapcast in einem Docker Container laufen. Snapcast bietet Multi-Room Streaming. Mehrere Devices die über das Netz ihren Output synchronisieren und das bei mir sogar über ein VPN hinweg. So kann ich Raspberry Pi's über all im Haus verteilen. Diese starten dann den snapclient nach dem booten und die Musik spielt ab.

Mopidy muss dazu den Output in ein FIFO-File schreiben. Dazu muss in der Config folgendes stehen:

[audio]
output = audioresample ! audioconvert ! audio/x-raw,rate=48000,channels=2,format=S16LE ! wavenc ! filesink location=/tmp/snapfifo

Den Server startet man mit den Defaults mit einem einfachen snapserver. Der Client sollte den Server über Avahi finden. Da ich den Port an meinem Container nicht freigegeben habe mache ich das manuell mit snapclient -h 192.168.1.77. Mein Docker Setup findet man hier.

xonsh aus Bash heraus starten

Noch ein kleiner Nachtrag zu dem Artikel von gestern. Heute wollte ich mal wieder in meiner Blogging Vagrant Docker Box alles auf den neusten stand bringen. Ich benutze dazu vagrant provision. Dies startet das Provisioning, also holt meine neuen Config-Files. installiert neue Software usw. Ansible benutzt SSH um auf die Vagrant Box zuzugreifen und dies war auf einmal ein Problem. Ansible lief sofort an die Wand.

/bin/sh: sudo -H -S -n -u root /bin/sh -c 'echo BECOME-SUCCESS-mtobhjchhszgqaaixzbsbsolwbuprmhn; LANG=de_DE.UTF-7 LC_ALL=de_DE.UTF-8 LC_MESSAGES=de_DE.UTF-8 /usr/bin/python /home/vagrant/.ansible/tmp/ansible-tmp-1469778394.25-104576843898387/apk; rm -rf \"/home/vagrant/.ansible/tmp/ansible-tmp-1469778394.25-104576843898387/\" > /dev/null 2>&1' && sleep 0: not found\r\n

Loggte ich mich ein und führe das Ansible Playbook manuell aus, kein Problem. Ich hatte xonsh als Login Shell eingerichtet und da läuft etwas schief. SSH ruft den Befehl per exec auf und da hat xonsh noch ein paar Probleme. Es gibt den Shell Befehl exec und die Python-Funktion exec. Diese sind schwierig zu unterscheiden und schon läuft es schief. Es wird an einem Fix gearbeitet. Der soll auch in den nächsten Tagen released werden. Bis dahin habe ich mich dazu entschieden meine Login-Shell wieder auf Bash umzustellen und daraus dann xonsh zu starten.

Dazu benutze ich das File ~/.profile. Dies ist dafür da Sachen beim Login auszuführen. Aber nur wenn es ~/.bash_profile und ~/bash_login nicht gibt. Ich erweitere also ~/.profile.

[ -f /usr/local/bin/xonsh ] && exec /usr/local/bin/xonsh

Wenn es /usr/local/bin/xonsh gibt dann führe es aus.

Ich nahm an das es so funktionieren würde. Falsch gedacht. Es gab einige Probleme. Das schwerwiegendste war das LightDM mich nicht mehr einloggen wollte. Es liest beim einloggen die ~/.profile und der exec Befehl behindert das ausführen von i3. Das starten von xonsh aus .profile heraus erschien mir als ein guter Weg. Aber sind wir mal ehrlich: Das ich xonsh nicht per chsh setzen kann, endet wohl oder übel in heftigstes, unsauberes gefrickel. Was macht man sonst an einem Samstag Vormittag? Nun kam ich zu folgender Lösung: Ich starte xonsh aus der ~/.bashrc mit:

[ -f /usr/local/bin/xonsh ] && exec /usr/local/bin/xonsh

Überraschung: die wird nicht immer geladen. SSH auf eine Alpine Linux Box warf mich in eine bash Shell. Also sollte man sich eine ~/.bash_profile anlegen mit folgenden Inhalt:

if [ -f ~/.bashrc ]; then
  . ~/.bashrc
fi

Nun geht erstmal wieder alles. Ein Workaround... aber was solls? Ich warte auf das nächste Krachen. Aber am meisten freue ich mich auf den Fix.

Meine neue Shell: xonsh

Manchmal macht man Sachen die man selber nicht so wirklich versteht. Zum Beispiel hatte ich mich lange quer gestellt ZSH einzusetzen. Wieso eine alternative Shell? BASH ist doch sowas wie ein standard auf den Servern auf denen ich arbeite. Vor ein paar Jahren dann die erste Installation. Total abgeschreckt von der Konfiguration (bin bis Heute nicht dahinter gekommen wie das alles funktioniert), war mir oh-my-zsh ein Steigbügelhalter in die Welt von ZSH. Es ist eine Paket aus verschiedenen Plugins und Konfigurationen die das erste ZSH-Erlebnis, instant, beeindruckend gestaltet. Irgendwann war es mir zu langsam und ich stieg um auf prezto. Hat sich alles ein wenig flotter angefühlt und ich habe noch weniger verstanden was da im Hintergrund abläuft. Ja, man sollte sich die Sachen genauer anschauen und dann wäre es auch kein Problem. Aber manchmal fehlt mir Zeit weil ich meine Nase in tausend anderen Projekten habe. Es soll einfach funktionieren und the world a better place machen tun... oder zumindest meinen Alltag.

Nun bin ich auf einen Beitrag auf der PyCon über xonsh gestossen.

Es ist eine alternative Shell die in Python geschrieben ist und das Bindeglied zwischen Shell und Python sein möchte. Und das beste: Endlich eine Syntax in der Shell die ich mir merken kann. Nie wieder nachschauen wie if oder for in Bash funktionieren. "Nie wieder" ist ein harter Ausdruck. Selbst ich installiere xonsh nicht auf all meinen Servern. Es setzt Python 3.4 oder 3.5 voraus.

Bevor ich ein paar Anwendungsbeispiele nenne, möchte ich auf mein Setup hinweisen. Auf GitHub befinden sich meine Ansible Roles um meine Rechner einzurichten. Unter anderen auch xonsh.

Python und Shell Kommandos gemeinsam nutzen

Hier ein Beispiel für ein xonsh-Script das den Output von allen Docker-Commands nimmt und den Output in Files schreibt. Dies habe ich gebraucht um meine xonsh Extension xonsh-docker-tabcomplete zu testen. Ein Completer für Docker Kommandos die auch direkt auf die Docker-API zugreift. Danke docker-py und Python als Shell.

"""Ein xonsh-Script um alle `--help` Outputs als Textfiles
abzuspeichern.
"""
import os
import re

# `parser` ist ein Modul mit ein paar Regex Parsern.
from docker_tabcomplete import parser

# Regex um Versionsnummer zu finden.
RE_VERSION = re.compile(r'((?:\d+\.)?(?:\d+\.)?(?:\*|\d+))')

# Hier nutze ich das erste mal ein wenig xonsh Magic.
# Das `$()` macht genau das gleiche wie in Bash-Scripten. Es führt
# Ein Kommando aus aus gibt den Output zurück. In diesem Fall den
# Output von `docker --version` und sucht in Python per Regex nach
# der Versionsnummer.
DOCKER_VERSION = re.search(RE_VERSION, $(docker --version)).group(1)

# Es wird ein Verzeichnis für die Docker Version angelegt falls es noch
# nicht existiert.
if not os.path.exists('../tests/data/{}'.format(DOCKER_VERSION)):
    os.makedirs('../tests/data/{}'.format(DOCKER_VERSION))

# `parser.commands` nimmt den Output von `docker --help` und parst
# nach allen Docker Kommandos. Wieder muss nicht mit `subprocess`
# gefummelt werden sondern wir nutzen einfach `$()` von xonsh um
# an den Output zu gelangen.
COMMANDS = parser.commands($(docker --help))

# Jetzt wird sogar Python mit Shell mit Python gemischt.
for command in COMMANDS:
    with open('../tests/data/{}/{}.stdout'.format(DOCKER_VERSION, command), 'w') as f:

        # Es wird die Python-Variabel `command`, die gerade in benutzung ist,
        # in das Shell-Kommando `docker ... --help` eingefügt. Magic.
        f.write($(docker @(command) --help))

Dieses Script speichert man als create_test_data.xsh und kann es mit xonsh create_test_data.xsh ausführen.

Meine ~/.xonshrc

Yeah, endlich eine Shell Konfiguration in Python...

import os
import shutil

from xonsh.environ import git_dirty_working_directory


# XONSH ENVIRONMENT VARIABLES ############

# Die Environment Variabeln werden wie Python Variabeln definiert.
# Hier einige Beispiele. Diese sind vor allem um das Verhalten von
# xonsh anzupassen.

# xonsh supportet die bestehenden bash completion files.
# Zumindestens die meisten. Ich hatte Probleme mit dem
# Docker-Completion File und habe deswegen mein eigenes geschrieben.
$BASH_COMPLETIONS = ['/etc/bash_completion.d', '/usr/share/bash-completion/completions']

$CASE_SENSITIVE_COMPLETIONS = False
$SHELL_TYPE = 'best'
$SUPPRESS_BRANCH_TIMEOUT_MESSAGE = True
$VC_BRANCH_TIMEOUT = 5
$XONSH_COLOR_STYLE = 'monokai'

# OTHER ENVIRONMENT VARIABLES ############
$EDITOR = 'vim'
$PYTHONIOENCODING = 'UTF-8'
$LC_ALL = 'C.UTF-8'
$SSL_CERT_FILE = '/etc/ssl/certs/ca-certificates.crt'

# ALIASES ################################

# Aliase sind in einem dict gespeichert das einfach erweitert werden kann.

aliases['exec'] = aliases['xexec']
aliases['ll'] = 'ls -la'
aliases['tmux'] = 'tmux -2'

# Jetzt wird es spannend: Wir können Funktionen oder Lambdas als
# Aliase definieren. Hier der Fall das Mosh eine bestimmte
# Environment Variabel gesetzt haben muss.
def _alias_mosh(args, stdin=None):
    """A function to use as alias to get mosh running.

    There is a strange problem with mosh and xonsh. I have to set $SHELL to
    /bin/bash before running it. It should work with this little hack.
    """
    os.environ['SHELL'] = '/bin/bash'
    args.insert(0, 'mosh')
    cmd = ' '.join(args)
    os.system(cmd)

aliases['mosh'] = _alias_mosh

# GIT ####################################
$FORMATTER_DICT['branch_color'] = lambda: ('{BOLD_INTENSE_RED}'
                                           if git_dirty_working_directory(include_untracked=True)
                                           else '{BOLD_INTENSE_GREEN}')

# PATH ###################################

# Die Path Variabel ist eine Liste die ich einfach extende.
$PATH.extend(
    [
        $HOME + '/.local/bin',
        $HOME + '/miniconda3/bin',
        $HOME + '/node_modules_global/bin'
    ]
)

# XONTRIB ################################

# Hier habe ich ein paar Erweiterungen enabled. Bei manchen schaue ich
# ob der Befehl auf denen sie beruhen überhaupt da und einsatzfähig ist
# bevor ich sie anschalte. Was nützt mir `apt-get` wenn ich nicht auf
# einem Debian/Ubuntu System bin oder Docker nicht installiert ist?
# Dies teste ich mit `shutil.which`.
xontrib autoxsh vox_tabcomplete

if shutil.which('apt-get'):
    xontrib apt_tabcomplete

if shutil.which('docker'):
    xontrib docker_tabcomplete

Ein wenig Rescue

Natürlich kann alles noch ein wenig buggy sein. Einmal gab es Probleme mit den Schreibrechten auf dem History-File und ich konnte kein Terminal mehr öffnen oder mich einloggen. Was da helfen kann: Per SSH und anderer Shell draufschalten und die Shell ändern.

ssh meinuser@meinrechner sh

Fazit

Das ist natürlich nur ein Bruchteil der Möglichkeiten und Sachen die man ausprobieren kann. Es ist ein Anfang und bis jetzt bin ich begeistert. Also einfach mal das Youtube Video anschauen und ausprobieren. Muss ja nicht gleich die Standard-Shell werden wie bei mir ;-).

Python, urllib und CERTIFICATE_VERIFY_FAILED

Da denkt man sich einfach "mal kurz mein Blogging-Container neu bauen" und schon funktioniert nichts mehr. Es fing schon damit an das mein Ansible Playbook Probleme hatte Files herrunterzuladen. urllib brach immer mit urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] ab. Wie aus dem nichts. Vor ein paar Wochen gab es keine Probleme mit der urllib. Es gab sogar ein StackOverflow Posting welches mir riet einfach mal die ca-certificates zu installieren. Das Paket war aber schon installiert. Nach viel googeln un verzweifeln bin ich auf eine obskure Seite gestoßen von der ich kaum etwas verstand. Es gab wohl die Möglichkeit urllib über eine Environment-Variabel den Ort der CA-Zertifikate mitzuteilen. Dies scheint auch in der PEP 476 so beschrieben. requests scheint dies per Default zu machen. Ein export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt fixed die Situation in Alpine Linux. Ich frage mich nur was sich in ein paar Wochen so verändert hat das dieses Problem auftritt.

Ansible Playbooks und variable Hosts

Ich benutze Ansible für viele Sachen. Unter anderem habe ich ein Set an Playbooks und Roles um meine Server so anzupassen das ich mich auf ihnen wohl fühle. Bis jetzt habe ich immer Ansible auch auf den Servern installiert und es dann lokal ausgeführt. Aber dabei nutze ich nicht das eigentlich Ansible Feature: das Ganze Remote über SSH auszurollen. Problem: In den Playbooks muss es die Zeile - hosts: geben. Aber eigentlich will ich das alles Variabel ausführen können. Zum Beispiel nutze ich die gleichen Files auch um meine Vagrant Container einzurichten. Wieso also beim Aufruf die Hosts dem Playbook nicht einfach übergeben? Die Lösung ist dann doch wieder einmal einfacher als man denkt. Man benutzt die Möglichkeit in Playbooks und Roles Variabeln einzusetzen.

---
- hosts: "{{ hosts }}"
  roles:
    - git
    - tmux
    - vim
    - zsh

Diese Variabel übergeben wir beim Aufruf von Ansible.

ansible-playbook base.yml --extra-vars="hosts=myservers"