Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2b914c2
feat: add interactive station selection menu via l/list command
deep5050 Apr 4, 2026
0af07dd
feat: add station cycling support and integrate automatic station inf…
deep5050 Apr 4, 2026
bd68f52
feat: add volume control support for mpv, vlc, and ffplay players
deep5050 Apr 4, 2026
39b3c1b
feat: replace console-based help output with interactive pick menu
deep5050 Apr 4, 2026
26849b2
refactor: replace pick with rich-based panel and table for runtime he…
deep5050 Apr 4, 2026
a902adc
feat: implement Vim-style command prompt with tab completion
deep5050 Apr 4, 2026
3c73caa
feat: implement fuzzy station search and auto-completion in the Vim-s…
deep5050 Apr 4, 2026
ed05a0f
chore: bump package version to 4.0.0
deep5050 Apr 4, 2026
b497067
docs: add release notes for version 4.0.0 to CHANGELOG.md
deep5050 Apr 4, 2026
ac2fc4e
refactor: implement persistent Rich Live UI display with centralized …
deep5050 Apr 5, 2026
2f3a1e5
Revert "refactor: implement persistent Rich Live UI display with cent…
deep5050 Apr 5, 2026
47a065e
feat: implement rich-based modal for station information and remove a…
deep5050 Apr 5, 2026
0a635ce
feat: add zen mode for minimalist station display and update UI layou…
deep5050 Apr 5, 2026
0503857
feat: add recording status popup and background FFmpeg process manage…
deep5050 Apr 5, 2026
3851492
style: update UI panel themes to use white borders and remove blinkin…
deep5050 Apr 5, 2026
d56116d
feat: enhance zen mode UI with station tags, codec information, and i…
deep5050 Apr 5, 2026
117e7f8
fix: refresh alias map after updates and implement station change val…
deep5050 Apr 6, 2026
0c8a825
feat: add Dockerfile and script to automate pipx installation testing…
deep5050 Apr 6, 2026
c2cb835
feat: add background process support with PID management and instance…
deep5050 Apr 11, 2026
1876624
feat: add background mode support with PID tracking and process manag…
deep5050 Apr 11, 2026
42b6964
feat: add HISTORY_FEATURE flag to configuration and feature manager
deep5050 Apr 12, 2026
83e50bc
feat: add auto-track info fetcher and improve case-insensitive statio…
deep5050 Apr 12, 2026
7f643da
refactor: centralize rich console instance and update song display ou…
deep5050 Apr 12, 2026
11e04c7
Revert "refactor: centralize rich console instance and update song di…
deep5050 Apr 12, 2026
7770610
feat: add desktop notifications for track changes using notify-send
deep5050 Apr 12, 2026
8476180
refactor: replace sys.exit calls with return statements to support id…
deep5050 Apr 13, 2026
267d6f6
feat: enhance desktop notifications with station branding and track m…
deep5050 Apr 17, 2026
a704f13
refactor: optimize track polling interval and update ffprobe metadata…
deep5050 Apr 17, 2026
873285c
chore: comment out volume increment/decrement help text in utilities …
deep5050 Apr 17, 2026
8396bd9
feat: add song identification feature using Shazam and FFmpeg recording
deep5050 Apr 17, 2026
5f39a98
feat: add Shazam song identification, background playback, and update…
deep5050 Apr 17, 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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
## 4.0.0

1. Shazam !! song identification built-in 🚀
2. Auto fetch current track information and send desktop notification
3. Volume control
4. Major UI changes
5. Command prompt with advanced tab completion 🚀
6. Real-time fuzzy station search across favorites and history
7. Interactive station selection menu via `l/list` command
8. Improved help menu and recording UI
9. Zen mode
10. Background play
11. Stability improvements

## 3.0.4

