Release 0.5.2: no duplicate playback entries in gapless mode
The content type of the first track is taken from getSong instead of a short probe stream request, which the music server counted as a second playback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
## 0.5.2 – 2026-10-25
|
||||
|
||||
- Fix: gapless mode showed every track twice as playing on the server – the content type is now taken
|
||||
from `getSong` instead of a probe stream request
|
||||
|
||||
## 0.5.1 – 2026-10-25
|
||||
|
||||
- `CLIENT_NAME`: configurable client name towards the music server (`{player}` = BluOS player name)
|
||||
|
||||
@@ -33,7 +33,7 @@ import xml.etree.ElementTree as ET
|
||||
from email.utils import formatdate
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
VERSION = "0.5.1"
|
||||
VERSION = "0.5.2"
|
||||
STARTED = time.time()
|
||||
|
||||
# --------------------------------------------------------------------------- config
|
||||
@@ -277,18 +277,24 @@ class Subsonic:
|
||||
endpoint = re.sub(r"/rest/[^/]+$", f"/rest/{path}.view", parts.path)
|
||||
return urllib.parse.urlunsplit((base.scheme, base.netloc, endpoint, urllib.parse.urlencode(q), ""))
|
||||
|
||||
def duration(self, song_id):
|
||||
"""Track length in seconds from getSong (0 if unknown)."""
|
||||
def song_info(self, song_id):
|
||||
"""Length and content type from getSong - unlike a stream request, this doesn't count as playback."""
|
||||
info = {"duration": 0, "content_type": ""}
|
||||
if not (self.enabled and song_id):
|
||||
return 0
|
||||
return info
|
||||
try:
|
||||
q = urllib.parse.urlencode({**self._auth(), "id": song_id})
|
||||
with urllib.request.urlopen(f"{self.base}/rest/getSong.view?{q}", timeout=10) as r:
|
||||
song = ET.fromstring(r.read()).find(".//{http://subsonic.org/restapi}song")
|
||||
return int(song.get("duration", "0")) if song is not None else 0
|
||||
if song is not None:
|
||||
info = {"duration": int(song.get("duration", "0") or 0), "content_type": song.get("contentType", "")}
|
||||
except Exception as exc:
|
||||
log.debug("getSong %s failed: %s", song_id, exc)
|
||||
return 0
|
||||
return info
|
||||
|
||||
def duration(self, song_id):
|
||||
"""Track length in seconds from getSong (0 if unknown)."""
|
||||
return self.song_info(song_id)["duration"]
|
||||
|
||||
def cover_url(self, song_id, size=600):
|
||||
if not (self.enabled and song_id):
|
||||
@@ -459,8 +465,9 @@ class Renderer:
|
||||
|
||||
def _segment(self, uri, meta):
|
||||
info = parse_didl(meta)
|
||||
if not info["duration"]:
|
||||
info["duration"] = self.subsonic.duration(self.subsonic.song_id(uri))
|
||||
song = self.subsonic.song_info(self.subsonic.song_id(uri))
|
||||
info["duration"] = info["duration"] or song["duration"]
|
||||
info["content_type"] = song["content_type"]
|
||||
return gapless.Segment(self.subsonic.rewrite(uri), uri, meta, info, self._title(info))
|
||||
|
||||
def _close_session(self):
|
||||
@@ -1558,14 +1565,8 @@ class Handler(BaseHTTPRequestHandler):
|
||||
"""Serve a gapless session as one endless stream (optionally with ICY titles)."""
|
||||
if session.closed:
|
||||
return self._send(410, "", "text/plain")
|
||||
first = session.segments[0]
|
||||
try:
|
||||
with urllib.request.urlopen(urllib.request.Request(
|
||||
first.upstream, headers={"Range": "bytes=0-1"}), timeout=10) as probe:
|
||||
ctype = probe.headers.get("Content-Type", "audio/mpeg")
|
||||
except Exception as exc:
|
||||
log.warning("gapless %r: upstream failed: %s", label, exc)
|
||||
return self._send(502, "", "text/plain")
|
||||
# content type from getSong: a probe request would show up as a second playback on the server
|
||||
ctype = session.segments[0].info.get("content_type") or "audio/mpeg"
|
||||
icy = self.headers.get("Icy-MetaData") == "1"
|
||||
log.info("gapless stream %r -> player (%s%s)", label, ctype, ", ICY titles" if icy else "")
|
||||
self.close_connection = True
|
||||
|
||||
Reference in New Issue
Block a user