Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
41227f0
fix(recorder): fix all bugs in recorder.py
dpnkrpl Apr 25, 2026
cf442d1
fix(utilities): replace sys.exit(1) with return None,None in search r…
dpnkrpl Apr 25, 2026
f205c37
fix(utilities): add player None guards to volume commands and remove …
dpnkrpl Apr 25, 2026
d88de8b
fix(utilities): guard player None in 'next' handler and sync AutoFetc…
dpnkrpl Apr 25, 2026
b78924c
fix(utilities): replace bare except in get_key() Windows path with sp…
dpnkrpl Apr 25, 2026
2bbce52
fix(utilities): fix mutable default argument in get_display() inner f…
dpnkrpl Apr 25, 2026
15a6e92
fix(utilities): prevent nested KeyError in handle_station_selection_menu
dpnkrpl Apr 25, 2026
54ac82e
fix(utilities): fix resource leak and bare except in background mode …
dpnkrpl Apr 25, 2026
fc560bd
refactor: improve application startup performance by deferring heavy …
dpnkrpl Apr 25, 2026
cd62e2b
feat: add interactive update notification modal and update installati…
dpnkrpl Apr 25, 2026
78e65f3
refactor: optimize ffplay process management using native system call…
dpnkrpl Apr 25, 2026
85c7051
feat: implement lazy-loaded country mapping and replace sys.exit call…
dpnkrpl Apr 25, 2026
766205d
refactor: optimize alias lookup with hash map and update file handlin…
dpnkrpl Apr 25, 2026
fdaab34
fix: improve history robustness with JSON error handling and atomic f…
dpnkrpl Apr 25, 2026
cc561b9
refactor: replace sys.exit calls with early returns and simplify hist…
dpnkrpl Apr 25, 2026
aabeee0
feat: enhance audio recording with codec mapping, metadata injection,…
dpnkrpl Apr 25, 2026
812c4a7
docs: add changelog entries for version 4.1.0 release
dpnkrpl Apr 25, 2026
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
## 4.1.0

1. Significant improvements on app loading speed :zap:
2. Recording quality improved 🎶
3. Faster station lookup
4. Optimize interaction with external player, improving play/pause time
5. Improved fuzzy find logic 🔍
6. Stability improvements for windows
7. Critical bug fixes 🐛
8. Minor UI fixes

## 4.0.2

1. More information on the shazam identified track
Expand Down
118 changes: 73 additions & 45 deletions radioactive/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,11 @@
import os
import signal
import sys
import threading
from time import sleep

import psutil
from zenlog import log

from radioactive.alias import Alias
from radioactive.app import App
from radioactive.ffplay import Ffplay, kill_background_ffplays
from radioactive.handler import Handler
from radioactive.help import show_help
from radioactive.history import History
from radioactive.last_station import Last_station
from radioactive.parser import parse_options
from radioactive.paths import get_pid_path
from radioactive.utilities import (
check_sort_by_parameter,
handle_add_station,
handle_add_to_favorite,
handle_current_play_panel,
handle_direct_play,
handle_favorite_table,
handle_history_table,
handle_listen_keypress,
handle_play_last_station,
handle_play_random_station,
handle_record,
handle_save_last_station,
handle_save_to_history,
handle_search_stations,
handle_station_selection_menu,
handle_station_uuid_play,
handle_update_screen,
handle_user_choice_from_search_result,
handle_welcome_screen,
)

# globally needed as signal handler needs it
# to terminate main() properly
# Globally needed as signal handler needs them.
# These are assigned inside main() or final_step().
ffplay = None
player = None

