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
17 changes: 17 additions & 0 deletions build-aux/alsa-utils.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "alsa-utils",
"config-opts": [
"--with-udev-rules-dir=${FLATPAK_DEST}/lib/udev/rules.d"
],
"sources": [
{
"type": "archive",
"url": "http://www.alsa-project.org/files/pub/utils/alsa-utils-1.2.14.tar.bz2",
"sha256": "0794c74d33fed943e7c50609c13089e409312b6c403d6ae8984fc429c0960741"
}
],
"cleanup": ["*"],
"modules": [
"libasound.json"
]
}
3 changes: 2 additions & 1 deletion build-aux/io.github.nokse22.high-tide.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"gstreamer.json",
"libportal.json",
"blueprint-compiler.json",
"alsa-utils.json",
"python3-pypresence.json",
"python3-tidalapi.json",
{
Expand All @@ -46,4 +47,4 @@
]
}
]
}
}
11 changes: 11 additions & 0 deletions build-aux/libasound.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "alsa-lib",
"sources": [
{
"type": "archive",
"url": "http://www.alsa-project.org/files/pub/lib/alsa-lib-1.2.14.tar.bz2",
"sha256": "be9c88a0b3604367dd74167a2b754a35e142f670292ae47a2fdef27a2ee97a32"
}
],
"cleanup": ["*"]
}
5 changes: 4 additions & 1 deletion data/io.github.nokse22.high-tide.gschema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
<default>0</default>
</key>
<key name="preferred-sink" type="i">
<range min="0" max="5"/>
<range min="0" max="6"/>
<default>0</default>
</key>
<key name="run-background" type="b">
Expand All @@ -46,6 +46,9 @@
</key>
<key name="discord-rpc" type="b">
<default>true</default>
</key>
<key name="alsa-device" type="s">
<default>'default'</default>
</key>
</schema>
</schemalist>
4 changes: 4 additions & 0 deletions data/ui/preferences.blp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ Adw.PreferencesDialog _preference_window {
title: _("Preferred Audio Sink");
subtitle: _("Gapless playback is disabled for the native Pipewire Sink due to a bug. Use 'Automatic' for gapless playback.");
}
Adw.ComboRow _alsa_device_row{
title: _("ALSA Device");
subtitle: _("Device used for exclusive ALSA access");
}
Adw.SwitchRow _normalize_row {
title: _("Normalize volume");
}
Expand Down
2 changes: 2 additions & 0 deletions po/POTFILES
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@ src/widgets/top_hit_widget.py
src/login.py
src/main.py
src/window.py
src/lib/utils.py
src/lib/player_object.py

64 changes: 50 additions & 14 deletions po/high-tide.pot
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-08-22 13:35+0200\n"
"POT-Creation-Date: 2025-08-22 20:42+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <[email protected]>\n"
Expand Down Expand Up @@ -129,26 +129,34 @@ msgid ""
msgstr ""

#: data/ui/preferences.blp:36
msgid "ALSA Device"
msgstr ""

#: data/ui/preferences.blp:37
msgid "Device used for exclusive ALSA access"
msgstr ""

#: data/ui/preferences.blp:40
msgid "Normalize volume"
msgstr ""

#: data/ui/preferences.blp:41
#: data/ui/preferences.blp:45
msgid "App"
msgstr ""

#: data/ui/preferences.blp:43
#: data/ui/preferences.blp:47
msgid "Run in the background"
msgstr ""

#: data/ui/preferences.blp:46
#: data/ui/preferences.blp:50
msgid "Use quadratic volume control"
msgstr ""

#: data/ui/preferences.blp:49
#: data/ui/preferences.blp:53
msgid "Display animated covers"
msgstr ""

#: data/ui/preferences.blp:52
#: data/ui/preferences.blp:56
msgid "Enable Discord Rich Presence"
msgstr ""

Expand Down Expand Up @@ -333,12 +341,12 @@ msgstr ""
msgid "Unknown"
msgstr ""

#: src/pages/album_page.py:64 src/pages/playlist_page.py:79
#: src/pages/album_page.py:64 src/pages/playlist_page.py:78
msgid "{} tracks ({})"
msgstr ""

#: src/pages/artist_page.py:91 src/widgets/card_widget.py:150
#: src/widgets/top_hit_widget.py:181
#: src/pages/artist_page.py:91 src/widgets/card_widget.py:151
#: src/widgets/top_hit_widget.py:179
msgid "Artist"
msgstr ""

Expand Down Expand Up @@ -395,15 +403,15 @@ msgstr ""
msgid "To be able to use this app, you need to login with your TIDAL account."
msgstr ""

#: src/pages/playlist_page.py:76
#: src/pages/playlist_page.py:75
msgid "by {}"
msgstr ""

#: src/widgets/card_widget.py:108
msgid "Track by {}"
msgstr ""

#: src/widgets/card_widget.py:141
#: src/widgets/card_widget.py:143 src/widgets/top_hit_widget.py:159
msgid "By {}"
msgstr ""

Expand All @@ -415,14 +423,42 @@ msgstr ""
msgid "Mix"
msgstr ""

#: src/main.py:103
#: src/main.py:107
msgid "Donate with Ko-Fi"
msgstr ""

#: src/main.py:104
#: src/main.py:108
msgid "Donate with Github"
msgstr ""

#: src/window.py:230
#: src/window.py:231
msgid "Playing Music"
msgstr ""

#: src/lib/utils.py:102
msgid "Default"
msgstr ""

#: src/lib/utils.py:300
msgid "Successfully added to my collection"
msgstr ""

