debug media server suite

This commit is contained in:
2025-11-27 09:50:21 +01:00
parent 0b31d75021
commit 3d1673157b
7 changed files with 360 additions and 11 deletions

142
tools/discover_upnp_devices.sh Executable file
View File

@@ -0,0 +1,142 @@
#!/bin/bash
#
# Script de découverte des devices UPnP via SSDP
#
# Usage: ./discover_upnp_devices.sh [timeout_seconds]
set -e
TIMEOUT="${1:-5}"
echo "=== Découverte des devices UPnP ===" >&2
echo "Timeout: ${TIMEOUT}s" >&2
echo >&2
# Créer un socket UDP pour envoyer la requête SSDP
DISCOVERY_MESSAGE="M-SEARCH * HTTP/1.1\r
Host: 239.255.255.250:1900\r
Man: \"ssdp:discover\"\r
MX: ${TIMEOUT}\r
ST: upnp:rootdevice\r
\r
"
# Envoyer la requête SSDP et collecter les réponses
echo "Envoi de la requête SSDP..." >&2
echo >&2
# Utiliser Python pour écouter les réponses SSDP
python3 - <<'PYTHON_SCRIPT' "$TIMEOUT"
import socket
import sys
import time
timeout = int(sys.argv[1])
# Créer un socket UDP
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.settimeout(timeout)
# Message SSDP M-SEARCH
msg = (
'M-SEARCH * HTTP/1.1\r\n'
'Host: 239.255.255.250:1900\r\n'
'Man: "ssdp:discover"\r\n'
'MX: {}\r\n'
'ST: upnp:rootdevice\r\n'
'\r\n'
).format(timeout)
# Envoyer la requête au multicast SSDP
sock.sendto(msg.encode(), ('239.255.255.250', 1900))
print("Écoute des réponses SSDP...", file=sys.stderr)
print(file=sys.stderr)
devices = {}
start_time = time.time()
try:
while time.time() - start_time < timeout:
try:
data, addr = sock.recvfrom(8192)
response = data.decode('utf-8', errors='ignore')
# Extraire l'URL de la description
location = None
server = None
usn = None
for line in response.split('\r\n'):
if line.lower().startswith('location:'):
location = line.split(':', 1)[1].strip()
elif line.lower().startswith('server:'):
server = line.split(':', 1)[1].strip()
elif line.lower().startswith('usn:'):
usn = line.split(':', 1)[1].strip()
if location and location not in devices:
devices[location] = {
'addr': addr[0],
'server': server,
'usn': usn
}
print(f"Trouvé: {location}", file=sys.stderr)
except socket.timeout:
break
except KeyboardInterrupt:
pass
finally:
sock.close()
print(file=sys.stderr)
print(f"=== {len(devices)} device(s) trouvé(s) ===", file=sys.stderr)
print(file=sys.stderr)
# Afficher les détails de chaque device
for location, info in devices.items():
print(f"Device: {location}", file=sys.stderr)
print(f" IP: {info['addr']}", file=sys.stderr)
if info['server']:
print(f" Server: {info['server']}", file=sys.stderr)
if info['usn']:
print(f" USN: {info['usn']}", file=sys.stderr)
# Récupérer la description XML
import urllib.request
try:
with urllib.request.urlopen(location, timeout=2) as response:
xml = response.read().decode('utf-8')
# Parser le XML pour trouver les services
import xml.etree.ElementTree as ET
root = ET.fromstring(xml)
# Namespaces UPnP
ns = {
'device': 'urn:schemas-upnp-org:device-1-0',
'service': 'urn:schemas-upnp-org:service-1-0'
}
# Trouver le nom du device
device_name = root.find('.//device:friendlyName', ns)
if device_name is not None:
print(f" Name: {device_name.text}", file=sys.stderr)
# Trouver le ContentDirectory service
for service in root.findall('.//device:service', ns):
service_type = service.find('device:serviceType', ns)
if service_type is not None and 'ContentDirectory' in service_type.text:
control_url = service.find('device:controlURL', ns)
if control_url is not None:
# Construire l'URL complète
from urllib.parse import urljoin
full_control_url = urljoin(location, control_url.text)
print(f" ContentDirectory Control URL: {full_control_url}", file=sys.stderr)
print(full_control_url) # Output pour utilisation dans scripts
except Exception as e:
print(f" Erreur lors de la récupération de la description: {e}", file=sys.stderr)
print(file=sys.stderr)
PYTHON_SCRIPT

132
tools/test_upnp_browse.sh Executable file
View File

