Last active
June 11, 2025 17:17
-
-
Save antonl-dev/e30c8ac46d441f66cd6f2bbc8564d0fa to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //Radio Switcher - Discovery mode with Fading, Mute, Refactored For Maintainability + Small Mode | |
| // | |
| // sudo dnf install gcc-c++ mpv-devel ncurses-devel pkg-config make | |
| // Download 'json.hpp' from https://github.com/nlohmann/json. | |
| // g++ radio.cpp -o radio $(pkg-config --cflags --libs mpv ncurses) -pthread | |
| // | |
| // - with Audio Fading | |
| // - Mute functionality | |
| // - Restructured | |
| // - JSON Station Name Logging | |
| // - Breakdown of UI, Main Loop, and Event Handlers | |
| // - Small Mode: Auto-rotation through all stations | |
| // Thanks to gemini-2.5-pro-preview-06-05 and claude-sonnet-4-20250514 | |
| #include <iostream> | |
| #include <vector> | |
| #include <string> | |
| #include <thread> | |
| #include <chrono> | |
| #include <memory> | |
| #include <atomic> | |
| #include <algorithm> | |
| #include <mutex> // ADDED: For protecting the string | |
| #include <cstring> // ADDED: For strcmp | |
| #include <fstream> // ADDED: For file I/O | |
| #include <iomanip> // ADDED: For pretty JSON output | |
| #include <ctime> // ADDED: For timestamps | |
| #include <mpv/client.h> | |
| #include <ncurses.h> | |
| #include "json.hpp" | |
| // === Configuration === | |
| #define FADE_TIME_MS 900 | |
| #define SMALL_MODE_TOTAL_TIME_SECONDS 720 // 12 minutes total | |
| // === Helper Functions === | |
| void check_mpv_error(int status, const std::string& context) { | |
| if (status < 0) { | |
| if (stdscr != NULL && !isendwin()) { | |
| endwin(); | |
| } | |
| std::cerr << "MPV Error (" << context << "): " << mpv_error_string(status) << std::endl; | |
| exit(1); | |
| } | |
| } | |
| //================================================================================ | |
| // 1. RadioStream Class | |
| //================================================================================ | |
| class RadioStream { | |
| public: | |
| RadioStream(int id, std::string name, std::string url) | |
| : m_id(id), m_name(std::move(name)), m_url(std::move(url)), | |
| m_mpv(nullptr), m_current_title("Initializing..."), | |
| m_is_fading(false), m_target_volume(0.0), m_current_volume(0.0), | |
| m_is_muted(false), m_pre_mute_volume(100.0) {} | |
| ~RadioStream() { | |
| destroy(); | |
| } | |
| // Disable copying. | |
| RadioStream(const RadioStream&) = delete; | |
| RadioStream& operator=(const RadioStream&) = delete; | |
| // ADDED: Custom move constructor because std::atomic members are not movable. | |
| RadioStream(RadioStream&& other) noexcept | |
| : m_id(other.m_id), | |
| m_name(std::move(other.m_name)), | |
| m_url(std::move(other.m_url)), | |
| m_mpv(other.m_mpv) | |
| { | |
| // For atomic members, we load the value from the source and store it in the new object. | |
| m_is_fading.store(other.m_is_fading.load()); | |
| m_target_volume.store(other.m_target_volume.load()); | |
| m_current_volume.store(other.m_current_volume.load()); | |
| m_is_muted.store(other.m_is_muted.load()); | |
| m_pre_mute_volume.store(other.m_pre_mute_volume.load()); | |
| // For the string, we must lock its mutex to safely get the value. | |
| m_current_title = other.getCurrentTitle(); | |
| // IMPORTANT: Nullify the source's mpv handle to prevent double-free. | |
| other.m_mpv = nullptr; | |
| } | |
| // ADDED: Custom move assignment operator. | |
| RadioStream& operator=(RadioStream&& other) noexcept { | |
| if (this != &other) { | |
| destroy(); // Clean up our own resources first. | |
| m_id = other.m_id; | |
| m_name = std::move(other.m_name); | |
| m_url = std::move(other.m_url); | |
| m_mpv = other.m_mpv; | |
| m_is_fading.store(other.m_is_fading.load()); | |
| m_target_volume.store(other.m_target_volume.load()); | |
| m_current_volume.store(other.m_current_volume.load()); | |
| m_is_muted.store(other.m_is_muted.load()); | |
| m_pre_mute_volume.store(other.m_pre_mute_volume.load()); | |
| m_current_title = other.getCurrentTitle(); | |
| other.m_mpv = nullptr; | |
| } | |
| return *this; | |
| } | |
| void initialize(double initial_volume) { | |
| m_mpv = mpv_create(); | |
| if (!m_mpv) { | |
| throw std::runtime_error("Failed to create MPV instance for " + m_name); | |
| } | |
| check_mpv_error(mpv_initialize(m_mpv), "mpv_initialize for " + m_name); | |
| mpv_set_property_string(m_mpv, "vo", "null"); | |
| mpv_set_property_string(m_mpv, "audio-display", "no"); | |
| mpv_set_property_string(m_mpv, "input-default-bindings", "no"); | |
| mpv_set_property_string(m_mpv, "terminal", "no"); | |
| mpv_set_property_string(m_mpv, "msg-level", "all=warn"); | |
| check_mpv_error(mpv_observe_property(m_mpv, m_id, "media-title", MPV_FORMAT_STRING), "observe media-title"); | |
| check_mpv_error(mpv_observe_property(m_mpv, m_id, "eof-reached", MPV_FORMAT_FLAG), "observe eof-reached"); | |
| const char* cmd[] = {"loadfile", m_url.c_str(), "replace", nullptr}; | |
| check_mpv_error(mpv_command_async(m_mpv, 0, cmd), "loadfile for " + m_name); | |
| m_current_volume = initial_volume; | |
| m_target_volume = initial_volume; | |
| mpv_set_property_async(m_mpv, 0, "volume", MPV_FORMAT_DOUBLE, &initial_volume); | |
| } | |
| void destroy() { | |
| if (m_mpv) { | |
| mpv_terminate_destroy(m_mpv); | |
| m_mpv = nullptr; | |
| } | |
| } | |
| std::string getStatusString(bool is_active, bool is_small_mode = false) const { | |
| if (m_is_muted) return "Muted"; | |
| if (m_is_fading) { | |
| return m_target_volume > m_current_volume ? "Fading In" : "Fading Out"; | |
| } | |
| if (is_small_mode && is_active) return "Auto-Playing"; | |
| return is_active ? "Playing" : "Silent"; | |
| } | |
| int getID() const { return m_id; } | |
| const std::string& getName() const { return m_name; } | |
| const std::string& getURL() const { return m_url; } | |
| mpv_handle* getMpvHandle() const { return m_mpv; } | |
| // CHANGED: Use a mutex to protect the string for thread-safe access. | |
| std::string getCurrentTitle() const { | |
| std::lock_guard<std::mutex> lock(m_title_mutex); | |
| return m_current_title; | |
| } | |
| void setCurrentTitle(const std::string& title) { | |
| std::lock_guard<std::mutex> lock(m_title_mutex); | |
| m_current_title = title; | |
| } | |
| bool isMuted() const { return m_is_muted; } | |
| void setMuted(bool muted) { m_is_muted = muted; } | |
| double getCurrentVolume() const { return m_current_volume; } | |
| void setCurrentVolume(double vol) { m_current_volume = vol; } | |
| double getPreMuteVolume() const { return m_pre_mute_volume; } | |
| void setPreMuteVolume(double vol) { m_pre_mute_volume = vol; } | |
| bool isFading() const { return m_is_fading; } | |
| void setFading(bool fading) { m_is_fading = fading; } | |
| double getTargetVolume() const { return m_target_volume; } | |
| void setTargetVolume(double vol) { m_target_volume = vol; } | |
| private: | |
| int m_id; | |
| std::string m_name; | |
| std::string m_url; | |
| mpv_handle* m_mpv; | |
| // CHANGED: m_current_title is now a regular string. | |
| std::string m_current_title; | |
| // ADDED: A mutex to protect m_current_title. 'mutable' allows it to be locked in const methods. | |
| mutable std::mutex m_title_mutex; | |
| // These can remain atomic as they are trivially copyable. | |
| std::atomic<bool> m_is_fading; | |
| std::atomic<double> m_target_volume; | |
| std::atomic<double> m_current_volume; | |
| std::atomic<bool> m_is_muted; | |
| std::atomic<double> m_pre_mute_volume; | |
| }; | |
| //================================================================================ | |
| // 2. UIManager Class | |
| //================================================================================ | |
| class UIManager { | |
| public: | |
| UIManager() { | |
| initscr(); cbreak(); noecho(); curs_set(0); keypad(stdscr, TRUE); timeout(100); | |
| } | |
| ~UIManager() { | |
| if (stdscr != NULL && !isendwin()) { endwin(); } | |
| } | |
| void draw(const std::vector<RadioStream>& stations, int active_station_idx, bool small_mode_active = false, int remaining_seconds = 0) { | |
| clear(); | |
| draw_header(small_mode_active, remaining_seconds); | |
| if (!stations.empty()) { | |
| draw_station_list(stations, active_station_idx, small_mode_active); | |
| draw_footer(small_mode_active); | |
| } | |
| refresh(); | |
| } | |
| int getInput() { return getch(); } | |
| private: | |
| void draw_header(bool small_mode_active, int remaining_seconds) { | |
| if (small_mode_active) { | |
| mvprintw(0, 0, "Radio Switcher - SMALL MODE ACTIVE | S: Exit Small Mode | Q: Quit"); | |
| mvprintw(1, 0, "Auto-rotating through all stations. Time left for current: %d seconds", remaining_seconds); | |
| } else { | |
| mvprintw(0, 0, "Radio Switcher (Refactored) | Q: Quit | Enter: Mute/Unmute | S: Small Mode"); | |
| } | |
| } | |
| void draw_station_list(const std::vector<RadioStream>& stations, int active_station_idx, bool small_mode_active) { | |
| for (size_t i = 0; i < stations.size(); ++i) { | |
| const auto& station = stations[i]; | |
| bool is_active = (static_cast<int>(i) == active_station_idx); | |
| if (is_active) attron(A_REVERSE); | |
| std::string status = station.getStatusString(is_active, small_mode_active); | |
| mvprintw(2 + i, 2, "[%s] %s: %s (Vol: %.0f)", | |
| status.c_str(), station.getName().c_str(), | |
| station.getCurrentTitle().c_str(), station.getCurrentVolume()); | |
| if (is_active) attroff(A_REVERSE); | |
| } | |
| } | |
| void draw_footer(bool small_mode_active) { | |
| if (!small_mode_active) { | |
| mvprintw(LINES - 2, 0, "Use UP/DOWN arrows to switch stations."); | |
| } else if (small_mode_active) { | |
| mvprintw(LINES - 2, 0, "Small Mode: Discovering radio stations automatically..."); | |
| } | |
| } | |
| }; | |
| //================================================================================ | |
| // 3. RadioPlayer Class | |
| //================================================================================ | |
| class RadioPlayer { | |
| public: | |
| RadioPlayer(std::vector<std::pair<std::string, std::string>> station_data) | |
| : m_ui(std::make_unique<UIManager>()), m_active_station_idx(0), m_quit_flag(false), | |
| m_small_mode_active(false), m_small_mode_start_time(std::chrono::steady_clock::now()), | |
| m_station_switch_duration(0) { | |
| if (station_data.empty()) { | |
| throw std::runtime_error("No radio stations provided."); | |
| } | |
| for (size_t i = 0; i < station_data.size(); ++i) { | |
| m_stations.emplace_back(i, station_data[i].first, station_data[i].second); | |
| } | |
| m_station_switch_duration = SMALL_MODE_TOTAL_TIME_SECONDS / static_cast<int>(m_stations.size()); | |
| load_history_from_disk(); | |
| for (size_t i = 0; i < m_stations.size(); ++i) { | |
| double initial_volume = (i == m_active_station_idx) ? 100.0 : 0.0; | |
| m_stations[i].initialize(initial_volume); | |
| } | |
| } | |
| ~RadioPlayer() { | |
| m_quit_flag = true; | |
| if (m_mpv_event_thread.joinable()) { | |
| m_mpv_event_thread.join(); | |
| } | |
| save_history_to_disk(); | |
| } | |
| void run() { | |
| m_mpv_event_thread = std::thread(&RadioPlayer::mpv_event_loop, this); | |
| bool needs_redraw = true; | |
| while (!m_quit_flag) { | |
| // Phase 1: Process user input | |
| int ch = m_ui->getInput(); | |
| if (ch != ERR) { | |
| handle_input(ch); | |
| needs_redraw = true; | |
| } | |
| // Phase 2: Update time-based and other automatic states | |
| if (update_state()) { | |
| needs_redraw = true; | |
| } | |
| // Phase 3: Render UI if anything changed | |
| if (needs_redraw) { | |
| int remaining_seconds = m_small_mode_active ? get_remaining_seconds_for_current_station() : 0; | |
| m_ui->draw(m_stations, m_active_station_idx, m_small_mode_active, remaining_seconds); | |
| needs_redraw = false; | |
| } | |
| std::this_thread::sleep_for(std::chrono::milliseconds(20)); | |
| } | |
| } | |
| private: | |
| bool update_state() { | |
| // Handle small mode auto-switching | |
| if (m_small_mode_active && should_switch_station()) { | |
| int next_station = (m_active_station_idx + 1) % static_cast<int>(m_stations.size()); | |
| switch_station(next_station); | |
| m_small_mode_start_time = std::chrono::steady_clock::now(); | |
| return true; // State changed | |
| } | |
| // Check if any station is currently fading its audio | |
| for (const auto& station : m_stations) { | |
| if (station.isFading()) { | |
| return true; // Fading requires continuous redraw | |
| } | |
| } | |
| // In small mode, the countdown timer always needs to be updated on screen | |
| if (m_small_mode_active) { | |
| return true; // Small mode requires continuous redraw | |
| } | |
| return false; // No state change that needs a redraw | |
| } | |
| std::vector<RadioStream> m_stations; | |
| std::unique_ptr<UIManager> m_ui; | |
| int m_active_station_idx; | |
| std::atomic<bool> m_quit_flag; | |
| std::thread m_mpv_event_thread; | |
| std::atomic<bool> m_small_mode_active; | |
| std::chrono::steady_clock::time_point m_small_mode_start_time; | |
| int m_station_switch_duration; | |
| nlohmann::json m_song_history; | |
| std::mutex m_history_mutex; | |
| void on_key_up() { | |
| if (!m_small_mode_active && m_active_station_idx > 0) { | |
| switch_station(m_active_station_idx - 1); | |
| } | |
| } | |
| void on_key_down() { | |
| if (!m_small_mode_active && m_active_station_idx < static_cast<int>(m_stations.size()) - 1) { | |
| switch_station(m_active_station_idx + 1); | |
| } | |
| } | |
| void on_key_enter() { | |
| if (!m_small_mode_active) { | |
| toggle_mute_station(m_active_station_idx); | |
| } | |
| } | |
| void handle_input(int ch) { | |
| switch (ch) { | |
| case KEY_UP: | |
| on_key_up(); | |
| break; | |
| case KEY_DOWN: | |
| on_key_down(); | |
| break; | |
| case '\n': case '\r': case KEY_ENTER: | |
| on_key_enter(); | |
| break; | |
| case 's': case 'S': | |
| toggle_small_mode(); | |
| break; | |
| case 'q': case 'Q': | |
| m_quit_flag = true; | |
| break; | |
| } | |
| } | |
| void toggle_small_mode() { | |
| if (m_small_mode_active) { | |
| m_small_mode_active = false; | |
| } else { | |
| m_small_mode_active = true; | |
| m_small_mode_start_time = std::chrono::steady_clock::now(); | |
| RadioStream& current_station = m_stations[m_active_station_idx]; | |
| if (current_station.isMuted()) { | |
| current_station.setMuted(false); | |
| fade_audio(current_station, current_station.getCurrentVolume(), current_station.getPreMuteVolume(), FADE_TIME_MS / 2); | |
| } else if (current_station.getCurrentVolume() < 50.0) { | |
| fade_audio(current_station, current_station.getCurrentVolume(), 100.0, FADE_TIME_MS); | |
| } | |
| } | |
| } | |
| bool should_switch_station() { | |
| auto now = std::chrono::steady_clock::now(); | |
| auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - m_small_mode_start_time); | |
| return elapsed.count() >= m_station_switch_duration; | |
| } | |
| int get_remaining_seconds_for_current_station() { | |
| auto now = std::chrono::steady_clock::now(); | |
| auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - m_small_mode_start_time); | |
| return std::max(0, m_station_switch_duration - static_cast<int>(elapsed.count())); | |
| } | |
| void switch_station(int new_idx) { | |
| if (new_idx == m_active_station_idx) return; | |
| RadioStream& current_station = m_stations[m_active_station_idx]; | |
| if (!current_station.isMuted()) { | |
| fade_audio(current_station, current_station.getCurrentVolume(), 0.0, FADE_TIME_MS); | |
| } | |
| RadioStream& new_station = m_stations[new_idx]; | |
| if (!new_station.isMuted()) { | |
| fade_audio(new_station, new_station.getCurrentVolume(), 100.0, FADE_TIME_MS); | |
| } | |
| m_active_station_idx = new_idx; | |
| } | |
| void toggle_mute_station(int station_idx) { | |
| RadioStream& station = m_stations[station_idx]; | |
| if (station.isMuted()) { | |
| station.setMuted(false); | |
| fade_audio(station, station.getCurrentVolume(), station.getPreMuteVolume(), FADE_TIME_MS / 2); | |
| } else { | |
| station.setPreMuteVolume(station.getCurrentVolume()); | |
| station.setMuted(true); | |
| fade_audio(station, station.getCurrentVolume(), 0.0, FADE_TIME_MS / 2); | |
| } | |
| } | |
| void fade_audio(RadioStream& station, double from_vol, double to_vol, int duration_ms) { | |
| station.setFading(true); | |
| station.setTargetVolume(to_vol); | |
| std::thread([this, &station, from_vol, to_vol, duration_ms]() { | |
| const int steps = 50; | |
| const int step_delay = duration_ms / steps; | |
| const double vol_step = (to_vol - from_vol) / steps; | |
| double current_vol = from_vol; | |
| for (int i = 0; i < steps && !m_quit_flag; ++i) { | |
| current_vol += vol_step; | |
| station.setCurrentVolume(current_vol); | |
| double clamped_vol = std::max(0.0, std::min(100.0, current_vol)); | |
| mpv_set_property_async(station.getMpvHandle(), 0, "volume", MPV_FORMAT_DOUBLE, &clamped_vol); | |
| std::this_thread::sleep_for(std::chrono::milliseconds(step_delay)); | |
| } | |
| if (!m_quit_flag) { | |
| station.setCurrentVolume(to_vol); | |
| double final_vol = std::max(0.0, std::min(100.0, to_vol)); | |
| mpv_set_property_async(station.getMpvHandle(), 0, "volume", MPV_FORMAT_DOUBLE, &final_vol); | |
| } | |
| station.setFading(false); | |
| }).detach(); | |
| } | |
| void mpv_event_loop() { | |
| while (!m_quit_flag) { | |
| bool event_found = false; | |
| for (auto& station : m_stations) { | |
| if (!station.getMpvHandle()) continue; | |
| mpv_event* event = mpv_wait_event(station.getMpvHandle(), 0.001); | |
| if (event->event_id != MPV_EVENT_NONE) { | |
| handle_mpv_event(event); | |
| event_found = true; | |
| } | |
| } | |
| if (!event_found) { | |
| std::this_thread::sleep_for(std::chrono::milliseconds(50)); | |
| } | |
| } | |
| } | |
| RadioStream* find_station_by_id(int station_id) { | |
| auto it = std::find_if(m_stations.begin(), m_stations.end(), | |
| [station_id](const RadioStream& s) { return s.getID() == station_id; }); | |
| return (it != m_stations.end()) ? &(*it) : nullptr; | |
| } | |
| void on_title_changed(RadioStream& station, const std::string& new_title) { | |
| if (new_title == station.getCurrentTitle() || new_title == "N/A" || new_title == "Initializing...") { | |
| if (new_title != station.getCurrentTitle()) { | |
| station.setCurrentTitle(new_title); | |
| } | |
| return; | |
| } | |
| auto now = std::chrono::system_clock::now(); | |
| std::time_t now_c = std::chrono::system_clock::to_time_t(now); | |
| char time_buf[100]; | |
| std::strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", std::localtime(&now_c)); | |
| nlohmann::json song_entry = {std::string(time_buf), new_title}; | |
| { | |
| std::lock_guard<std::mutex> lock(m_history_mutex); | |
| m_song_history[station.getName()].push_back(song_entry); | |
| } | |
| save_history_to_disk(); | |
| station.setCurrentTitle(new_title); | |
| } | |
| void on_stream_eof(RadioStream& station) { | |
| station.setCurrentTitle("Stream Error - Reconnecting..."); | |
| const char* cmd[] = {"loadfile", station.getURL().c_str(), "replace", nullptr}; | |
| mpv_command_async(station.getMpvHandle(), 0, cmd); | |
| } | |
| void handle_mpv_event(mpv_event* event) { | |
| if (event->event_id != MPV_EVENT_PROPERTY_CHANGE) { | |
| return; | |
| } | |
| mpv_event_property* prop = (mpv_event_property*)event->data; | |
| RadioStream* station = find_station_by_id(event->reply_userdata); | |
| if (!station) { | |
| return; | |
| } | |
| if (strcmp(prop->name, "media-title") == 0 && prop->format == MPV_FORMAT_STRING) { | |
| char* title_cstr = *(char**)prop->data; | |
| on_title_changed(*station, title_cstr ? title_cstr : "N/A"); | |
| } else if (strcmp(prop->name, "eof-reached") == 0 && prop->format == MPV_FORMAT_FLAG) { | |
| if (*(int*)prop->data) { | |
| on_stream_eof(*station); | |
| } | |
| } | |
| } | |
| void load_history_from_disk() { | |
| std::ifstream i("radio_history.json"); | |
| if (i.is_open()) { | |
| try { | |
| i >> m_song_history; | |
| if (!m_song_history.is_object()) { | |
| m_song_history = nlohmann::json::object(); | |
| } | |
| } catch (...) { | |
| m_song_history = nlohmann::json::object(); | |
| } | |
| } | |
| for (const auto& station : m_stations) { | |
| if (!m_song_history.contains(station.getName())) { | |
| m_song_history[station.getName()] = nlohmann::json::array(); | |
| } | |
| } | |
| } | |
| void save_history_to_disk() { | |
| std::lock_guard<std::mutex> lock(m_history_mutex); | |
| std::ofstream o("radio_history.json"); | |
| if (o.is_open()) { | |
| o << std::setw(4) << m_song_history << std::endl; | |
| } | |
| } | |
| }; | |
| //================================================================================ | |
| // 4. Main Function | |
| //================================================================================ | |
| int main() { | |
| std::vector<std::pair<std::string, std::string>> station_data = { | |
| //{"ILoveRadio", "https://ilm.stream18.radiohost.de/ilm_iloveradio_mp3-192"}, | |
| {"ILove2Dance", "https://ilm.stream18.radiohost.de/ilm_ilove2dance_mp3-192"}, | |
| {"RM Deutschrap", "https://rautemusik.stream43.radiohost.de/rm-deutschrap-charts_mp3-192"}, | |
| {"RM Charthits", "https://rautemusik.stream43.radiohost.de/charthits"}, | |
| {"bigFM OldSchool Deutschrap", "https://stream.bigfm.de/oldschooldeutschrap/aac-128"}, | |
| {"bigFM Dance", "https://audiotainment-sw.streamabc.net/atsw-dance-aac-128-2321625"}, | |
| //{"bigFM DanceHall", "https://audiotainment-sw.streamabc.net/atsw-dancehall-aac-128-5420319"}, | |
| //{"bigFM GrooveNight", "https://audiotainment-sw.streamabc.net/atsw-groovenight-aac-128-5346495"}, | |
| {"BreakZ", "https://rautemusik.stream43.radiohost.de/breakz"}, | |
| //{"1.fm DeepHouse", "http://strm112.1.fm/deephouse_mobile_mp3"}, | |
| {"1.fm Dance", "https://strm112.1.fm/dance_mobile_mp3"}, | |
| {"104.6rtl Dance Hits", "https://stream.104.6rtl.com/dance-hits/mp3-128/konsole"}, | |
| //{"Absolut.de Hot", "https://edge22.live-sm.absolutradio.de/absolut-hot"}, | |
| {"Sunshine Live - EDM", "http://stream.sunshine-live.de/edm/mp3-192/stream.sunshine-live.de/"}, | |
| {"Sunshine Live - Amsterdam Club", "https://stream.sunshine-live.de/ade18club/mp3-128"}, | |
| {"Sunshine Live - Charts", "http://stream.sunshine-live.de/sp6/mp3-128"}, | |
| //{"Sunshine Live - EuroDance", "https://sunsl.streamabc.net/sunsl-eurodance-mp3-192-9832422"}, | |
| //{"Sunshine Live - Summer Beats", "http://stream.sunshine-live.de/sp2/aac-64"}, | |
| {"Kiss FM - German Beats", "https://topradio-stream31.radiohost.de/kissfm-deutschrap-hits_mp3-128"}, | |
| //{"Kiss FM - DJ Sets", "https://topradio.stream41.radiohost.de/kissfm-electro_mp3-192"}, | |
| {"Kiss FM - Remix", "https://topradio.stream05.radiohost.de/kissfm-remix_mp3-192"}, | |
| //{"Kiss FM - Events", "https://topradio.stream10.radiohost.de/kissfm-event_mp3-192"}, | |
| {"Energy - Dance", "https://edge01.streamonkey.net/energy-dance/stream/mp3"}, | |
| //{"Energy - MasterMix", "https://scdn.nrjaudio.fm/adwz1/de/33027/mp3_128.mp3"}, | |
| {"Energy - Deutschrap", "https://edge07.streamonkey.net/energy-deutschrap"}, | |
| {"PulseEDM Dance Radio", "http://pulseedm.cdnstream1.com:8124/1373_128.m3u"}, | |
| {"Puls Radio Dance", "https://sc4.gergosnet.com/pulsHD.mp3"}, | |
| {"Puls Radio Club", "https://str3.openstream.co/2138"}, | |
| {"Los 40 Dance", "https://playerservices.streamtheworld.com/api/livestream-redirect/LOS40_DANCEAAC.aac"}, | |
| {"RadCap Uplifting", "http://79.111.119.111:8000/upliftingtrance"}, | |
| {"Regenbogen", "https://streams.regenbogen.de/"}, | |
| {"RadCap ClubDance", "http://79.111.119.111:8000/clubdance"}, | |
| }; | |
| try { | |
| RadioPlayer player(station_data); | |
| player.run(); | |
| } catch (const std::exception& e) { | |
| if (stdscr != NULL && !isendwin()) { | |
| endwin(); | |
| } | |
| std::cerr << "Critical Error: " << e.what() << std::endl; | |
| return 1; | |
| } | |
| std::cout << "Radio player exited gracefully." << std::endl; | |
| return 0; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment