Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions aur/rorqual
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
#!/bin/sh
export PYTHONPATH=/opt/rorqual/virtualenv/lib/python3.13/site-packages
python -m rorqual
# The venv's own interpreter, not the system one: it resolves site-packages
# without PYTHONPATH and pins the ABI the deps were built against. The console
# scripts in bin/ can't be used, their shebangs point into makepkg's $pkgdir.
venv_python=/opt/rorqual/virtualenv/bin/python

# Dangling symlink once Arch drops the minor version the venv was built with.
if [ ! -x "$venv_python" ]; then
echo "rorqual: /opt/rorqual/virtualenv was built for a Python version that is no longer installed; rebuild rorqual-venv" >&2
exit 1
fi

exec "$venv_python" -m rorqual "$@"
10 changes: 7 additions & 3 deletions src/rorqual/stream_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from .config import PrefetchingConfig
from .subsonic_client import Buffer, SubsonicClient

type FetchingState = Literal["pending", "fetching", "done"]
type FetchingState = Literal["pending", "fetching", "done", "failed"]
type StreamId = str


Expand Down Expand Up @@ -44,8 +44,12 @@ async def _worker(self) -> NoReturn:

self.fetching_state_callbacks(id, "fetching")
await self._subsonic.stream(id, buffer)
self.fetching_state_callbacks(id, "done")
self._cache.store(id, self._buffers[id].data)

if buffer.complete:
self._cache.store(id, buffer.data)
self.fetching_state_callbacks(id, "done")
else:
self.fetching_state_callbacks(id, "failed")

del self._task_streams[task]

Expand Down
11 changes: 11 additions & 0 deletions src/rorqual/subsonic_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ async def download_cover(self, cover_id: str, destination: BinaryIO) -> None:

async def stream(self, song_id: str, buffer: Buffer) -> None:
async with self.client.stream("GET", "/rest/stream", params=httpx.QueryParams({"id": song_id})) as response:
# On failure, Subsonic serves an XML document in place of the audio data, often with a 200 status
content_type = response.headers.get("content-type", "")
if not response.is_success or content_type.startswith(("text/xml", "application/xml")):
buffer.allocate(0)
buffer.finalize()
return

buffer.allocate(int(response.headers["content-length"]))
try:
async for chunk in response.aiter_raw():
Expand All @@ -121,6 +128,10 @@ def write(self, data: bytes) -> None:
self.bytes_written += len(data)
self.data_written.set()

@property
def complete(self) -> bool:
return 0 < self.bytes_written == len(self.data)

async def read(self, size: int) -> bytearray:
await self.started.wait()
requested_pos = min(self.cursor + size, len(self.data))
Expand Down
8 changes: 4 additions & 4 deletions src/rorqual/subsonic_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from collections.abc import Sequence
from typing import Any, Literal, cast

import httpx
from mpv import MPV, MpvEvent, MpvEventID

from subsonic.subsonic_rest_api import Child
Expand Down Expand Up @@ -42,13 +41,14 @@ def __init__(self, streams: StreamManager, loop: asyncio.AbstractEventLoop) -> N
self._mpv.observe_property("playlist-current-pos", self.dummy_property_handler)

def _open(self, url: str) -> SubsonicStreamFrontend:
parsed_url = httpx.URL(url)
# Parsed by hand: a URL parser lowercases the host, and song ids are case sensitive
scheme, separator, song_id = url.partition("://")

if parsed_url.scheme != self.PROTOCOL:
if scheme != self.PROTOCOL or not separator:
raise ValueError("Unsupported protocol")

return SubsonicStreamFrontend(
asyncio.run_coroutine_threadsafe(self._streams.fetch(parsed_url.host), self._loop).result(),
asyncio.run_coroutine_threadsafe(self._streams.fetch(song_id), self._loop).result(),
self._loop,
)

Expand Down
2 changes: 2 additions & 0 deletions src/rorqual/ui/playlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ def track_strip(self, track: Child, track_index: int) -> Strip:
icon = str(Emoji("down_arrow"))
elif fetch_state == "done":
icon = str(Emoji("heavy_check_mark"))
elif fetch_state == "failed":
icon = str(Emoji("heavy_multiplication_x"))
else:
icon = ""

Expand Down
Loading
Loading