1. Release notes are now shown to the end users for future versions
Expand Down
42 changes: 26 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
- [x] Supports more than 40K stations !! :radio:
- [x] Record audio from live radio on demand :zap:
- [x] Get song information on run-time 🎶
- [x] Shazam identification of tracks
- [x] Saves last station information
- [x] Favorite stations :heart:
- [x] Selection menu for favorite stations
Expand All @@ -55,7 +56,7 @@
- [x] Sleep Timer (pomodoro) ⏲️
- [x] History/Recently Played stations
- [x] Scheduled Recording
- [ ] I'm feeling lucky! Play Random stations
- [x] I'm feeling lucky! Play Random stations


> See my progress ➡️ [here](https://github.com/users/deep5050/projects/5)
Expand Down Expand Up @@ -207,21 +208,30 @@ This will countdown until 18:30, then record the station for 30 minutes, and exi

### Runtime Commands

Input a command during the radio playback to perform an action. Available commands are:

```
Enter a command to perform an action: ?

t/T/track: Current song name (track info)
r/R/record: Record a station
f/F/fav: Add station to favorite list
s/S/search: Search for a new station
n/N/next: Play next station from search results or favorite list
timer/sleep: Set a sleep timer (duration in minutes)
rf/RF/recordfile: Specify a filename for the recording.
h/H/help/?: Show this help message
q/Q/quit: Quit radioactive
```
Radioactive features a modern, **Vim-style command bar** at the bottom of the screen. Instead of the old prompt, you now see a subtle `:` where you can type commands and search for stations.

#### Available Commands:
| Shortcut | Full Command | Description |
| :--- | :--- | :--- |
| `p` | `play/pause` | Toggle current station playback |
| `t` | `track` | Show current track info |
| `i` | `info` | Show station details |
| `r` | `record` | Start/Stop recording |
| `rf` | `recordfile` | Record with a specific filename |
| `f` | `fav` | Add current station to favorites |
| `l` | `list` | Open favorite station selection menu |
| `s` | `search` | Search for a new station online |
| `n` | `next` | Play next station (from search/favs) |
| `timer` | `sleep` | Set a sleep timer |
| `v` | `volume` | Set volume (e.g., `v 50`) |
| `sz` | `shazam` | Identify current song using Shazam |
| `b` | `background` | Run radioactive in the background |
| `q` | `quit` | Exit Radioactive |

#### Power Features:
* **Tab Completion**: Type a few letters and press `Tab` or `Right Arrow` to auto-complete commands and station names.
* **Instant Suggestions**: As you type, the bar shows descriptive hints (e.g., typing `p` shows `(play/pause)`).
* **Universal Fuzzy Search**: If your input doesn't match a command, Radioactive instantly searches your **Favorites** and **History**. Just type the name of a station and press `Enter` to play it immediately!

### Sort Parameters

Expand Down
27 changes: 27 additions & 0 deletions docker/Dockerfile.pipx_test
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Dockerfile.pipx_test
# Used to test the installation of radio-active using pipx across multiple python versions
ARG PYTHON_VERSION=3.9
FROM python:${PYTHON_VERSION}-slim

# Upgrade pip and install common dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
gcc \
python3-dev \
&& rm -rf /var/lib/apt/lists/*

RUN pip install --upgrade pip setuptools wheel
RUN pip install pipx
RUN pipx ensurepath

# Install radio-active from PyPI using pipx
# Add /root/.local/bin to PATH where pipx installs binaries
ENV PATH="/root/.local/bin:${PATH}"
RUN pipx install radio-active

# test if radioactive/radio commands are working and show version
RUN radioactive --version
RUN radio --version

# Smoke test: check help output
RUN radioactive --help
1 change: 1 addition & 0 deletions features.conf
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ SEARCH_FEATURE=true
CYCLE_FEATURE=true
INFO_FEATURE=true
TIMER_FEATURE=true
HISTORY_FEATURE=true
163 changes: 122 additions & 41 deletions radioactive/__main__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
#!/usr/bin/env python
import atexit
import os
import signal
import sys
from time import sleep

import psutil
from zenlog import log

from radioactive.alias import Alias
Expand All @@ -14,6 +16,7 @@
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,
Expand Down Expand Up @@ -46,22 +49,22 @@
global ffplay # always needed
global player

# check target URL for the last time
if options["target_url"].strip() == "":
log.error("something is wrong with the url")
sys.exit(1)

if options["audio_player"] == "vlc":
# check target URL
target_url = (options.get("target_url") or "").strip()
if target_url == "":
log.info("Type 's' to search for a station or '?' for help")
player = None
elif options["audio_player"] == "vlc":
from radioactive.vlc import VLC

vlc = VLC()
vlc = VLC(options["volume"])
vlc.start(options["target_url"])
player = vlc

elif options["audio_player"] == "mpv":
from radioactive.mpv import MPV

mpv = MPV()
mpv = MPV(options["volume"])
mpv.start(options["target_url"])
player = mpv

Expand All @@ -73,32 +76,32 @@
log.error("Unsupported media player selected")
sys.exit(1)

if options["curr_station_name"].strip() == "":
if (options.get("curr_station_name") or "").strip() == "":
options["curr_station_name"] = "N/A"

handle_save_last_station(
last_station, options["curr_station_name"], options["target_url"]
)
if target_url != "":
handle_save_last_station(last_station, options["curr_station_name"], target_url)

handle_save_to_history(history, options["curr_station_name"], options["target_url"])
handle_save_to_history(history, options["curr_station_name"], target_url)

if options["add_to_favorite"]:
handle_add_to_favorite(
alias, options["curr_station_name"], options["target_url"]
)
if options["add_to_favorite"]:
handle_add_to_favorite(alias, options["curr_station_name"], target_url)

handle_current_play_panel(options["curr_station_name"])
handle_current_play_panel(options["curr_station_name"])

if options["record_stream"]:
handle_record(
options["target_url"],
options["curr_station_name"],
options["record_file_path"],
options["record_file"],
options["record_file_format"],
options["loglevel"],
options.get("record_duration"),
)
if target_url == "":
log.error("Cannot record in idle mode. Please select a station first.")
else:
handle_record(
target_url,
options["curr_station_name"],
options["record_file_path"],
options["record_file"],
options["record_file_format"],
options["loglevel"],
options.get("record_duration"),
)

handle_listen_keypress(
alias,
Expand All @@ -111,44 +114,118 @@
record_file_format=options["record_file_format"],
loglevel=options["loglevel"],
handler=handler,
last_station=last_station,
station_list=station_list,
history=history,
audio_player=options["audio_player"],
volume=options["volume"],
)


def main():
log.level("info")

app = App()

options = parse_options()

VERSION = app.get_version()

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()

# --------------- app logic starts here ------------------- #

if options["version"]:
log.info("RADIO-ACTIVE : version {}".format(VERSION))
sys.exit(0)

handle_welcome_screen()

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

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

if options["kill_ffplays"]:
kill_background_ffplays()
pid_file = get_pid_path()
if os.path.exists(pid_file):
with open(pid_file, "r") as f:
try:
pid = int(f.read().strip())
if psutil.pid_exists(pid):
os.kill(pid, signal.SIGTERM)
log.info(
f"Terminated background radioactive process (PID: {pid})"
)
except:

Check notice on line 163 in radioactive/__main__.py

View check run for this annotation

codefactor.io / CodeFactor

radioactive/__main__.py#L163

Do not use bare 'except'. (E722)
pass

Check notice on line 164 in radioactive/__main__.py

View check run for this annotation

codefactor.io / CodeFactor

radioactive/__main__.py#L163-L164

Try, Except, Pass detected. (B110)
try:
os.remove(pid_file)
except:

Check notice on line 167 in radioactive/__main__.py

View check run for this annotation

codefactor.io / CodeFactor

radioactive/__main__.py#L167

Do not use bare 'except'. (E722)
pass

Check notice on line 168 in radioactive/__main__.py

View check run for this annotation

codefactor.io / CodeFactor

radioactive/__main__.py#L167-L168

Try, Except, Pass detected. (B110)
sys.exit(0)

# --------------- PID check ------------------- #
pid_file = get_pid_path()
if os.path.exists(pid_file):
with open(pid_file, "r") as f:
try:
content = f.read().strip()
if content:
old_pid = int(content)
if psutil.pid_exists(old_pid):
proc = psutil.Process(old_pid)
# Check if it's likely our app
if (
"python" in proc.name().lower()
or "radioactive" in proc.name().lower()
):
log.warning(
f"Another instance of radioactive is already running (PID: {old_pid})"
)
try:
choice = input(
"Open the existing one or open a new instance? (e/n): "
).lower()
if choice == "e":
log.info(
"Continuing with existing instance. Exiting."
)
sys.exit(0)
elif choice == "n":
log.info("Starting a new instance.")
else:
log.info("Invalid choice. Exiting.")
sys.exit(1)
except EOFError:
sys.exit(0)
except (ValueError, psutil.NoSuchProcess, Exception) as e:
log.debug(f"Error checking PID: {e}")
pass

# Save current PID
with open(pid_file, "w") as f:
f.write(str(os.getpid()))

def cleanup():
if os.path.exists(pid_file):
try:
with open(pid_file, "r") as f:
content = f.read().strip()
if content:
pid = int(content)
if pid == os.getpid():
os.remove(pid_file)
except:

Check notice on line 222 in radioactive/__main__.py

View check run for this annotation

codefactor.io / CodeFactor

radioactive/__main__.py#L222

Do not use bare 'except'. (E722)
pass

Check notice on line 223 in radioactive/__main__.py

View check run for this annotation

codefactor.io / CodeFactor

radioactive/__main__.py#L222-L223

Try, Except, Pass detected. (B110)

atexit.register(cleanup)

handle_welcome_screen()

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

Expand Down Expand Up @@ -340,7 +417,8 @@
) = handle_user_choice_from_search_result(handler, response)
final_step(options, last_station, alias, handler, history, response)
else:
sys.exit(0)
log.info("No stations found for this country.")
final_step(options, last_station, alias, handler, history)

# -------------- state ------------- #
if options["discover_state"]:
Expand All @@ -357,7 +435,8 @@
) = handle_user_choice_from_search_result(handler, response)
final_step(options, last_station, alias, handler, history, response)
else:
sys.exit(0)
log.info("No stations found for this state.")
final_step(options, last_station, alias, handler, history)

# ----------- language ------------ #
if options["discover_language"]:
Expand All @@ -374,7 +453,8 @@
) = handle_user_choice_from_search_result(handler, response)
final_step(options, last_station, alias, handler, history, response)
else:
sys.exit(0)
log.info("No stations found for this language.")
final_step(options, last_station, alias, handler, history)

# -------------- tag ------------- #
if options["discover_tag"]:
Expand All @@ -391,7 +471,8 @@
) = handle_user_choice_from_search_result(handler, response)
final_step(options, last_station, alias, handler, history, response)
else:
sys.exit(0)
log.info("No stations found for this tag.")
final_step(options, last_station, alias, handler, history)

# -------------------- NOTHING PROVIDED --------------------- #
if (
Expand Down Expand Up @@ -439,11 +520,11 @@
# print(response)
final_step(options, last_station, alias, handler, history, response)
else:
sys.exit(0)
final_step(options, last_station, alias, handler, history)
# ------------------------- direct play ------------------------#
if options["direct_play"] is not None:
options["curr_station_name"], options["target_url"] = handle_direct_play(
alias, options["direct_play"]
alias, history, options["direct_play"]
)
final_step(options, last_station, alias, handler, history)

Expand Down
Loading
Loading