Expand All @@ -49,7 +16,18 @@ def final_step(options, last_station, alias, handler, history, station_list=None
global ffplay # always needed
global player

# check target URL
from zenlog import log

from radioactive.ffplay import Ffplay
from radioactive.utilities import (
handle_add_to_favorite,
handle_current_play_panel,
handle_listen_keypress,
handle_record,
handle_save_last_station,
handle_save_to_history,
)

target_url = (options.get("target_url") or "").strip()
if target_url == "":
log.info("Type 's' to search for a station or '?' for help")
Expand Down Expand Up @@ -123,28 +101,62 @@ def final_step(options, last_station, alias, handler, history, station_list=None


def main():
from zenlog import log

log.level("info")

app = App()
from radioactive.app import App
from radioactive.parser import parse_options

app = App()
options = parse_options()

VERSION = app.get_version()

# --- Fast early exits: avoid all heavy imports ---
if options["version"]:
log.info("RADIO-ACTIVE : version {}".format(VERSION))
sys.exit(0)

handler = Handler()
alias = Alias()
alias.generate_map()
last_station = Last_station()
history = History()
from radioactive.help import show_help

if options["show_help_table"]:
show_help()
sys.exit(0)

# --- Deferred heavy imports (saves ~260ms when not needed) ---
import psutil

from radioactive.alias import Alias
from radioactive.ffplay import kill_background_ffplays
from radioactive.handler import Handler
from radioactive.history import History
from radioactive.last_station import Last_station
from radioactive.paths import get_pid_path
from radioactive.utilities import (
check_sort_by_parameter,
handle_add_station,
handle_add_to_favorite,
handle_current_play_panel,
handle_direct_play,
handle_favorite_table,
handle_history_table,
handle_play_last_station,
handle_play_random_station,
handle_record,
handle_search_stations,
handle_station_selection_menu,
handle_station_uuid_play,
handle_update_modal,
handle_update_screen,
handle_user_choice_from_search_result,
handle_welcome_screen,
)

alias = Alias()
alias.generate_map()
last_station = Last_station()
history = History()

if options["flush_fav_list"]:
sys.exit(alias.flush())

Expand Down Expand Up @@ -226,6 +238,11 @@ def cleanup():

handle_welcome_screen()

# Run update check in background so it never blocks the interactive prompt.
# The modal will be shown if an update is detected.
_update_thread = threading.Thread(target=app.is_update_available, daemon=True)
_update_thread.start()

# ------------------ SCHEDULED RECORDING MODE ------------------ #
from radioactive.feature_flags import RECORDING_FEATURE

Expand Down Expand Up @@ -314,6 +331,7 @@ def cleanup():
log.info(f"Target URL: {options['target_url']}")
else:
# We need to use handler to validate UUID
handler = Handler()
options["curr_station_name"], options["target_url"] = (
handle_station_uuid_play(handler, options["search_station_uuid"])
)
Expand Down Expand Up @@ -400,7 +418,15 @@ def cleanup():

options["sort_by"] = check_sort_by_parameter(options["sort_by"])

handle_update_screen(app)
# Construct Handler as late as possible — right before we actually need the API.
# This avoids the ~50ms init cost for flag-only paths (--list, --kill, etc.)
handler = Handler()

# Update check is already running in the background thread started above;
# wait briefly (non-blocking) so it can print before we proceed to prompts
_update_thread.join(timeout=0.4)
if app.update_available:
handle_update_modal(app)

# ----------- country ----------- #
if options["discover_country_code"]:
Expand Down Expand Up @@ -560,6 +586,8 @@ def cleanup():


def signal_handler(sig, frame):
from zenlog import log

log.debug("You pressed Ctrl+C!")
log.debug("Stopping the radio")
if ffplay and ffplay.is_playing:
Expand Down
41 changes: 11 additions & 30 deletions radioactive/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,12 @@ def handle_record(
outfile_path = os.path.join(record_file_path, tmp_filename)

process = record_audio_from_url(
target_url, outfile_path, force_mp3, loglevel, duration
target_url,
outfile_path,
force_mp3,
loglevel,
duration,
station_name=curr_station_name,
)
return process, outfile_path

Expand All @@ -205,10 +210,9 @@ def handle_add_station(alias) -> None:

if left.strip() == "" or right.strip() == "":
log.error("Empty inputs not allowed")
sys.exit(1)
return
alias.add_entry(left, right)
log.info("New entry: {}={} added\n".format(left, right))
sys.exit(0)


def handle_add_to_favorite(alias, station_name: str, station_uuid_url: str) -> None:
Expand Down Expand Up @@ -337,37 +341,14 @@ def handle_direct_play(
return station_name, station_name_or_url
else:
log.debug("Direct play: station name provided")
# station name from fav list
# search for the station in fav list and return name and url

# search in favorites first
response = alias.search(station_name_or_url)

# if not found, check history
if not response and history:
log.debug("Not found in favorites, checking history")
# history object should have a search method or we iterate
# looking at history.py might be good
if hasattr(history, "search"):
response = history.search(station_name_or_url)
else:
# fallback iteration
for entry in history.get_list():
name = entry.get("name", "").strip()
val = entry.get("uuid_or_url", "").strip()
# also check stationuuid if it exists (older history)
sid = entry.get("stationuuid", "").strip()

token = station_name_or_url.strip().lower()
log.debug(
f"Comparing history entry: '{name.lower()}' with token: '{token}'"
)

if (
name.lower() == token
or val == station_name_or_url.strip()
or sid == station_name_or_url.strip()
):
log.debug(f"History match found: {name}")
response = entry
break

if not response:
log.debug(f"Search failed for: {station_name_or_url}")
Expand All @@ -376,7 +357,7 @@ def handle_direct_play(
)
return None, None
else:
log.debug(f"Direct play: {response}")
log.debug(f"Direct play found: {response}")
return response["name"], response.get("uuid_or_url") or response.get(
"stationuuid"
)
Expand Down
67 changes: 35 additions & 32 deletions radioactive/alias.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ def __init__(self):
from radioactive.paths import get_alias_path

self.alias_map = []
self.alias_dict = {} # for O(1) lookups
self.found = False

self.alias_path = get_alias_path()
Expand All @@ -33,62 +34,64 @@ def generate_map(self):
if os.path.exists(self.alias_path):
log.debug(f"Alias file at: {self.alias_path}")
try:
with open(self.alias_path, "r+") as f:
with open(self.alias_path, "r") as f:
alias_data = f.read().strip()
if alias_data == "":
log.debug("Empty alias list")
return
alias_list = alias_data.splitlines()
self.alias_map = []
self.alias_dict = {}
for alias in alias_list:
if alias.strip() == "":
# empty line pass
continue
temp = alias.split("==")
left = temp[0]
right = temp[1]
# may contain both URL and UUID
self.alias_map.append({"name": left, "uuid_or_url": right})
if "==" in alias:
temp = alias.split("==")
left = temp[0].strip()
right = temp[1].strip()
entry = {"name": left, "uuid_or_url": right}
self.alias_map.append(entry)
self.alias_dict[left.lower()] = entry
except Exception as e:
log.debug(f"could not get / parse alias data: {e}")

else:
log.debug("Alias file does not exist")

def search(self, entry):
"""searches for an entry in the fav list with the name
the right side may contain both url or uuid , need to check properly
"""
log.debug("Alias search: {}".format(entry))
if len(self.alias_map) > 0:
log.debug("looking under alias file")
for alias in self.alias_map:
if alias["name"].strip().lower() == entry.strip().lower():
log.debug(
"Alias found: {} == {}".format(
alias["name"], alias["uuid_or_url"]
)
)
self.found = True
return alias

log.debug("Alias not found")
else:
log.debug("Empty Alias file")
"""searches for an entry in the fav list with the name (fast O(1))"""
if not entry:
return None

target = entry.strip().lower()
if target in self.alias_dict:
res = self.alias_dict[target]
log.debug(f"Alias found: {res['name']} == {res['uuid_or_url']}")
self.found = True
return res

log.debug(f"Alias not found: {entry}")
return None

def add_entry(self, left, right):
"""Adds a new entry to the fav list"""
# Ensure map is current before adding
self.generate_map()
if self.search(left) is not None:
log.warning("An entry with same name already exists, try another name")
return False
else:
with open(self.alias_path, "a+") as f:

try:
with open(self.alias_path, "a") as f:
f.write("{}=={}\n".format(left.strip(), right.strip()))
log.info("Current station added to your favorite list")
# Refresh map after writing
self.generate_map()

# Update internal state directly to avoid re-reading the whole file
entry = {"name": left.strip(), "uuid_or_url": right.strip()}
self.alias_map.append(entry)
self.alias_dict[left.strip().lower()] = entry
return True
except Exception as e:
log.error(f"Could not add entry: {e}")
return False

def flush(self):
"""deletes all the entries in the fav list"""
Expand Down
Loading