#: src/lib/utils.py:303
msgid "Failed to add item to my collection"
msgstr ""

#: src/lib/utils.py:329
msgid "Successfully removed from my collection"
msgstr ""

#: src/lib/utils.py:331
msgid "Failed to remove item from my collection"
msgstr ""

#: src/lib/utils.py:371
msgid "Copied share URL in the clipboard"
msgstr ""

#: src/lib/player_object.py:268
msgid "ALSA Audio Device is not available"
msgstr ""
17 changes: 16 additions & 1 deletion src/lib/player_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
from . import utils
from . import discord_rpc

from gettext import gettext as _

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -74,6 +76,7 @@ class PlayerObject(GObject.GObject):
def __init__(
self,
preferred_sink: AudioSink = AudioSink.AUTO,
alsa_device: str = "default",
normalize: bool = False,
quadratic_volume: bool = False,
) -> None:
Expand Down Expand Up @@ -108,6 +111,8 @@ def __init__(

self.discord_rpc_enabled = True


self.alsa_device: str = alsa_device
# Configure audio sink
self._setup_audio_sink(preferred_sink)

Expand Down Expand Up @@ -181,7 +186,7 @@ def _setup_audio_sink(self, sink_type: AudioSink) -> None:
sink_map = {
AudioSink.AUTO: "autoaudiosink",
AudioSink.PULSE: "pulsesink",
AudioSink.ALSA: "alsasink",
AudioSink.ALSA: f"alsasink device={self.alsa_device}",
AudioSink.JACK: "jackaudiosink",
AudioSink.OSS: "osssink",
AudioSink.PIPEWIRE: "pipewiresink",
Expand Down Expand Up @@ -254,6 +259,16 @@ def _on_bus_error(self, bus: Any, message: Any) -> None:
print(f"Error: {err.message}")
print(f"Debug info: {debug}")

# Use string compare instead of error codes (Seems be just generic error)
if "Internal data stream error" in err.message and "not-linked" in debug:
logger.error("Stream error: Element not linked. Attempting to restart pipeline...")
self.play_track(self.playing_track)

elif "Error outputting to audio device" in err.message and "disconnected" in err.message:
utils.send_toast(_("ALSA Audio Device is not available"), 5)
self.pause()
self.pipeline.set_state(Gst.State.NULL)

def _on_buffering_message(self, bus: Any, message: Any) -> None:
buffer_per: int = message.parse_buffering()
mode, avg_in, avg_out, buff_left = message.parse_buffering_stats()
Expand Down
84 changes: 84 additions & 0 deletions src/lib/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import uuid
import re
import html
import subprocess

from gettext import gettext as _

Expand Down Expand Up @@ -80,6 +81,89 @@ def init() -> None:
session = None
cache = HTCache(session)

def get_alsa_devices() -> List[dict]:
"""Get ALSA devices"""
try:
alsa_devices = get_alsa_devices_from_aplay()
except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
alsa_devices = get_alsa_devices_from_proc()
return alsa_devices

def get_alsa_devices_from_aplay() -> List[dict]:
"""Get ALSA devices from aplay -l"""
result = subprocess.run(['aplay', '-l'], capture_output=True, text=True)

devices = [
{
"hw_device": "default",
"name": _("Default"),
}
]
for line in result.stdout.split('\n'):
# Example String: card 3: KA13 [FiiO KA13], device 0: USB Audio [USB Audio]
match = re.match(
r"^card\s+\d+:\s+([^[]+)\s+\[([^\]]+)\],\s+device\s+(\d+):\s+([^[]+)\s+\[([^\]]+)\]",
line
)
if match:
card_short_name = match.group(1).strip() # "KA13"
card_full_name = match.group(2).strip() # "FiiO KA13"
device = int(match.group(3)) # 0
device_short_name = match.group(4).strip() # "USB Audio"
device_full_name = match.group(5).strip() # "USB Audio"

# Persistent device string
hw_string = f"hw:CARD={card_short_name},DEV={device}"
devices.append({
"hw_device": hw_string,
"name": f"{card_full_name} - {device_full_name} ({hw_string})",
})

return devices


def get_alsa_devices_from_proc() -> List[dict]:
"""Get ALSA devices from files in /proc/asound"""
cards = {}
card_names = {}
with open("/proc/asound/cards", "r") as f:
for line in f:
# Example String: 3 [KA13 ]: USB-Audio - FiiO KA13
match = re.match(r"^\s*(\d+)\s+\[([^\]]+)\]\s*:\s*.+?\s-\s(.+)$", line)
if match:
index = int(match.group(1))
shortname = match.group(2).strip()
fullname = match.group(3).strip()
cards[index] = fullname
card_names[index] = shortname

devices = [
{
"hw_device": "default",
"name": _("Default"),
}
]
with open("/proc/asound/devices", "r") as f:
for line in f:
# Example String: 19: [ 3- 0]: digital audio playback
match = re.match(
r"^\s*\d+:\s+\[\s*(\d+)-\s*(\d+)\]:\s*digital audio playback", line
)
if match:
card, device = int(match.group(1)), int(match.group(2))
card_name = cards.get(card, f"Card {card}")
short_name = card_names.get(card, f"{card}")

# Persistent device string
hw_string = f"hw:CARD={short_name},DEV={device}"

devices.append({
"hw_device": hw_string,
"name": f"{card_name} ({hw_string})",
})

return devices


def get_artist(artist_id: str) -> Artist:
"""Get an artist object by ID from the cache.
Expand Down
Loading