@@ -0,0 +1,132 @@
#!/bin/bash
#
# Script de test pour les requêtes Browse UPnP ContentDirectory
#
# Usage: ./test_upnp_browse.sh [options]
#
# Options:
# -u URL URL de contrôle du service ContentDirectory
# -o ID Object ID à parcourir (défaut: "0")
# -f FLAG BrowseFlag: BrowseMetadata ou BrowseDirectChildren (défaut: BrowseDirectChildren)
# -s INDEX StartingIndex (défaut: 0)
# -c COUNT RequestedCount (défaut: 0 = tous)
# -h Afficher cette aide
set -e
# Valeurs par défaut
CONTROL_URL="http://localhost:8080/device/88b84e76-4de0-4ee6-b794-99cc4a278cc9/service/ContentDirectory/control"
OBJECT_ID="0"
BROWSE_FLAG="BrowseDirectChildren"
STARTING_INDEX=0
REQUESTED_COUNT=0
# Fonction d'aide
show_help() {
cat << EOF
Script de test pour les requêtes Browse UPnP ContentDirectory
Usage: $0 [options]
Options:
-u URL URL de contrôle du service ContentDirectory
(défaut: http://localhost:8080/device/.../ContentDirectory/control)
-o ID Object ID à parcourir (défaut: "0")
-f FLAG BrowseFlag: BrowseMetadata ou BrowseDirectChildren
(défaut: BrowseDirectChildren)
-s INDEX StartingIndex (défaut: 0)
-c COUNT RequestedCount (défaut: 0 = tous)
-h Afficher cette aide
Exemples:
# Parcourir la racine
$0
# Parcourir un canal Radio Paradise
$0 -o "radio-paradise:channel:main"
# Obtenir les métadonnées d'un container
$0 -o "radio-paradise:channel:main:history" -f BrowseMetadata
# Parcourir l'historique
$0 -o "radio-paradise:channel:main:history"
EOF
}
# Parser les options
while getopts "u:o:f:s:c:h" opt; do
case $opt in
u) CONTROL_URL="$OPTARG" ;;
o) OBJECT_ID="$OPTARG" ;;
f) BROWSE_FLAG="$OPTARG" ;;
s) STARTING_INDEX="$OPTARG" ;;
c) REQUESTED_COUNT="$OPTARG" ;;
h) show_help; exit 0 ;;
\?) echo "Option invalide: -$OPTARG" >&2; show_help; exit 1 ;;
esac
done
# Afficher les paramètres
echo "=== Test Browse UPnP ===" >&2
echo "Control URL: $CONTROL_URL" >&2
echo "Object ID: $OBJECT_ID" >&2
echo "Browse Flag: $BROWSE_FLAG" >&2
echo "Starting Index: $STARTING_INDEX" >&2
echo "Requested Count: $REQUESTED_COUNT" >&2
echo >&2
# Échapper l'Object ID pour XML
OBJECT_ID_ESCAPED=$(echo "$OBJECT_ID" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g; s/'"'"'/\&apos;/g')
# Construire et envoyer la requête SOAP
RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" -X POST "$CONTROL_URL" \
-H "Content-Type: text/xml; charset=\"utf-8\"" \
-H "SOAPACTION: \"urn:schemas-upnp-org:service:ContentDirectory:1#Browse\"" \
-d "<?xml version=\"1.0\"?>
<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">
<s:Body>
<u:Browse xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">
<ObjectID>$OBJECT_ID_ESCAPED</ObjectID>
<BrowseFlag>$BROWSE_FLAG</BrowseFlag>
<Filter>*</Filter>
<StartingIndex>$STARTING_INDEX</StartingIndex>
<RequestedCount>$REQUESTED_COUNT</RequestedCount>
<SortCriteria></SortCriteria>
</u:Browse>
</s:Body>
</s:Envelope>")
# Extraire le code de statut HTTP
HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS:" | cut -d: -f2)
BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS:/d')
echo "=== HTTP Status: $HTTP_STATUS ===" >&2
echo >&2
# Afficher la réponse formatée
if [ -n "$BODY" ]; then
echo "=== SOAP Response ===" >&2
echo "$BODY" | xmllint --format - 2>&1
# Extraire et décoder le DIDL-Lite
DIDL=$(echo "$BODY" | xmllint --xpath "string(//Result)" - 2>/dev/null || true)
if [ -n "$DIDL" ]; then
echo >&2
echo "=== DIDL-Lite Content ===" >&2
echo "$DIDL" | xmllint --format - 2>&1 || echo "$DIDL"
fi
# Afficher NumberReturned et TotalMatches
echo >&2
echo "=== Statistics ===" >&2
NUMBER_RETURNED=$(echo "$BODY" | xmllint --xpath "string(//NumberReturned)" - 2>/dev/null || echo "N/A")
TOTAL_MATCHES=$(echo "$BODY" | xmllint --xpath "string(//TotalMatches)" - 2>/dev/null || echo "N/A")
UPDATE_ID=$(echo "$BODY" | xmllint --xpath "string(//UpdateID)" - 2>/dev/null || echo "N/A")
echo "NumberReturned: $NUMBER_RETURNED" >&2
echo "TotalMatches: $TOTAL_MATCHES" >&2
echo "UpdateID: $UPDATE_ID" >&2
else
echo "Aucune réponse du serveur" >&2
exit 1
fi