From 4dc6771a626d8a34c35d0bb52a538c47c74610f7 Mon Sep 17 00:00:00 2001 From: user Date: Tue, 2 Jun 2026 02:03:16 +0200 Subject: [PATCH 1/3] Update and stabilization --- .gitignore | 3 + source/HttpUtils.cpp | 298 +++++++++++++++---- source/HttpUtils.h | 18 +- source/MainWindow.cpp | 96 +++---- source/Makefile | 39 ++- source/PackageInfo | 38 +++ source/RadioApp.cpp | 7 - source/Station.cpp | 17 +- source/StationFinder.cpp | 148 +++------- source/StationFinder.h | 135 +++++---- source/StationFinderListenLive.cpp | 415 --------------------------- source/StationFinderListenLive.h | 55 ---- source/StationFinderRadioNetwork.cpp | 187 ++++++------ source/StationFinderRadioNetwork.h | 15 +- source/StreamIO.cpp | 17 +- source/StreamPlayer.cpp | 7 +- source/Utils.cpp | 28 -- source/Utils.h | 4 - 18 files changed, 606 insertions(+), 921 deletions(-) create mode 100644 source/PackageInfo delete mode 100644 source/StationFinderListenLive.cpp delete mode 100644 source/StationFinderListenLive.h diff --git a/.gitignore b/.gitignore index c173ce7..6dee055 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ dist/* source/objects*/ +nbproject +.genio.yaml +.clang-format \ No newline at end of file diff --git a/source/HttpUtils.cpp b/source/HttpUtils.cpp index e8229ab..62b119f 100644 --- a/source/HttpUtils.cpp +++ b/source/HttpUtils.cpp @@ -19,6 +19,10 @@ #include #include +#include +#include +#include +#include #include "Debug.h" #include "HttpUtils.h" @@ -58,53 +62,8 @@ class DataLimit : public BUrlProtocolListener, public BDataIO { }; -/** - * Loops on the DNS responses looking for connectivity on specified port. - */ -status_t -HttpUtils::CheckPort(BUrl url, BUrl* newUrl, uint32 flags) -{ - uint16 port; - if (url.HasPort()) - port = url.Port(); - else - port = 80; - - BReference resolver - = BNetworkAddressResolver::Resolve(url.Host(), port, flags); - if (resolver.Get() == NULL) - return B_NO_MEMORY; - status_t status = resolver->InitCheck(); - if (status != B_OK) - return status; - - BString newUrlString; - BNetworkAddress ipAddress; - BSocket* socket; - uint32 cookie = 0; - - status_t portStatus = B_ERROR; - while (portStatus != B_OK) { - status = resolver->GetNextAddress(AF_INET6, &cookie, ipAddress); - if (status != B_OK) - status = resolver->GetNextAddress(&cookie, ipAddress); - if (status != B_OK) - return status; - socket = new (std::nothrow) BSocket(); - portStatus = socket->Connect(ipAddress); - delete socket; - } - - // If port number is 80, do not add it to the final URL - // Then, prepend the appropiate protocol - newUrlString - = ipAddress.ToString(ipAddress.Port() != 80).Prepend("://").Prepend(url.Protocol()); - if (url.HasPath()) - newUrlString.Append(url.Path()); - newUrl->SetUrlString(newUrlString.String()); - - return B_OK; -} +BString +HttpUtils::jsonType("application/json"); /** @@ -115,7 +74,7 @@ HttpUtils::CheckPort(BUrl url, BUrl* newUrl, uint32 flags) * @return BMallocIO* filled with retrieved content */ BMallocIO* -HttpUtils::GetAll(BUrl url, BHttpHeaders* responseHeaders, bigtime_t timeOut, BString* contentType, +HttpUtils::GetAll(BUrl* url, BHttpHeaders* responseHeaders, bigtime_t timeOut, BString* contentType, size_t sizeLimit) { BMallocIO* data = new BMallocIO(); @@ -126,37 +85,42 @@ HttpUtils::GetAll(BUrl url, BHttpHeaders* responseHeaders, bigtime_t timeOut, BS BHttpRequest* request; if (sizeLimit) request = dynamic_cast( - BUrlProtocolRoster::MakeRequest(url.UrlString().String(), &reader, &reader, NULL)); + BUrlProtocolRoster::MakeRequest(url->UrlString().String(), &reader, &reader, NULL)); else request = dynamic_cast( - BUrlProtocolRoster::MakeRequest(url.UrlString().String(), data, NULL, NULL)); + BUrlProtocolRoster::MakeRequest(url->UrlString().String(), data, NULL, NULL)); - if (request == NULL) + if (request == NULL) { + delete data; return NULL; + } if (contentType && !contentType->IsEmpty()) { - BHttpHeaders* requestHeaders = new BHttpHeaders(); - requestHeaders->AddHeader("accept", contentType->String()); - request->AdoptHeaders(requestHeaders); + BHttpHeaders requestHeaders; + requestHeaders.AddHeader("accept", contentType->String()); + request->SetHeaders(requestHeaders); } request->SetAutoReferrer(true); request->SetFollowLocation(true); request->SetTimeout(timeOut); - request->SetUserAgent(Utils::UserAgent()); + request->SetUserAgent(UserAgent()); thread_id threadId = request->Run(); status_t status; wait_for_thread(threadId, &status); - BHttpResult& result = (BHttpResult&)request->Result(); - int32 statusCode = result.StatusCode(); - size_t bufferLen = data->BufferLength(); + const BHttpResult& result = dynamic_cast(request->Result()); + int32 statusCode = result.StatusCode(); + size_t bufferLen = data->BufferLength(); if (!(statusCode == 0 || request->IsSuccessStatusCode(statusCode)) || bufferLen == 0) { delete data; data = NULL; - } else if (contentType != NULL) - contentType->SetTo(result.ContentType()); + } else { + data->Seek(0, SEEK_SET); + if (contentType != NULL) + contentType->SetTo(result.ContentType()); + } if (responseHeaders != NULL) *responseHeaders = result.Headers(); @@ -164,3 +128,217 @@ HttpUtils::GetAll(BUrl url, BHttpHeaders* responseHeaders, bigtime_t timeOut, BS delete request; return data; } + + +/** + * Helper class for scanning simple JSONs + */ +class JsonPathExtractor : public BJsonEventListener { +public: + + /** + * Listens to JSON parsing events and extracts specific path + * @param path Dot-separated location of element in JSON hierarchy + * [].{}.address.{}.street will get street within an + * address object within the unnamed objects of the array + * @param result Initialized BStringList to capture all occurrences + */ + JsonPathExtractor(BString* path, BStringList* result) : + BJsonEventListener(), + pathElements(true) + { + path->Split(".", true, pathElements); + this->result = result; + } + + ~JsonPathExtractor() { + pathElements.MakeEmpty(); + } + + virtual bool Handle(const BJsonEvent& event) { + switch (event.EventType()) { + case B_JSON_NUMBER : + case B_JSON_TRUE : + case B_JSON_FALSE : + case B_JSON_NULL : + if (awaitValue) + awaitValue = false; + if (offIndex > 0) + offIndex--; + else + index--; + break; + + case B_JSON_STRING : + if (awaitValue) { + result->Add(event.Content()); + awaitValue = false; + } + if (offIndex > 0) + offIndex--; + else + index--; + break; + + case B_JSON_OBJECT_START : + if (index < pathElements.CountStrings() && + pathElements.StringAt(index) == "{}" && + offIndex == 0) + index++; + else + offIndex++; + break; + + case B_JSON_OBJECT_END : + if (offIndex > 0) + offIndex--; + else + index--; + break; + + case B_JSON_OBJECT_NAME : + if (index < pathElements.CountStrings() && + pathElements.StringAt(index) == event.Content() && + offIndex == 0) + { + index++; + if (index == pathElements.CountStrings()) + awaitValue = true; + } + else + offIndex++; + break; + + case B_JSON_ARRAY_START : + if (index < pathElements.CountStrings() && + pathElements.StringAt(index) == "[]" && + offIndex == 0) + index++; + else + offIndex++; + break; + + case B_JSON_ARRAY_END : + if (offIndex > 0) + offIndex--; + else { + index--; + } + break; + } + return true; + } + + virtual void HandleError(status_t status, int32 line, const char* message) { + printf("JsonParser line %d: %s", line, message); + } + virtual void Complete() { } + +private: + int index = 0; + int offIndex = 0; + bool awaitValue = false; + BString name; + BStringList* result = NULL; + + // path like "[].{}.name" extracts all elements called "name" in root array of unnamed objects + BStringList pathElements; +}; + +/** + * Helper to make retrieve list of occurrences of specific element in JSON + * @param url REST-Url to request + * @param path Dot-separated location of element in JSON hierarchy + * [].{}.address.{}.street will get street within an + * address object within the unnamed objects of the array + * @param contentType In/Out: Received headers + * @param timeout Timeout of request in ms, default set to 3 seconds + * @return BStringList* filled with all occurrences of element described by path + */ +BMessage* +HttpUtils::GetMsgFromREST(BUrl* url, BHttpHeaders* responseHeaders, bigtime_t timeOut) { + BMallocIO* json = GetAll(url, responseHeaders, timeOut, &jsonType, 0); + if (json == NULL) + return NULL; + + BMessage* data = new BMessage(); + + status_t status = BJson::Parse((const char*)json->Buffer(), json->BufferLength(), *data); + delete json; + + if (status != B_OK) { + delete data; + data = NULL; + } + + return data; +} + +/** + * Helper to make retrieve list of occurrences of specific element in JSON + * @param url REST-Url to request + * @param path Dot-separated location of element in JSON hierarchy + * [].{}.address.{}.street will get street within an + * address object within the unnamed objects of the root array + * @param responseHeaders Unless NULL, populated with HTTP headers of response + * @param contentType In/Out: Received headers + * @param timeout Timeout of request in ms, default set to 3 seconds + * @return BStringList* filled with all occurrences of element described by path + */ +BStringList* +HttpUtils::GetStringsFromREST(BUrl* url, BString* path, BHttpHeaders* responseHeaders, + bigtime_t timeout) { + BMallocIO* json = GetAll(url, responseHeaders, timeout, &jsonType, 0); + if (json == NULL) + return NULL; + + if (json->BufferLength() == 0) { + delete json; + return NULL; + } + + BStringList* result = new BStringList(); + JsonPathExtractor extractor(path, result); + BJson::Parse(json, &extractor); + delete json; + + if (result->IsEmpty()) { + delete result; + result = NULL; + } + + return result; +} + +char HttpUtils::sUserAgent[50] = "\0"; + +const char* +HttpUtils::UserAgent() +{ + if (sUserAgent[0] != 0) + return sUserAgent; + + app_info appInfo; + be_app->GetAppInfo(&appInfo); + BFile file(&appInfo.ref, B_READ_ONLY); + if (file.InitCheck() == B_OK) { + BAppFileInfo appFileInfo(&file); + struct version_info version; + if (appFileInfo.GetVersionInfo(&version, B_APP_VERSION_KIND) == B_OK) + snprintf(sUserAgent, sizeof(sUserAgent), + "StreamRadio/%" B_PRIu32 ".%" B_PRIu32 ".%" B_PRIu32, version.major, version.middle, + version.minor); + } + + if (sUserAgent[0] == 0) { + // There was an error trying to get the version info + sprintf(sUserAgent, "StreamRadio"); + } + + return sUserAgent; +} + + + + + diff --git a/source/HttpUtils.h b/source/HttpUtils.h index e1df8f3..b5eb7d6 100644 --- a/source/HttpUtils.h +++ b/source/HttpUtils.h @@ -23,16 +23,26 @@ #include #include #include +#include using namespace BPrivate::Network; +#define HTTP_DEFAULT_TIMEOUT 3000 class HttpUtils { public: - static status_t CheckPort(BUrl url, BUrl* newUrl, uint32 flags = 0); - - static BMallocIO* GetAll(BUrl url, BHttpHeaders* returnHeaders = NULL, bigtime_t timeOut = 3000, - BString* contentType = NULL, size_t sizeLimit = 0); + static BString jsonType; + static BMallocIO* GetAll(BUrl* url, BHttpHeaders* responseHeaders = NULL, + bigtime_t timeOut = HTTP_DEFAULT_TIMEOUT, + BString* contentType = NULL, size_t sizeLimit = 0); + static BMessage* GetMsgFromREST(BUrl* url, BHttpHeaders* responseHeaders = NULL, + bigtime_t timeOut = HTTP_DEFAULT_TIMEOUT); + static BStringList* GetStringsFromREST(BUrl* url, BString* path, + BHttpHeaders* responseHeaders = NULL, + bigtime_t timeout = HTTP_DEFAULT_TIMEOUT); + static const char* UserAgent(); +private: + static char sUserAgent[50]; }; diff --git a/source/MainWindow.cpp b/source/MainWindow.cpp index e1a1bc8..1a402a9 100644 --- a/source/MainWindow.cpp +++ b/source/MainWindow.cpp @@ -1,23 +1,23 @@ /* - Copyright (C) 2008-2010 Lukas Sommer < SommerLuk at gmail dot com > - Copyright (C) 2017 Kai Niessen - Copyright (C) 2020 Jacob Secunda - - This program is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License as - published by the Free Software Foundation; either version 2 of - the License or (at your option) version 3 or any later version - accepted by the membership of KDE e.V. (or its successor approved - by the membership of KDE e.V.), which shall act as a proxy - defined in Section 14 of version 3 of the license. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . + Copyright (C) 2008-2010 Lukas Sommer < SommerLuk at gmail dot com > + Copyright (C) 2017 Kai Niessen + Copyright (C) 2020 Jacob Secunda + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ @@ -36,7 +36,7 @@ #include #include -#include "Debug.h" + #include "RadioApp.h" @@ -45,35 +45,34 @@ MainWindow::MainWindow() - : BWindow(BRect(0, 0, 400, 200), B_TRANSLATE_SYSTEM_NAME("StreamRadio"), B_DOCUMENT_WINDOW, - B_AUTO_UPDATE_SIZE_LIMITS), + : BWindow(BRect(0, 0, 400, 200), B_TRANSLATE_SYSTEM_NAME("StreamRadio"), B_DOCUMENT_WINDOW, 0), fStationFinder(NULL) { fSettings = &((RadioApp*)be_app)->Settings; fAllowParallelPlayback = fSettings->GetAllowParallelPlayback(); fMenuParallelPlayback = new BMenuItem( - B_TRANSLATE("Allow parallel playback"), new BMessage(MSG_PARALLEL_PLAYBACK)); + B_TRANSLATE("Allow parallel playback"), new BMessage(MSG_PARALLEL_PLAYBACK)); fMenuParallelPlayback->SetMarked(fAllowParallelPlayback); fMainMenu = new BMenuBar(Bounds(), "MainMenu"); BLayoutBuilder::Menu<>(fMainMenu) .AddMenu(B_TRANSLATE("App")) - .AddItem(fMenuParallelPlayback) - .AddItem(B_TRANSLATE("Help" B_UTF8_ELLIPSIS), MSG_HELP) - .AddItem(B_TRANSLATE("About"), B_ABOUT_REQUESTED) - .AddSeparator() - .AddItem(B_TRANSLATE("Quit"), B_QUIT_REQUESTED, 'Q') + .AddItem(fMenuParallelPlayback) + .AddItem(B_TRANSLATE("Help" B_UTF8_ELLIPSIS), MSG_HELP) + .AddItem(B_TRANSLATE("About"), B_ABOUT_REQUESTED) + .AddSeparator() + .AddItem(B_TRANSLATE("Quit"), B_QUIT_REQUESTED, 'Q') .End() .AddMenu(B_TRANSLATE("Station")) - .AddItem(B_TRANSLATE("Paste Shoutcast URL"), MSG_PASTE_URL, 'V') - .AddItem(B_TRANSLATE("Check station"), MSG_CHECK) - .AddItem(B_TRANSLATE("Remove station"), MSG_REMOVE, 'R') + .AddItem(B_TRANSLATE("Paste Shoutcast URL"), MSG_PASTE_URL, 'V') + .AddItem(B_TRANSLATE("Check station"), MSG_CHECK) + .AddItem(B_TRANSLATE("Remove station"), MSG_REMOVE, 'R') .End() .AddMenu(B_TRANSLATE("Search")) - .AddItem(B_TRANSLATE("Find stations" B_UTF8_ELLIPSIS), MSG_SEARCH, 'S') + .AddItem(B_TRANSLATE("Find stations" B_UTF8_ELLIPSIS), MSG_SEARCH, 'S') .End() - .End(); + .End(); fStationList = new StationListView(true); BScrollView* stationScroll = new BScrollView("scrollStation", fStationList, 0, false, true); @@ -84,16 +83,16 @@ MainWindow::MainWindow() BLayoutBuilder::Group<>(this, B_VERTICAL) .Add(fMainMenu) - .AddGroup(B_VERTICAL) - .SetInsets(B_USE_WINDOW_INSETS, 0, B_USE_WINDOW_INSETS, B_USE_WINDOW_INSETS) - .AddSplit(B_VERTICAL, 2) - .Add(stationScroll, 1.0f) - .Add(fStationPanel) - .SetCollapsible(1, true) - .End() - .Add(fStatusBar) + .AddGroup(B_VERTICAL) + .SetInsets(B_USE_WINDOW_INSETS, 0, B_USE_WINDOW_INSETS, B_USE_WINDOW_INSETS) + .AddSplit(B_VERTICAL, 2) + .Add(stationScroll, 1.0f) + .Add(fStationPanel) + .SetCollapsible(1, true) + .End() + .Add(fStatusBar) .End() - .End(); + .End(); fStationList->Sync(fSettings->Stations); fStationList->SetInvocationMessage(new BMessage(MSG_INVOKE_STATION)); @@ -145,7 +144,7 @@ MainWindow::MessageReceived(BMessage* message) B_TRANSLATE("Added station %s to list"), station->Name()->String()); } } else - result.SetToFormat(B_TRANSLATE("File %s could not be loaded as a station")); + result.SetToFormat(B_TRANSLATE("File could not be loaded as a station")); fStatusBar->SetText(result.String()); } @@ -153,13 +152,11 @@ MainWindow::MessageReceived(BMessage* message) } case MSG_SEARCH: - { if (fStationFinder == NULL) fStationFinder = new StationFinderWindow(this); fStationFinder->Show(); break; - } case MSG_PASTE_URL: { @@ -212,10 +209,8 @@ MainWindow::MessageReceived(BMessage* message) fStationList->Invalidate(); fStationPanel->SetStation(stationItem); } - break; } - case MSG_REMOVE: { Station* station = fStationList->StationAt(fStationList->CurrentSelection(0)); @@ -322,16 +317,16 @@ MainWindow::MessageReceived(BMessage* message) case MSG_HELP: { - BUrl userguide = BUrl( + BUrl userguide( "https://github.com/HaikuArchives/" - "StreamRadio/blob/master/docs/userguide.md"); + "StreamRadio/blob/master/docs/userguide.md" + ); userguide.OpenWithPreferredApplication(true); break; } case MSG_PARALLEL_PLAYBACK: - { fAllowParallelPlayback = !fAllowParallelPlayback; fSettings->SetAllowParallelPlayback(fAllowParallelPlayback); fMenuParallelPlayback->SetMarked(fAllowParallelPlayback); @@ -341,7 +336,6 @@ MainWindow::MessageReceived(BMessage* message) _TogglePlay(fActiveStations.LastItem()); } break; - } case B_ABOUT_REQUESTED: be_app->AboutRequested(); diff --git a/source/Makefile b/source/Makefile index ac2b490..8ca8df8 100644 --- a/source/Makefile +++ b/source/Makefile @@ -8,7 +8,7 @@ ## file:///system/develop/documentation/makefile-engine.html # The name of the binary. -NAME = ../../dist/StreamRadio +NAME = StreamRadio # The type of binary, must be one of: # APP: Application @@ -17,6 +17,10 @@ NAME = ../../dist/StreamRadio # DRIVER: Kernel driver TYPE = APP +ARCHITECTURES="all !x86_gcc2" +SECONDARY_ARCHITECTURES="x86" +ARCH=$(shell uname -m) + # If you plan to use localization, specify the application's MIME signature. APP_MIME_SIG = application/x-vnd.Fishpond-StreamRadio @@ -39,15 +43,12 @@ SRCS = \ RadioSettings.cpp \ Station.cpp \ StationFinder.cpp \ - StationFinderListenLive.cpp \ StationFinderRadioNetwork.cpp \ StationListView.cpp \ StationPanel.cpp \ StreamIO.cpp \ StreamPlayer.cpp \ - Utils.cpp \ - - + Utils.cpp # Specify the resource definition files to use. Full or relative paths can be # used. @@ -124,7 +125,7 @@ SYMBOLS := # Includes debug information, which allows the binary to be debugged easily. # If set to "TRUE", debug info will be created. -DEBUGGER := +DEBUGGER := TRUE # Specify any additional compiler flags to be used. COMPILER_FLAGS = @@ -135,15 +136,23 @@ LINKER_FLAGS = # Specify the version of this binary. Example: # -app 3 4 0 d 0 -short 340 -long "340 "`echo -n -e '\302\251'`"1999 GNU GPL" # This may also be specified in a resource. -APP_VERSION := - -# (Only used when "TYPE" is "DRIVER"). Specify the desired driver install -# location in the /dev hierarchy. Example: -# DRIVER_PATH = video/usb -# will instruct the "driverinstall" rule to place a symlink to your driver's -# binary in ~/add-ons/kernel/drivers/dev/video/usb, so that your driver will -# appear at /dev/video/usb when loaded. The default is "misc". -DRIVER_PATH = +APP_VERSION := 1.0.1-1 + + +TARGET_DIR := ../dist +PACKAGE_DIR = $(TARGET_DIR)/package +PACKAGE_NAME = $(TARGET_DIR)/$(NAME)-$(APP_VERSION)-$(ARCH).hpkg + +package:: default + + rm -rf $(PACKAGE_DIR) + mkdir -p $(PACKAGE_DIR)/apps + mkdir -p $(PACKAGE_DIR)/data/deskbar/menu/Applications + cp $(TARGET) $(PACKAGE_DIR)/apps + ln -s ../../../../apps/$(NAME) $(PACKAGE_DIR)/data/deskbar/menu/Applications/$(NAME) + cat ./PackageInfo | sed 's/$$VERSION/$(APP_VERSION)/' | sed 's/$$ARCH/$(ARCH)/' > $(TARGET_DIR)/PackageInfo_$(ARCH) + package create -C $(PACKAGE_DIR) -i $(TARGET_DIR)/PackageInfo_$(ARCH) $(PACKAGE_NAME) + copyattr -n BEOS:ICON $(TARGET) $(PACKAGE_NAME) ## Include the Makefile-Engine DEVEL_DIRECTORY := \ diff --git a/source/PackageInfo b/source/PackageInfo new file mode 100644 index 0000000..fa43265 --- /dev/null +++ b/source/PackageInfo @@ -0,0 +1,38 @@ +name streamradio +version 1.0.0-6 +architecture x86_64 +summary "Find and listen to internet radio stations" +description "StreamRadio helps you find and listen to internet radio streams." +packager "Builder mmlr_x86_64 " +vendor "Haiku Project" +licenses { + "GNU GPL v3" +} +copyrights { + "2017, 2026 Kai Niessen" + "2018-2024 HaikuArchives team" +} +provides { + streamradio = 1.0.0 + app:streamradio = 1.0.0 +} +requires { + haiku >= r1~beta5_hrev57937_111-1 + lib:libstdc++ >= 6.0.32 + lib:libxml2 >= 2.12.5 +} +urls { + "https://github.com/HaikuArchives/StreamRadio" +} +global-writable-files { + "settings/Stations" +} +user-settings-files { + "settings/StreamRadio.settings" +} +source-urls { +# Download + "https://github.com/HaikuArchives/StreamRadio/archive/56a6422bc2a9148fb15b61342a29e9fa0d34efc1.tar.gz" +# Location 2 + "https://ports-mirror.haiku-os.org/streamradio/56a6422bc2a9148fb15b61342a29e9fa0d34efc1.tar.gz" +} diff --git a/source/RadioApp.cpp b/source/RadioApp.cpp index 9951305..11b1e7c 100644 --- a/source/RadioApp.cpp +++ b/source/RadioApp.cpp @@ -21,7 +21,6 @@ #include "RadioApp.h" #include "StationFinder.h" -#include "StationFinderListenLive.h" #include "StationFinderRadioNetwork.h" @@ -127,13 +126,7 @@ main(int argc, char* argv[]) { StationFinderRadioNetwork::RegisterSelf(); - // FIXME: "listenlive.eu" no longer seems to exist, though it looks like - // "radiomap.eu" might be its successor? This plugin crashes after searching - // anyway... - // StationFinderListenLive::RegisterSelf(); - new RadioApp(); be_app->Run(); - delete be_app; } diff --git a/source/Station.cpp b/source/Station.cpp index 4b0eaa3..a5104d0 100644 --- a/source/Station.cpp +++ b/source/Station.cpp @@ -190,7 +190,7 @@ Station::Save() icon->AddChild(canvas); canvas->LockLooper(); - canvas->DrawBitmap(fLogo, fLogo->Bounds(), icon->Bounds()); + canvas->DrawBitmap(fLogo, fLogo->Bounds(), canvas->Bounds(), B_FILTER_BITMAP_BILINEAR); canvas->UnlockLooper(); icon->RemoveChild(canvas); stationInfo.SetIcon(icon, B_LARGE_ICON); @@ -226,7 +226,7 @@ Station::RetrieveStreamUrl() status_t status = B_ERROR; BString contentType("*/*"); - BMallocIO* plsData = HttpUtils::GetAll(fSource, NULL, 100000, &contentType, 2000); + BMallocIO* plsData = HttpUtils::GetAll(&fSource, NULL, 100000, &contentType, 2000); if (plsData != NULL) { status = ParseUrlReference((const char*)plsData->Buffer(), fSource); delete plsData; @@ -272,8 +272,9 @@ Station::Probe() // of streams using HTTPS and load balancing between two or more different // IP's should be small, anyway. - if (fStreamUrl.Protocol() == "https") { - buffer = HttpUtils::GetAll(fStreamUrl, &headers, 2 * 1000 * 1000, &contentType, 4096); + //if (fStreamUrl.Protocol() == "https") { + buffer = HttpUtils::GetAll(&fStreamUrl, &headers, 2 * 1000 * 1000, &contentType, 4096); + /* } else { BUrl resolvedUrl; status_t resolveStatus = HttpUtils::CheckPort(fStreamUrl, &resolvedUrl); @@ -282,7 +283,7 @@ Station::Probe() buffer = HttpUtils::GetAll(resolvedUrl, &headers, 2 * 1000 * 1000, &contentType, 4096); } - + */ #ifdef DEBUGGING for (int32 i = 0; i < headers.CountHeaders(); i++) TRACE("Header: %s\r\n", headers.HeaderAt(i).Header()); @@ -565,7 +566,7 @@ Station::LoadIndirectUrl(BString& shoutCastUrl) BString contentType("*/*"); - BMallocIO* dataIO = HttpUtils::GetAll(url, NULL, 10000, &contentType, 2000); + BMallocIO* dataIO = HttpUtils::GetAll(&url, NULL, 10000, &contentType, 2000); if (dataIO == NULL) return NULL; @@ -615,7 +616,7 @@ Station::LoadIndirectUrl(BString& shoutCastUrl) shoutCastUrl.RemoveCharsSet("#?"); finalUrl.SetUrlString(shoutCastUrl); - dataIO = HttpUtils::GetAll(finalUrl); + dataIO = HttpUtils::GetAll(&finalUrl); if (dataIO != NULL) { dataIO->Write(&"", 1); body = (char*)dataIO->Buffer(); @@ -633,7 +634,7 @@ Station::LoadIndirectUrl(BString& shoutCastUrl) contentType = "image/*"; - BMallocIO* iconIO = HttpUtils::GetAll(finalUrl, NULL, 10000, &contentType, 2000); + BMallocIO* iconIO = HttpUtils::GetAll(&finalUrl, NULL, 10000, &contentType, 2000); if (iconIO != NULL) { iconIO->Seek(0, SEEK_SET); station->fLogo = BTranslationUtils::GetBitmap(iconIO); diff --git a/source/StationFinder.cpp b/source/StationFinder.cpp index 144c577..56271e3 100644 --- a/source/StationFinder.cpp +++ b/source/StationFinder.cpp @@ -7,7 +7,6 @@ #include #include #include -#include "HttpUtils.h" #include "RadioApp.h" #include "Utils.h" @@ -16,103 +15,33 @@ #define B_TRANSLATION_CONTEXT "StationFinder" -std::vector > StationFinderServices::sServices; - - -StationFinderServices::~StationFinderServices() -{ - sServices.clear(); -} - - -void -StationFinderServices::Register(char* serviceName, InstantiateFunc instantiate) -{ - std::pair service(serviceName, instantiate); - sServices.push_back(service); -} - - -int32 -StationFinderServices::CountItems() -{ - return sServices.size(); -} - - -StationFinderService* -StationFinderServices::Instantiate(char* name) +FindByCapability::FindByCapability(const char* name) + : fKeywords(true) { - for (uint32 i = 0; i < sServices.size(); i++) { - std::pair service = sServices[i]; - if (!strcmp(service.first, name)) - return service.second(); - } - - return NULL; + fName.SetTo(name); } -char* -StationFinderServices::Name(int i) +FindByCapability::FindByCapability(const char* name, BStringList* keyWords) + : FindByCapability(name) { - return sServices[i].first; + fKeywords.Add(*keyWords); } - -FindByCapability::FindByCapability(char* name) - : fName(name), - fKeywords() +FindByCapability::FindByCapability(const char* name, char* keyWords, char* delimiter) + : FindByCapability(name) { + BString tmp(keyWords); + tmp.Split(delimiter, true, fKeywords); } - -FindByCapability::FindByCapability(char* name, char* keyWords, char* delimiter) - : fName(name), - fKeywords() -{ - SetKeyWords(keyWords, delimiter); -} - - FindByCapability::~FindByCapability() { fKeywords.MakeEmpty(); } - -bool -FindByCapability::HasKeyWords() -{ - return !fKeywords.IsEmpty(); -} - - -const BStringList* -FindByCapability::KeyWords() -{ - return &fKeywords; -} - - -const char* -FindByCapability::Name() -{ - return fName.String(); -} - - -void -FindByCapability::SetKeyWords(char* keyWords, char* delimiter) -{ - BString tmp(keyWords); - tmp.Split(delimiter, true, fKeywords); -} - - StationFinderService::StationFinderService() - : serviceName("unknown"), - serviceHomePage(""), + : serviceHomePage(""), serviceLogo(NULL), #if B_HAIKU_VERSION > B_HAIKU_VERSION_1_BETA_5 findByCapabilities(5) @@ -131,14 +60,14 @@ StationFinderService::~StationFinderService() void -StationFinderService::Register(char* name, InstantiateFunc instantiate) +StationFinderService::Register(BString* name, InstantiateFunc instantiate) { - StationFinderServices::Register(name, instantiate); + stationFinderServices.Register(name, instantiate); } BBitmap* -StationFinderService::RetrieveLogo(BUrl url) +StationFinderService::RetrieveLogo(BUrl* url) { BBitmap* bm = NULL; BString contentType("image/*"); @@ -146,7 +75,7 @@ StationFinderService::RetrieveLogo(BUrl url) BMallocIO* bmData = HttpUtils::GetAll(url, NULL, 3000, &contentType); if (bmData != NULL) { bm = BTranslationUtils::GetBitmap(bmData); - if (bm != NULL && bm->InitCheck() != B_OK) { + if (bm != NULL && (bm->InitCheck() != B_OK || !bm->IsValid())) { delete bm; bm = NULL; } @@ -158,23 +87,25 @@ StationFinderService::RetrieveLogo(BUrl url) } -FindByCapability* -StationFinderService::RegisterSearchCapability(char* name) +uint32 +StationFinderService::RegisterSearchCapability(const char* name) { FindByCapability* newCapability = new FindByCapability(name); - findByCapabilities.AddItem(newCapability); - - return newCapability; + if (findByCapabilities.AddItem(newCapability)) + return findByCapabilities.IndexOf(newCapability); + else + return -1; } -FindByCapability* -StationFinderService::RegisterSearchCapability(char* name, char* keywords, char* delimiter) +uint32 +StationFinderService::RegisterSearchCapability(const char* name, BStringList* keywords) { - FindByCapability* newCapability = new FindByCapability(name, keywords, delimiter); - findByCapabilities.AddItem(newCapability); - - return newCapability; + FindByCapability* newCapability = new FindByCapability(name, keywords); + if (findByCapabilities.AddItem(newCapability)) + return findByCapabilities.IndexOf(newCapability); + else + return -1; } @@ -197,11 +128,11 @@ StationFinderWindow::StationFinderWindow(BWindow* parent) fDdServices = new BOptionPopUp("fDdServices", NULL, new BMessage(MSG_SELECT_SERVICE)); int currentServiceIndex = 0; const char* settingsServiceName = ((RadioApp*)be_app)->Settings.StationFinderName(); - for (int32 i = 0; i < StationFinderServices::CountItems(); i++) { - const char* serviceName = StationFinderServices::Name(i); - if (settingsServiceName && !strcmp(serviceName, settingsServiceName)) + for (int32 i = 0; i < stationFinderServices.CountItems(); i++) { + BString* serviceName = stationFinderServices.Name(i); + if (settingsServiceName && serviceName->Compare(settingsServiceName)) currentServiceIndex = i; - fDdServices->AddOption(serviceName, i); + fDdServices->AddOption(serviceName->String(), i); } fBnVisit = new BButton("fBnVisit", "", new BMessage(MSG_VISIT_SERVICE)); @@ -382,22 +313,21 @@ StationFinderWindow::MessageReceived(BMessage* msg) void StationFinderWindow::SelectService(int index) { - char* serviceName = StationFinderServices::Name(index); + BString* serviceName = stationFinderServices.Name(index); if (serviceName == NULL) return; - StationFinderService* selectedService = StationFinderServices::Instantiate(serviceName); - if (selectedService == NULL) + if (fCurrentService != NULL && *fCurrentService->Name() == *serviceName) return; - if (fCurrentService != NULL) { - delete fCurrentService; - fCurrentService = NULL; - } + StationFinderService* selectedService = stationFinderServices.Instantiate(index); + if (selectedService == NULL) + return; + delete fCurrentService; fCurrentService = selectedService; - ((RadioApp*)be_app)->Settings.SetStationFinderName(serviceName); + ((RadioApp*)be_app)->Settings.SetStationFinderName(serviceName->String()); while (fDdSearchBy->CountOptions() > 0) fDdSearchBy->RemoveOptionAt(0); diff --git a/source/StationFinder.h b/source/StationFinder.h index 177e341..b7a57f6 100644 --- a/source/StationFinder.h +++ b/source/StationFinder.h @@ -23,7 +23,7 @@ #include #include -#include +#include #include #include #include @@ -58,34 +58,49 @@ typedef BObjectList StationList; typedef class StationFinderService* (*InstantiateFunc)(); - -class StationFinderServices { +class StationFinderEntry { public: - StationFinderServices(){}; - ~StationFinderServices(); - - static void Register(char* serviceName, InstantiateFunc); - - static StationFinderService* Instantiate(char* s); + StationFinderEntry() { }; + StationFinderEntry(BString* name, InstantiateFunc instantiateFunc) + : Name(name->String()) + { + this->instantiate = instantiateFunc; + } + BString Name; + StationFinderService* Get() { return instance != NULL ? instance : instantiate(); } +private: + InstantiateFunc instantiate; + StationFinderService* instance = NULL; +}; - static int32 CountItems(); - static char* Name(int i); +class StationFinderServices : public BObjectList { +public: + StationFinderServices() : BObjectList(32, true) {}; + ~StationFinderServices() { MakeEmpty(); }; + + void Register(BString* serviceName, InstantiateFunc instantiate) { + AddItem(new StationFinderEntry(serviceName, instantiate)); + } + BString* Name(int index) { return &ItemAt(index)->Name; } + StationFinderService* Instantiate(int index) { return ItemAt(index)->Get(); } + int32 IndexOf(BString* name) { return BinarySearchIndexByKey(*name, CompareName); } private: - static std::vector > sServices; + static int CompareName(const BString* name, const StationFinderEntry* entry) { return name->Compare(entry->Name); } }; +static StationFinderServices stationFinderServices; + class FindByCapability { public: - FindByCapability(char* name); - FindByCapability(char* name, char* keyWords, char* delimiter); + FindByCapability(const char* name); + FindByCapability(const char* name, BStringList* keyWords); + FindByCapability(const char* name, char* keyWords, char* delimiter); ~FindByCapability(); - bool HasKeyWords(); - void SetKeyWords(char* keyWords, char* delimiter); - const BStringList* KeyWords(); - - const char* Name(); + bool HasKeyWords() { return !fKeywords.IsEmpty(); } + const BStringList* KeyWords() { return &fKeywords; } + const char* Name() { return fName.String(); } private: BString fName; @@ -97,67 +112,71 @@ class StationFinderService { public: // Overridden in specific StationFinder implementations - StationFinderService(); - virtual ~StationFinderService(); + StationFinderService(); + virtual ~StationFinderService(); - static void RegisterSelf(); - static StationFinderService* Instantiate(); + static void RegisterSelf(); + static StationFinderService* Instantiate(); - virtual StationList* FindBy( - int capabilityIndex, const char* searchFor, BLooper* resultUpdateTarget) - = 0; + virtual StationList* FindBy( + int capabilityIndex, const char* searchFor, BLooper* resultUpdateTarget) + = 0; // Provided by ancestor class - const char* Name() const { return serviceName.String(); } + BString* Name() + { return &serviceName; } - int CountCapabilities() const { return findByCapabilities.CountItems(); } - FindByCapability* Capability(int index) const { return findByCapabilities.ItemAt(index); } + int CountCapabilities() const + { return findByCapabilities.CountItems(); } + FindByCapability* Capability(int index) const + { return findByCapabilities.ItemAt(index); } - static void Register(char* name, InstantiateFunc instantiate); + static void Register(BString* name, InstantiateFunc instantiate); protected: // To be filled by specific StationFinder implementations - BString serviceName; - BUrl serviceHomePage; - BBitmap* serviceLogo; + BString serviceName; + BUrl serviceHomePage; + BBitmap* serviceLogo; #if B_HAIKU_VERSION > B_HAIKU_VERSION_1_BETA_5 - BObjectList findByCapabilities; + BObjectList + findByCapabilities; #else - BObjectList findByCapabilities; + BObjectList findByCapabilities; #endif // Helper functions - BBitmap* RetrieveLogo(BUrl url); - FindByCapability* RegisterSearchCapability(char* name); - FindByCapability* RegisterSearchCapability(char* name, char* keyWords, char* delimiter); + BBitmap* RetrieveLogo(BUrl* url); + uint32 RegisterSearchCapability(const char* name); + uint32 RegisterSearchCapability(const char* name, BStringList* keyWords); }; class StationFinderWindow : public BWindow { public: - StationFinderWindow(BWindow* parent); - virtual ~StationFinderWindow(); + StationFinderWindow(BWindow* parent); + virtual ~StationFinderWindow(); - void MessageReceived(BMessage* msg); - virtual bool QuitRequested(); + void MessageReceived(BMessage* msg); + virtual bool QuitRequested(); - void SelectService(int index); - void SelectCapability(int index); - void DoSearch(const char* text); + void SelectService(int index); + void SelectCapability(int index); + void DoSearch(const char* text); private: - StationFinderService* fCurrentService; - - BMessenger* fMessenger; - BTextControl* fTxSearch; - BOptionPopUp* fKwSearch; - BButton* fBnSearch; - BOptionPopUp* fDdServices; - BButton* fBnVisit; - BOptionPopUp* fDdSearchBy; - StationListView* fResultView; - BButton* fBnAdd; - - BGridLayout* fSearchGrid; + StationFinderService* fCurrentService; + + BMessenger* fMessenger; + BTextControl* fTxSearch; + BOptionPopUp* fKwSearch; + BButton* fBnSearch; + BOptionPopUp* fDdServices; + BButton* fBnVisit; + BOptionPopUp* fDdSearchBy; + StationListView* fResultView; + BButton* fBnAdd; + + BGridLayout* fSearchGrid; }; diff --git a/source/StationFinderListenLive.cpp b/source/StationFinderListenLive.cpp deleted file mode 100644 index dc18ca9..0000000 --- a/source/StationFinderListenLive.cpp +++ /dev/null @@ -1,415 +0,0 @@ -/* - * Copyright (C) 2017 Kai Niessen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - - -#include "StationFinderListenLive.h" - -#define __USE_GNU -#include - -#include - -#include "Debug.h" - - -#undef B_TRANSLATION_CONTEXT -#define B_TRANSLATION_CONTEXT "StationFinderListenLive" - - -const char* kGenreList - = "Top 40|top40\r\ -Hot Adult Contemporary|hotac\r\ -Adult Contemporary|ac\r\ -Oldies/80s/90s|oldies\r\ -Country music|country\r\ -RnB/HipHop|rnb\r\ -Dance/Techno|dance\r\ -Rock/Classic Rock|rock\r\ -Alternative|alternative\r\ -Chillout/Lounge|chillout"; - -const char* kCountryList - = "Andorra|andorra\r\ -Armenia|armenia\r\ -Austria|austria\r\ -Azerbaijan|azerbaijan\r\ -Belarus|belarus\r\ -Belgium|belgium\r\ -Bosnia-Herzegovina|bosnia\r\ -Bulgaria|bulgaria\r\ -Croatia|croatia\r\ -Cyprus|cyprus\r\ -Czech Republic|czech-republic\r\ -Denmark|denmark\r\ -Estonia|estonia\r\ -Faroe Islands|faroe\r\ -Finland|finland\r\ -France|france\r\ -Georgia|georgia\r\ -Germany|germany\r\ -Gibraltar|gibraltar\r\ -Greece|greece\r\ -Hungary|hungary\r\ -Iceland|iceland\r\ -Ireland|ireland\r\ -Italy|italy\r\ -Kosovo|kosovo\r\ -Latvia|latvia\r\ -Liechtenstein|liechtenstein\r\ -Lithuania|lithuania\r\ -Luxembourg|luxembourg\r\ -Macedonia|macedonia\r\ -Malta|malta\r\ -Moldova|moldova\r\ -Monaco|monaco\r\ -Montenegro|montenegro\r\ -Netherlands|netherlands\r\ -Norway|norway\r\ -Poland|poland\r\ -Portugal|portugal\r\ -Romania|romania\r\ -Russia|russia\r\ -San Marino|san-marino\r\ -Serbia|serbia\r\ -Slovakia|slovakia\r\ -Slovenia|slovenia\r\ -Spain|spain\r\ -Sweden|sweden\r\ -Switzerland|switzerland\r\ -Turkey|turkey\r\ -Ukraine|ukraine\r\ -United Kingdom|uk\r\ -Vatican State|vatican"; - -const char* StationFinderListenLive::kBaseUrl = "http://listenlive.eu/"; - - -StationFinderListenLive::StationFinderListenLive() - : StationFinderService(), - fCountryKeywordAndPath(), - fGenreKeywordAndPath(), - fLookupThread(-1), - fPlsLookupList() -{ - serviceName.SetTo(B_TRANSLATE("listenlive.eu [experimental]")); - serviceHomePage.SetUrlString("http://www.listenlive.eu/"); - - BString tmp(kCountryList); - tmp.Split("\r", true, fCountryKeywordAndPath); - FindByCapability* findByCountry = RegisterSearchCapability("Country"); - BStringList* keywords = (BStringList*)findByCountry->KeyWords(); - for (int32 i = 0; i < fCountryKeywordAndPath.CountStrings(); i++) { - BString keyword(fCountryKeywordAndPath.StringAt(i)); - keyword.Truncate(keyword.FindFirst('|')); - keywords->Add(keyword); - } - - tmp.SetTo(kGenreList); - tmp.Split("\r", true, fGenreKeywordAndPath); - FindByCapability* findByGenre = RegisterSearchCapability("Genre"); - keywords = (BStringList*)findByGenre->KeyWords(); - for (int32 i = 0; i < fGenreKeywordAndPath.CountStrings(); i++) { - BString keyword(fGenreKeywordAndPath.StringAt(i)); - keyword.Truncate(keyword.FindFirst('|')); - keywords->Add(keyword); - } -} - - -StationFinderService* -StationFinderListenLive::Instantiate() -{ - return new StationFinderListenLive(); -} - - -void -StationFinderListenLive::RegisterSelf() -{ - Register("listenlive.eu [experimental]", &StationFinderListenLive::Instantiate); -} - - -StationFinderListenLive::~StationFinderListenLive() -{ - fPlsLookupList.MakeEmpty(false); - if (fLookupThread >= 0) { - status_t dummy; - wait_for_thread(fLookupThread, &dummy); - } -} - - -StationList* -StationFinderListenLive::FindBy( - int capabilityIndex, const char* searchFor, BLooper* resultUpdateTarget) -{ - if (fLookupThread >= 0) - suspend_thread(fLookupThread); - - fPlsLookupList.MakeEmpty(false); - fLookupNotify = resultUpdateTarget; - - StationList* result = NULL; - - BString urlString(kBaseUrl); - strlwr((char*)searchFor); - int keywordIndex = Capability(capabilityIndex)->KeyWords()->IndexOf(searchFor); - BStringList* keywordAndPaths - = capabilityIndex ? &fGenreKeywordAndPath : &fCountryKeywordAndPath; - BString path(keywordAndPaths->StringAt(keywordIndex)); - path.Remove(0, path.FindFirst('|') + 1); - urlString << path << ".html"; - BUrl url(urlString); - - BMallocIO* data = HttpUtils::GetAll(url); - if (data != NULL) { - switch (capabilityIndex) { - case 0: // findByCountry - result = ParseCountryReturn(data, searchFor); - break; - - case 1: // findByGenre - result = ParseGenreReturn(data, searchFor); - break; - } - } - - data->Flush(); - delete data; - - if (!fPlsLookupList.IsEmpty()) { - if (fLookupThread < 0) - fLookupThread = spawn_thread(&_PlsLookupFunc, "plslookup", B_NORMAL_PRIORITY, this); - resume_thread(fLookupThread); - } - - return result; -} - - -StationList* -StationFinderListenLive::ParseCountryReturn(BMallocIO* data, const char* searchFor) -{ -#if B_HAIKU_VERSION > B_HAIKU_VERSION_1_BETA_5 - StationList* result = new StationList(20); -#else - StationList* result = new StationList(20, true); -#endif - if (result == NULL) - return result; - - regmatch_t matches[10]; - regex_t regex; - - // - // ORF Ö-1 - // Vienna - // Windows
-	// Media
MP3 - // 128 Kbps
192 Kbps - // Information/Culture - // - - int res = regcomp(®ex, - "\\s*\\s*([^<]*)\\s*\\s*\\s*" - "([^<]*)\\s*" - "([^<]|<[^/]|\\s*" - "\\s*([^<]*)([^<]|<[^/]|\\s*" - "([^<]*)\\s*", - RE_ICASE | RE_DOT_NEWLINE | RE_DOT_NOT_NULL | RE_NO_BK_PARENS | RE_NO_BK_VBAR - | RE_BACKSLASH_ESCAPE_IN_LISTS); - - char* doc = (char*)data->Buffer(); - off_t size; - data->GetSize(&size); - doc[size] = 0; - - while ((res = regexec(®ex, doc, 10, matches, 0)) == 0) { - doc[matches[1].rm_eo] = 0; - doc[matches[2].rm_eo] = 0; - doc[matches[3].rm_eo] = 0; - doc[matches[5].rm_eo] = 0; - doc[matches[6].rm_eo] = 0; - doc[matches[8].rm_eo] = 0; - printf("Station %s at %s in %s address %s, data rate=%s, genre=%s\n", - doc + matches[2].rm_so, doc + matches[1].rm_so, doc + matches[3].rm_so, - doc + matches[5].rm_so, doc + matches[6].rm_so, doc + matches[8].rm_so); - - Station* station = new Station(doc + matches[2].rm_so, NULL); - if (station == NULL) - continue; - - result->AddItem(station); - - BString source(doc + matches[5].rm_so); - if (source.EndsWith(".pls") || source.EndsWith(".m3u")) { - station->SetSource(BUrl(source)); - fPlsLookupList.AddItem(station); - } else - station->SetStreamUrl(BUrl(source)); - - for (int32 i = matches[6].rm_so; i < matches[6].rm_eo; i++) { - if (!strchr("0123456789.", doc[i])) { - doc[i] = 0; - station->SetBitRate(atof(doc + matches[6].rm_so) * 1000); - break; - } - } - - station->SetStation(doc + matches[1].rm_so); - station->SetGenre(doc + matches[8].rm_so); - - BString country; - country.SetTo(searchFor); - country.Append(" - "); - country.Append(doc + matches[3].rm_so); - station->SetCountry(country); - - doc += matches[0].rm_eo; - } - - return result; -} - - -StationList* -StationFinderListenLive::ParseGenreReturn(BMallocIO* data, const char* searchFor) -{ -#if B_HAIKU_VERSION > B_HAIKU_VERSION_1_BETA_5 - StationList* result = new StationList(20); -#else - StationList* result = new StationList(20, true); -#endif - if (result == NULL) - return result; - - regmatch_t matches[9]; - regex_t regex; - - // - // ORF Ö-1 - // Vienna - // Windows
-	// Media
MP3 - // 128 Kbps
192 Kbps - // Information/Culture - // - - // - // Radio - // Stephansdom Vienna Austria MP3
32 Kbps, 56 Kbps
128 Kbps - // - - int res = regcomp(®ex, - "\\s*\\s*([^<]*)\\s*\\s*\\s*" - "([^<]*)\\s*" - "([^<]*)\\s*" - "([^<]|<[^/]|\\s*" - "\\s*([^<]*)([^<]|<[^/]|\\s*" - "", - RE_ICASE | RE_DOT_NEWLINE | RE_DOT_NOT_NULL | RE_NO_BK_PARENS | RE_NO_BK_VBAR - | RE_BACKSLASH_ESCAPE_IN_LISTS); - - char* doc = (char*)data->Buffer(); - off_t size; - data->GetSize(&size); - doc[size] = 0; - - while ((res = regexec(®ex, doc, 9, matches, 0)) == 0) { - doc[matches[1].rm_eo] = 0; - doc[matches[2].rm_eo] = 0; - doc[matches[3].rm_eo] = 0; - doc[matches[4].rm_eo] = 0; - doc[matches[6].rm_eo] = 0; - doc[matches[7].rm_eo] = 0; - printf("Station %s at %s in %s - %s address %s, data rate=%s, genre=%s\n", - doc + matches[2].rm_so, doc + matches[1].rm_so, doc + matches[3].rm_so, - doc + matches[4].rm_so, doc + matches[6].rm_so, doc + matches[7].rm_so, searchFor); - - Station* station = new Station(doc + matches[2].rm_so, NULL); - if (station == NULL) - continue; - - result->AddItem(station); - - BString source(doc + matches[6].rm_so); - if (source.EndsWith(".pls") || source.EndsWith(".m3u")) { - station->SetSource(BUrl(source)); - fPlsLookupList.AddItem(station); - } else - station->SetStreamUrl(BUrl(source)); - - for (int32 i = matches[7].rm_so; i < matches[7].rm_eo; i++) { - if (!strchr("0123456789.", doc[i])) { - doc[i] = 0; - station->SetBitRate(atof(doc + matches[7].rm_so) * 1000); - break; - } - } - - station->SetStation(doc + matches[1].rm_so); - station->SetGenre(searchFor); - - BString country(doc + matches[4].rm_so); - country.Append(" - "); - country.Append(doc + matches[5].rm_so); - station->SetCountry(country); - - doc += matches[0].rm_eo; - } - - return result; -} - - -int32 -StationFinderListenLive::_PlsLookupFunc(void* data) -{ - StationFinderListenLive* _this = (StationFinderListenLive*)data; - while (!_this->fPlsLookupList.IsEmpty()) { - Station* station = _this->fPlsLookupList.FirstItem(); - TRACE("Looking up stream URL for station %s in %s\n", station->Name()->String(), - station->Source().UrlString().String()); - - Station* plsStation = Station::LoadIndirectUrl((BString&)station->Source().UrlString()); - if (plsStation != NULL) - station->SetStreamUrl(plsStation->StreamUrl()); - - BMessage* notification = new BMessage(MSG_UPDATE_STATION); - notification->AddPointer("station", station); - if (_this->fLookupNotify->LockLooper()) { - _this->fLookupNotify->PostMessage(notification); - _this->fLookupNotify->UnlockLooper(); - } - - _this->fPlsLookupList.RemoveItem(station, false); - } - - _this->fLookupThread = -1; - return B_OK; -} diff --git a/source/StationFinderListenLive.h b/source/StationFinderListenLive.h deleted file mode 100644 index ec88183..0000000 --- a/source/StationFinderListenLive.h +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2017 Kai Niessen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#ifndef _STATION_FINDER_LISTEN_LIVE_H -#define _STATION_FINDER_LISTEN_LIVE_H - - -#include - -#include "StationFinder.h" - - -class StationFinderListenLive : public StationFinderService { -public: - StationFinderListenLive(); - virtual ~StationFinderListenLive(); - - static void RegisterSelf(); - static StationFinderService* Instantiate(); - - virtual StationList* FindBy( - int capabilityIndex, const char* searchFor, BLooper* resultUpdateTarget); - - StationList* ParseCountryReturn(BMallocIO* data, const char* country); - StationList* ParseGenreReturn(BMallocIO* data, const char* genre); - -private: - static int32 _PlsLookupFunc(void* data); - -private: - static const char* kBaseUrl; - - BStringList fCountryKeywordAndPath; - BStringList fGenreKeywordAndPath; - thread_id fLookupThread; - BObjectList fPlsLookupList; - - BLooper* fLookupNotify; -}; - - -#endif // _STATION_FINDER_LISTEN_LIVE_H diff --git a/source/StationFinderRadioNetwork.cpp b/source/StationFinderRadioNetwork.cpp index b18c08e..d7a67be 100644 --- a/source/StationFinderRadioNetwork.cpp +++ b/source/StationFinderRadioNetwork.cpp @@ -22,20 +22,20 @@ #include #include - #include +#include + + -#include "Debug.h" #include "HttpUtils.h" #undef B_TRANSLATION_CONTEXT #define B_TRANSLATION_CONTEXT "StationFinderRadioNetwork" +BString StationFinderRadioNetwork::sBaseUrl("https://de1.api.radio-browser.info/"); -const char* StationFinderRadioNetwork::kBaseUrl = "https://all.api.radio-browser.info/"; - -BString StationFinderRadioNetwork::sCachedServerUrl = B_EMPTY_STRING; +const char* StationFinderRadioNetwork::serviceNameInternal = "Community Radio Browser"; IconLookup::IconLookup(Station* station, BUrl iconUrl) @@ -45,6 +45,33 @@ IconLookup::IconLookup(Station* station, BUrl iconUrl) } +BStringList* +StationFinderRadioNetwork::_GetKeywords(const char* path, int32 minStations) { + BString keywordUrlString(sBaseUrl); + keywordUrlString.Append(path); + BUrl keywordUrl(keywordUrlString); + + BMessage* parsedData = HttpUtils::GetMsgFromREST(&keywordUrl); + if (parsedData != NULL) { + BStringList* keywords = new BStringList(true); + char* name; + uint32 type; + int32 count; + for (int32 index = 0; parsedData->GetInfo(B_MESSAGE_TYPE, index, &name, &type, &count) == B_OK; index++) { + BMessage keywordMessage; + if (parsedData->FindMessage(name, &keywordMessage) == B_OK) { + const char* keyword = keywordMessage.FindString("name"); + double stationCount = keywordMessage.FindDouble("stationcount"); + if (stationCount > minStations) + keywords->Add(keyword); + } + } + delete parsedData; + return keywords; + } + return NULL; +}; + StationFinderRadioNetwork::StationFinderRadioNetwork() : StationFinderService(), fIconLookupThread(-1), @@ -54,16 +81,41 @@ StationFinderRadioNetwork::StationFinderRadioNetwork() fIconLookupList(100, true) #endif { - serviceName.SetTo(B_TRANSLATE("Community Radio Browser")); + serviceName.SetTo(B_TRANSLATE(serviceNameInternal)); serviceHomePage.SetUrlString("https://www.radio-browser.info"); - RegisterSearchCapability("Name"); - RegisterSearchCapability("Tag"); - RegisterSearchCapability("Language"); - RegisterSearchCapability("Country"); - RegisterSearchCapability("Country code"); - RegisterSearchCapability("State/Region"); - RegisterSearchCapability("Unique identifier"); + BStringList* keywords; + + // Register different search capabilities + searchCapabilityName = RegisterSearchCapability("Name"); + + // Tag Search (dropdown) + keywords = _GetKeywords("json/tags?limit=1000000&hidebroken=true", 20); + if (keywords != NULL) { + searchCapabilityTag = RegisterSearchCapability("Tag", keywords); + delete keywords; + } + + // Language Search (dropdown) + keywords = _GetKeywords("json/languages?limit=1000000&hidebroken=true", 20); + if (keywords != NULL) { + searchCapabilityLanguage = RegisterSearchCapability("Language", keywords); + delete keywords; + } + + keywords = _GetKeywords("json/countries?limit=1000000&hidebroken=true", 10); + if (keywords != NULL) { + searchCapabilityCountry = RegisterSearchCapability("Country", keywords); + delete keywords; + } + + keywords = _GetKeywords("json/codecs?limit=1000000&hidebroken=true", 10); + if (keywords != NULL && !keywords->IsEmpty()) { + searchCapabilityCodec = RegisterSearchCapability("Codec", keywords); + delete keywords; + } + + searchCapabilityUuid = RegisterSearchCapability("Unique identifier"); } @@ -84,7 +136,7 @@ StationFinderRadioNetwork::Instantiate() void StationFinderRadioNetwork::RegisterSelf() { - Register("Community Radio Browser", &StationFinderRadioNetwork::Instantiate); + Register(new BString(B_TRANSLATE(serviceNameInternal)), &StationFinderRadioNetwork::Instantiate); } @@ -100,69 +152,42 @@ StationFinderRadioNetwork::FindBy( StationList* result = new StationList(); if (result == NULL) return result; - - if (_CheckServer() != B_OK) - return result; - - printf("Connected to server: %s\n", sCachedServerUrl.String()); - - BString urlString(sCachedServerUrl); + + BString urlString(sBaseUrl); // Add the format and station section... urlString.Append("json/stations/"); - switch (capabilityIndex) { - case 0: // Name search - urlString.Append("byname/"); - break; - - case 1: // Tag search - urlString.Append("bytag/"); - break; - - case 2: // Language search - urlString.Append("bylanguage/"); - break; - - case 3: // Country search - urlString.Append("bycountry/"); - break; - - case 4: // Country code search - urlString.Append("bycountrycodeexact/"); - break; - - case 5: // State/Region search - urlString.Append("bystate/"); - break; - - case 6: // Unique identifier search - urlString.Append("byuuid/"); - break; - - default: // A very bad kind of search? Just do a name search... - urlString.Append("byname/"); - break; - } + if (capabilityIndex == searchCapabilityName) // Name search + urlString.Append("byname/"); + else if (capabilityIndex == searchCapabilityTag) // Tag search + urlString.Append("bytag/"); + else if (capabilityIndex == searchCapabilityLanguage) // Language search + urlString.Append("bylanguage/"); + else if (capabilityIndex == searchCapabilityCountry) // Country search + urlString.Append("bycountry/"); + else if (capabilityIndex == searchCapabilityCodec) // Codec search + urlString.Append("bycodec/"); + else if (capabilityIndex == searchCapabilityUuid) // Unique identifier search + urlString.Append("byuuid/"); + else // A very bad kind of search? Just do a name search... + urlString.Append("byname/"); BString searchForString(searchFor); searchForString = BUrl::UrlEncode(searchForString, true, true); urlString.Append(searchForString); + urlString.Append("?limit=2000000&hidebroken=true"); BUrl finalUrl(urlString); - BMessage parsedData; - BMallocIO* data = HttpUtils::GetAll(finalUrl); - if (data != NULL - && BJson::Parse((const char*)data->Buffer(), data->BufferLength(), parsedData) == B_OK) { - delete data; - + BMessage* parsedData = HttpUtils::GetMsgFromREST(&finalUrl, NULL, 5000); + if (parsedData != NULL) { char* name; uint32 type; int32 count; for (int32 index = 0; - parsedData.GetInfo(B_MESSAGE_TYPE, index, &name, &type, &count) == B_OK; index++) { + parsedData->GetInfo(B_MESSAGE_TYPE, index, &name, &type, &count) == B_OK; index++) { BMessage stationMessage; - if (parsedData.FindMessage(name, &stationMessage) == B_OK) { + if (parsedData->FindMessage(name, &stationMessage) == B_OK) { Station* station = new Station("unknown"); if (station == NULL) continue; @@ -177,11 +202,8 @@ StationFinderRadioNetwork::FindBy( station->SetStation(stationMessage.GetString("homepage", B_EMPTY_STRING)); BString iconUrl; - if (stationMessage.FindString("favicon", &iconUrl) == B_OK) { - if (!iconUrl.IsEmpty()) { - fIconLookupList.AddItem(new IconLookup(station, BUrl(iconUrl))); - } - } + if (stationMessage.FindString("favicon", &iconUrl) == B_OK && !iconUrl.IsEmpty()) + fIconLookupList.AddItem(new IconLookup(station, BUrl(iconUrl))); station->SetGenre(stationMessage.GetString("tags", B_EMPTY_STRING)); @@ -206,13 +228,13 @@ StationFinderRadioNetwork::FindBy( result->AddItem(station); } } + delete parsedData; if (!fIconLookupList.IsEmpty()) { fIconLookupThread = spawn_thread(&_IconLookupFunc, "iconlookup", B_LOW_PRIORITY, this); resume_thread(fIconLookupThread); } } else { - delete data; delete result; result = NULL; } @@ -227,8 +249,8 @@ StationFinderRadioNetwork::_IconLookupFunc(void* data) StationFinderRadioNetwork* _this = (StationFinderRadioNetwork*)data; while (_this->fIconLookupThread >= 0 && !_this->fIconLookupList.IsEmpty()) { IconLookup* item = _this->fIconLookupList.FirstItem(); - BBitmap* logo = _this->RetrieveLogo(item->fIconUrl); - if (logo != NULL) { + BBitmap* logo = _this->RetrieveLogo(&item->fIconUrl); + if (logo != NULL && logo->IsValid()) { item->fStation->SetLogo(logo); BMessage* notification = new BMessage(MSG_UPDATE_STATION); @@ -247,31 +269,6 @@ StationFinderRadioNetwork::_IconLookupFunc(void* data) } -status_t -StationFinderRadioNetwork::_CheckServer() -{ - // Just a quick check up on our cached server...if it exists. - BUrl cachedServerUrl(sCachedServerUrl); - if (!sCachedServerUrl.IsEmpty() - && HttpUtils::CheckPort(cachedServerUrl, &cachedServerUrl, 0) == B_OK) { - // It's still there! - return B_OK; - } - - // Try to find an active server! - BUrl testServerUrl(kBaseUrl); - status_t result = HttpUtils::CheckPort(testServerUrl, &testServerUrl, 0); - if (result != B_OK) { - // Oh no...this is, uh, pretty bad. - return result; - } - - // Cache it! - sCachedServerUrl.SetTo(testServerUrl.UrlString()); - - return B_OK; -} - void StationFinderRadioNetwork::_WaitForIconLookupThread() { diff --git a/source/StationFinderRadioNetwork.h b/source/StationFinderRadioNetwork.h index 1195d19..1c49b12 100644 --- a/source/StationFinderRadioNetwork.h +++ b/source/StationFinderRadioNetwork.h @@ -43,15 +43,24 @@ class StationFinderRadioNetwork : public StationFinderService { int capabilityIndex, const char* searchFor, BLooper* resultUpdateTarget); private: + static const char* serviceNameInternal; + + static BStringList* _GetKeywords(const char* path, int32 minStations); static int32 _IconLookupFunc(void* data); - status_t _CheckServer(); void _WaitForIconLookupThread(); private: - static const char* kBaseUrl; - static BString sCachedServerUrl; + static BString sBaseUrl; thread_id fIconLookupThread; + + int searchCapabilityName = -1; + int searchCapabilityTag = -1; + int searchCapabilityLanguage = -1; + int searchCapabilityCountry = -1; + int searchCapabilityCodec = -1; + int searchCapabilityUuid = -1; + #if B_HAIKU_VERSION > B_HAIKU_VERSION_1_BETA_5 BObjectList fIconLookupList; #else diff --git a/source/StreamIO.cpp b/source/StreamIO.cpp index 389cb96..ca6a66a 100644 --- a/source/StreamIO.cpp +++ b/source/StreamIO.cpp @@ -78,10 +78,11 @@ StreamIO::StreamIO(Station* station, BLooper* metaListener) // of streams using HTTPS and load balancing between two or more different // IP's should be small, anyway. - if (url.Protocol() == "https") { + // if (url.Protocol() == "https") { fReq = dynamic_cast( BUrlProtocolRoster::MakeRequest(url.UrlString().String(), this, this)); - } else { + /* + } else { BUrl* newUrl = new BUrl(); if (newUrl == NULL) return; @@ -94,7 +95,7 @@ StreamIO::StreamIO(Station* station, BLooper* metaListener) BUrlProtocolRoster::MakeRequest(newUrl->UrlString().String(), this, this)); delete newUrl; } - + */ if (fReq == NULL) return; @@ -150,7 +151,7 @@ StreamIO::WriteAt(off_t position, const void* buffer, size_t size) ssize_t StreamIO::ReadAt(off_t position, void* buffer, size_t size) { - if (fLimit == 0 || position < fLimit) { + if (fLimit == 0 || (size_t)position < fLimit) { ssize_t read = BAdapterIO::ReadAt(position, buffer, size); if (read > 0) { TRACE("Read %" B_PRIdSSIZE " of %" B_PRIuSIZE " bytes from position %" B_PRIdOFF @@ -256,7 +257,7 @@ StreamIO::_DataWithMetaReceived(const char* data, size_t size, int next) while (size > 0) { if (fUntilMetaEnd != 0) { // We are reading metadata - if (fUntilMetaEnd <= size) { + if ((size_t)fUntilMetaEnd <= size) { // The metadata ends before the buffer memcpy(fMetaBuffer + fMetaSize, (void*)data, fUntilMetaEnd); @@ -283,7 +284,7 @@ StreamIO::_DataWithMetaReceived(const char* data, size_t size, int next) } else { // No metadata right now, feed content to consumer DataFunc nextFunc = fDataFuncs.Item(next); - if (size <= fUntilMetaStart) { + if (size <= (size_t)fUntilMetaStart) { written += (*this.*nextFunc)(data, size, next + 1); fUntilMetaStart -= size; @@ -324,7 +325,7 @@ ssize_t StreamIO::_DataUnsyncedReceived(const char* data, size_t size, int next) { off_t frameStart; - for (frameStart = 0; frameStart < size; frameStart++) { + for (frameStart = 0; frameStart < (off_t)size; frameStart++) { if (fFrameSync == none) { if (data[frameStart] == kMpegHeader1) { fFrameSync = first; @@ -401,7 +402,7 @@ StreamIO::_ProcessMeta() text[matches[1].rm_eo] = 0; text[matches[2].rm_eo] = 0; - if (text + matches[2].rm_so && !(text + matches[2].rm_so)[0]) + if (text[matches[2].rm_so] == 0) msg->AddString(strlwr(text + matches[1].rm_so), fIcyName); else msg->AddString(strlwr(text + matches[1].rm_so), text + matches[2].rm_so); diff --git a/source/StreamPlayer.cpp b/source/StreamPlayer.cpp index 3c66982..6a279d8 100644 --- a/source/StreamPlayer.cpp +++ b/source/StreamPlayer.cpp @@ -46,6 +46,8 @@ StreamPlayer::StreamPlayer(Station* station, BLooper* notify) if (fInitStatus != B_OK) { MSG("Error retrieving stream from %s - %s\n", station->StreamUrl().UrlString().String(), strerror(fInitStatus)); + delete fStream; + fStream = NULL; return; } } @@ -181,7 +183,8 @@ StreamPlayer::_GetDecodedChunk( if (player->fFlushCount++ > 1000) { player->fFlushCount = 0; - player->fStream->FlushRead(); + if (player->fStream) + player->fStream->FlushRead(); } } @@ -189,6 +192,8 @@ StreamPlayer::_GetDecodedChunk( status_t StreamPlayer::_StartPlayThreadFunc(StreamPlayer* _this) { + if (!_this->fStream) return B_ERROR; + _this->Lock(); _this->_SetState(StreamPlayer::Buffering); diff --git a/source/Utils.cpp b/source/Utils.cpp index c4ca259..860aca7 100644 --- a/source/Utils.cpp +++ b/source/Utils.cpp @@ -18,7 +18,6 @@ #include "Utils.h" -#include #include #include #include @@ -44,30 +43,3 @@ Utils::ResourceBitmap(int32 id) return NULL; } - - -const char* -Utils::UserAgent() -{ - if (sUserAgent[0] != 0) - return sUserAgent; - - app_info appInfo; - be_app->GetAppInfo(&appInfo); - BFile file(&appInfo.ref, B_READ_ONLY); - if (file.InitCheck() == B_OK) { - BAppFileInfo appFileInfo(&file); - struct version_info version; - if (appFileInfo.GetVersionInfo(&version, B_APP_VERSION_KIND) == B_OK) - snprintf(sUserAgent, sizeof(sUserAgent), - "StreamRadio/%" B_PRIu32 ".%" B_PRIu32 ".%" B_PRIu32, version.major, version.middle, - version.minor); - } - - if (sUserAgent[0] == 0) { - // There was an error trying to get the version info - sprintf(sUserAgent, "StreamRadio"); - } - - return sUserAgent; -} diff --git a/source/Utils.h b/source/Utils.h index 6213c64..7600711 100644 --- a/source/Utils.h +++ b/source/Utils.h @@ -30,11 +30,7 @@ class Utils { public: - Utils(){}; - virtual ~Utils(){}; - static BBitmap* ResourceBitmap(int32 id); - static const char* UserAgent(); }; From ba5d2398dad8e9b966a3dff882642637e44b2258 Mon Sep 17 00:00:00 2001 From: fishpond-haiku Date: Mon, 14 Sep 2026 14:26:24 +0200 Subject: [PATCH 2/3] Fix station search Enhance stations finder plug interface (even though only one working service left) and adapt to new RadioNetwork API, using dropdowns for country or codec searches. Removed DNS caching and failover again, only one Radionetwork server left. --- .gitignore | 3 + source/HttpUtils.cpp | 298 +++++++++++++++---- source/HttpUtils.h | 18 +- source/MainWindow.cpp | 97 +++---- source/Makefile | 39 ++- source/PackageInfo | 38 +++ source/RadioApp.cpp | 11 +- source/Station.cpp | 17 +- source/StationFinder.cpp | 145 +++------- source/StationFinder.h | 135 +++++---- source/StationFinderListenLive.cpp | 415 --------------------------- source/StationFinderListenLive.h | 55 ---- source/StationFinderRadioNetwork.cpp | 195 +++++++------ source/StationFinderRadioNetwork.h | 15 +- source/StreamIO.cpp | 17 +- source/StreamPlayer.cpp | 7 +- source/Utils.cpp | 33 +-- source/Utils.h | 4 - 18 files changed, 614 insertions(+), 928 deletions(-) create mode 100644 source/PackageInfo delete mode 100644 source/StationFinderListenLive.cpp delete mode 100644 source/StationFinderListenLive.h diff --git a/.gitignore b/.gitignore index c173ce7..3dbfb56 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ dist/* source/objects*/ +nbproject/* +.genio.yaml +.clang-format \ No newline at end of file diff --git a/source/HttpUtils.cpp b/source/HttpUtils.cpp index 55b501b..62b119f 100644 --- a/source/HttpUtils.cpp +++ b/source/HttpUtils.cpp @@ -19,6 +19,10 @@ #include #include +#include +#include +#include +#include #include "Debug.h" #include "HttpUtils.h" @@ -58,53 +62,8 @@ class DataLimit : public BUrlProtocolListener, public BDataIO { }; -/** - * Loops on the DNS responses looking for connectivity on specified port. - */ -status_t -HttpUtils::CheckPort(BUrl url, BUrl* newUrl, uint32 flags) -{ - uint16 port; - if (url.HasPort()) - port = url.Port(); - else - port = 80; - - BReference resolver - = BNetworkAddressResolver::Resolve(url.Host(), port, flags); - if (resolver.Get() == NULL) - return B_NO_MEMORY; - status_t status = resolver->InitCheck(); - if (status != B_OK) - return status; - - BString newUrlString; - BNetworkAddress ipAddress; - BSocket* socket; - uint32 cookie = 0; - - status_t portStatus = B_ERROR; - while (portStatus != B_OK) { - status = resolver->GetNextAddress(AF_INET6, &cookie, ipAddress); - if (status != B_OK) - status = resolver->GetNextAddress(&cookie, ipAddress); - if (status != B_OK) - return status; - socket = new (std::nothrow) BSocket(); - portStatus = socket->Connect(ipAddress); - delete socket; - } - - // If port number is 80, do not add it to the final URL - // Then, prepend the appropiate protocol - newUrlString - = ipAddress.ToString(ipAddress.Port() != 80).Prepend("://").Prepend(url.Protocol()); - if (url.HasPath()) - newUrlString.Append(url.Path()); - newUrl->SetUrlString(newUrlString.String(), true); - - return B_OK; -} +BString +HttpUtils::jsonType("application/json"); /** @@ -115,7 +74,7 @@ HttpUtils::CheckPort(BUrl url, BUrl* newUrl, uint32 flags) * @return BMallocIO* filled with retrieved content */ BMallocIO* -HttpUtils::GetAll(BUrl url, BHttpHeaders* responseHeaders, bigtime_t timeOut, BString* contentType, +HttpUtils::GetAll(BUrl* url, BHttpHeaders* responseHeaders, bigtime_t timeOut, BString* contentType, size_t sizeLimit) { BMallocIO* data = new BMallocIO(); @@ -126,37 +85,42 @@ HttpUtils::GetAll(BUrl url, BHttpHeaders* responseHeaders, bigtime_t timeOut, BS BHttpRequest* request; if (sizeLimit) request = dynamic_cast( - BUrlProtocolRoster::MakeRequest(url.UrlString().String(), &reader, &reader, NULL)); + BUrlProtocolRoster::MakeRequest(url->UrlString().String(), &reader, &reader, NULL)); else request = dynamic_cast( - BUrlProtocolRoster::MakeRequest(url.UrlString().String(), data, NULL, NULL)); + BUrlProtocolRoster::MakeRequest(url->UrlString().String(), data, NULL, NULL)); - if (request == NULL) + if (request == NULL) { + delete data; return NULL; + } if (contentType && !contentType->IsEmpty()) { - BHttpHeaders* requestHeaders = new BHttpHeaders(); - requestHeaders->AddHeader("accept", contentType->String()); - request->AdoptHeaders(requestHeaders); + BHttpHeaders requestHeaders; + requestHeaders.AddHeader("accept", contentType->String()); + request->SetHeaders(requestHeaders); } request->SetAutoReferrer(true); request->SetFollowLocation(true); request->SetTimeout(timeOut); - request->SetUserAgent(Utils::UserAgent()); + request->SetUserAgent(UserAgent()); thread_id threadId = request->Run(); status_t status; wait_for_thread(threadId, &status); - BHttpResult& result = (BHttpResult&)request->Result(); - int32 statusCode = result.StatusCode(); - size_t bufferLen = data->BufferLength(); + const BHttpResult& result = dynamic_cast(request->Result()); + int32 statusCode = result.StatusCode(); + size_t bufferLen = data->BufferLength(); if (!(statusCode == 0 || request->IsSuccessStatusCode(statusCode)) || bufferLen == 0) { delete data; data = NULL; - } else if (contentType != NULL) - contentType->SetTo(result.ContentType()); + } else { + data->Seek(0, SEEK_SET); + if (contentType != NULL) + contentType->SetTo(result.ContentType()); + } if (responseHeaders != NULL) *responseHeaders = result.Headers(); @@ -164,3 +128,217 @@ HttpUtils::GetAll(BUrl url, BHttpHeaders* responseHeaders, bigtime_t timeOut, BS delete request; return data; } + + +/** + * Helper class for scanning simple JSONs + */ +class JsonPathExtractor : public BJsonEventListener { +public: + + /** + * Listens to JSON parsing events and extracts specific path + * @param path Dot-separated location of element in JSON hierarchy + * [].{}.address.{}.street will get street within an + * address object within the unnamed objects of the array + * @param result Initialized BStringList to capture all occurrences + */ + JsonPathExtractor(BString* path, BStringList* result) : + BJsonEventListener(), + pathElements(true) + { + path->Split(".", true, pathElements); + this->result = result; + } + + ~JsonPathExtractor() { + pathElements.MakeEmpty(); + } + + virtual bool Handle(const BJsonEvent& event) { + switch (event.EventType()) { + case B_JSON_NUMBER : + case B_JSON_TRUE : + case B_JSON_FALSE : + case B_JSON_NULL : + if (awaitValue) + awaitValue = false; + if (offIndex > 0) + offIndex--; + else + index--; + break; + + case B_JSON_STRING : + if (awaitValue) { + result->Add(event.Content()); + awaitValue = false; + } + if (offIndex > 0) + offIndex--; + else + index--; + break; + + case B_JSON_OBJECT_START : + if (index < pathElements.CountStrings() && + pathElements.StringAt(index) == "{}" && + offIndex == 0) + index++; + else + offIndex++; + break; + + case B_JSON_OBJECT_END : + if (offIndex > 0) + offIndex--; + else + index--; + break; + + case B_JSON_OBJECT_NAME : + if (index < pathElements.CountStrings() && + pathElements.StringAt(index) == event.Content() && + offIndex == 0) + { + index++; + if (index == pathElements.CountStrings()) + awaitValue = true; + } + else + offIndex++; + break; + + case B_JSON_ARRAY_START : + if (index < pathElements.CountStrings() && + pathElements.StringAt(index) == "[]" && + offIndex == 0) + index++; + else + offIndex++; + break; + + case B_JSON_ARRAY_END : + if (offIndex > 0) + offIndex--; + else { + index--; + } + break; + } + return true; + } + + virtual void HandleError(status_t status, int32 line, const char* message) { + printf("JsonParser line %d: %s", line, message); + } + virtual void Complete() { } + +private: + int index = 0; + int offIndex = 0; + bool awaitValue = false; + BString name; + BStringList* result = NULL; + + // path like "[].{}.name" extracts all elements called "name" in root array of unnamed objects + BStringList pathElements; +}; + +/** + * Helper to make retrieve list of occurrences of specific element in JSON + * @param url REST-Url to request + * @param path Dot-separated location of element in JSON hierarchy + * [].{}.address.{}.street will get street within an + * address object within the unnamed objects of the array + * @param contentType In/Out: Received headers + * @param timeout Timeout of request in ms, default set to 3 seconds + * @return BStringList* filled with all occurrences of element described by path + */ +BMessage* +HttpUtils::GetMsgFromREST(BUrl* url, BHttpHeaders* responseHeaders, bigtime_t timeOut) { + BMallocIO* json = GetAll(url, responseHeaders, timeOut, &jsonType, 0); + if (json == NULL) + return NULL; + + BMessage* data = new BMessage(); + + status_t status = BJson::Parse((const char*)json->Buffer(), json->BufferLength(), *data); + delete json; + + if (status != B_OK) { + delete data; + data = NULL; + } + + return data; +} + +/** + * Helper to make retrieve list of occurrences of specific element in JSON + * @param url REST-Url to request + * @param path Dot-separated location of element in JSON hierarchy + * [].{}.address.{}.street will get street within an + * address object within the unnamed objects of the root array + * @param responseHeaders Unless NULL, populated with HTTP headers of response + * @param contentType In/Out: Received headers + * @param timeout Timeout of request in ms, default set to 3 seconds + * @return BStringList* filled with all occurrences of element described by path + */ +BStringList* +HttpUtils::GetStringsFromREST(BUrl* url, BString* path, BHttpHeaders* responseHeaders, + bigtime_t timeout) { + BMallocIO* json = GetAll(url, responseHeaders, timeout, &jsonType, 0); + if (json == NULL) + return NULL; + + if (json->BufferLength() == 0) { + delete json; + return NULL; + } + + BStringList* result = new BStringList(); + JsonPathExtractor extractor(path, result); + BJson::Parse(json, &extractor); + delete json; + + if (result->IsEmpty()) { + delete result; + result = NULL; + } + + return result; +} + +char HttpUtils::sUserAgent[50] = "\0"; + +const char* +HttpUtils::UserAgent() +{ + if (sUserAgent[0] != 0) + return sUserAgent; + + app_info appInfo; + be_app->GetAppInfo(&appInfo); + BFile file(&appInfo.ref, B_READ_ONLY); + if (file.InitCheck() == B_OK) { + BAppFileInfo appFileInfo(&file); + struct version_info version; + if (appFileInfo.GetVersionInfo(&version, B_APP_VERSION_KIND) == B_OK) + snprintf(sUserAgent, sizeof(sUserAgent), + "StreamRadio/%" B_PRIu32 ".%" B_PRIu32 ".%" B_PRIu32, version.major, version.middle, + version.minor); + } + + if (sUserAgent[0] == 0) { + // There was an error trying to get the version info + sprintf(sUserAgent, "StreamRadio"); + } + + return sUserAgent; +} + + + + + diff --git a/source/HttpUtils.h b/source/HttpUtils.h index e1df8f3..b5eb7d6 100644 --- a/source/HttpUtils.h +++ b/source/HttpUtils.h @@ -23,16 +23,26 @@ #include #include #include +#include using namespace BPrivate::Network; +#define HTTP_DEFAULT_TIMEOUT 3000 class HttpUtils { public: - static status_t CheckPort(BUrl url, BUrl* newUrl, uint32 flags = 0); - - static BMallocIO* GetAll(BUrl url, BHttpHeaders* returnHeaders = NULL, bigtime_t timeOut = 3000, - BString* contentType = NULL, size_t sizeLimit = 0); + static BString jsonType; + static BMallocIO* GetAll(BUrl* url, BHttpHeaders* responseHeaders = NULL, + bigtime_t timeOut = HTTP_DEFAULT_TIMEOUT, + BString* contentType = NULL, size_t sizeLimit = 0); + static BMessage* GetMsgFromREST(BUrl* url, BHttpHeaders* responseHeaders = NULL, + bigtime_t timeOut = HTTP_DEFAULT_TIMEOUT); + static BStringList* GetStringsFromREST(BUrl* url, BString* path, + BHttpHeaders* responseHeaders = NULL, + bigtime_t timeout = HTTP_DEFAULT_TIMEOUT); + static const char* UserAgent(); +private: + static char sUserAgent[50]; }; diff --git a/source/MainWindow.cpp b/source/MainWindow.cpp index 3e77b0a..c6fbe4f 100644 --- a/source/MainWindow.cpp +++ b/source/MainWindow.cpp @@ -1,23 +1,23 @@ /* - Copyright (C) 2008-2010 Lukas Sommer < SommerLuk at gmail dot com > - Copyright (C) 2017 Kai Niessen - Copyright (C) 2020 Jacob Secunda - - This program is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License as - published by the Free Software Foundation; either version 2 of - the License or (at your option) version 3 or any later version - accepted by the membership of KDE e.V. (or its successor approved - by the membership of KDE e.V.), which shall act as a proxy - defined in Section 14 of version 3 of the license. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . + Copyright (C) 2008-2010 Lukas Sommer < SommerLuk at gmail dot com > + Copyright (C) 2017 Kai Niessen + Copyright (C) 2020 Jacob Secunda + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ @@ -36,7 +36,7 @@ #include #include -#include "Debug.h" + #include "RadioApp.h" @@ -45,35 +45,34 @@ MainWindow::MainWindow() - : BWindow(BRect(0, 0, 400, 200), B_TRANSLATE_SYSTEM_NAME("StreamRadio"), B_DOCUMENT_WINDOW, - B_AUTO_UPDATE_SIZE_LIMITS), + : BWindow(BRect(0, 0, 400, 200), B_TRANSLATE_SYSTEM_NAME("StreamRadio"), B_DOCUMENT_WINDOW, 0), fStationFinder(NULL) { fSettings = &((RadioApp*)be_app)->Settings; fAllowParallelPlayback = fSettings->GetAllowParallelPlayback(); fMenuParallelPlayback = new BMenuItem( - B_TRANSLATE("Allow parallel playback"), new BMessage(MSG_PARALLEL_PLAYBACK)); + B_TRANSLATE("Allow parallel playback"), new BMessage(MSG_PARALLEL_PLAYBACK)); fMenuParallelPlayback->SetMarked(fAllowParallelPlayback); fMainMenu = new BMenuBar(Bounds(), "MainMenu"); BLayoutBuilder::Menu<>(fMainMenu) .AddMenu(B_TRANSLATE("App")) - .AddItem(fMenuParallelPlayback) - .AddItem(B_TRANSLATE("Help" B_UTF8_ELLIPSIS), MSG_HELP) - .AddItem(B_TRANSLATE("About"), B_ABOUT_REQUESTED) - .AddSeparator() - .AddItem(B_TRANSLATE("Quit"), B_QUIT_REQUESTED, 'Q') + .AddItem(fMenuParallelPlayback) + .AddItem(B_TRANSLATE("Help" B_UTF8_ELLIPSIS), MSG_HELP) + .AddItem(B_TRANSLATE("About"), B_ABOUT_REQUESTED) + .AddSeparator() + .AddItem(B_TRANSLATE("Quit"), B_QUIT_REQUESTED, 'Q') .End() .AddMenu(B_TRANSLATE("Station")) - .AddItem(B_TRANSLATE("Paste Shoutcast URL"), MSG_PASTE_URL, 'V') - .AddItem(B_TRANSLATE("Check station"), MSG_CHECK) - .AddItem(B_TRANSLATE("Remove station"), MSG_REMOVE, 'R') + .AddItem(B_TRANSLATE("Paste Shoutcast URL"), MSG_PASTE_URL, 'V') + .AddItem(B_TRANSLATE("Check station"), MSG_CHECK) + .AddItem(B_TRANSLATE("Remove station"), MSG_REMOVE, 'R') .End() .AddMenu(B_TRANSLATE("Search")) - .AddItem(B_TRANSLATE("Find stations" B_UTF8_ELLIPSIS), MSG_SEARCH, 'S') + .AddItem(B_TRANSLATE("Find stations" B_UTF8_ELLIPSIS), MSG_SEARCH, 'S') .End() - .End(); + .End(); fStationList = new StationListView(true); BScrollView* stationScroll = new BScrollView("scrollStation", fStationList, 0, false, true); @@ -84,16 +83,16 @@ MainWindow::MainWindow() BLayoutBuilder::Group<>(this, B_VERTICAL) .Add(fMainMenu) - .AddGroup(B_VERTICAL) - .SetInsets(B_USE_WINDOW_INSETS, 0, B_USE_WINDOW_INSETS, B_USE_WINDOW_INSETS) - .AddSplit(B_VERTICAL, 2) - .Add(stationScroll, 1.0f) - .Add(fStationPanel) - .SetCollapsible(1, true) - .End() - .Add(fStatusBar) + .AddGroup(B_VERTICAL) + .SetInsets(B_USE_WINDOW_INSETS, 0, B_USE_WINDOW_INSETS, B_USE_WINDOW_INSETS) + .AddSplit(B_VERTICAL, 2) + .Add(stationScroll, 1.0f) + .Add(fStationPanel) + .SetCollapsible(1, true) + .End() + .Add(fStatusBar) .End() - .End(); + .End(); fStationList->Sync(fSettings->Stations); fStationList->SetInvocationMessage(new BMessage(MSG_INVOKE_STATION)); @@ -145,7 +144,7 @@ MainWindow::MessageReceived(BMessage* message) B_TRANSLATE("Added station %s to list"), station->Name()->String()); } } else - result.SetToFormat(B_TRANSLATE("File %s could not be loaded as a station")); + result.SetToFormat(B_TRANSLATE("File could not be loaded as a station")); fStatusBar->SetText(result.String()); } @@ -153,13 +152,11 @@ MainWindow::MessageReceived(BMessage* message) } case MSG_SEARCH: - { if (fStationFinder == NULL) fStationFinder = new StationFinderWindow(this); fStationFinder->Show(); break; - } case MSG_PASTE_URL: { @@ -212,10 +209,8 @@ MainWindow::MessageReceived(BMessage* message) fStationList->Invalidate(); fStationPanel->SetStation(stationItem); } - break; } - case MSG_REMOVE: { Station* station = fStationList->StationAt(fStationList->CurrentSelection(0)); @@ -322,17 +317,18 @@ MainWindow::MessageReceived(BMessage* message) case MSG_HELP: { - BUrl userguide = BUrl( + BUrl userguide( "https://github.com/HaikuArchives/" "StreamRadio/blob/master/docs/userguide.md", - true); + true + ); + userguide.OpenWithPreferredApplication(true); break; } case MSG_PARALLEL_PLAYBACK: - { fAllowParallelPlayback = !fAllowParallelPlayback; fSettings->SetAllowParallelPlayback(fAllowParallelPlayback); fMenuParallelPlayback->SetMarked(fAllowParallelPlayback); @@ -342,7 +338,6 @@ MainWindow::MessageReceived(BMessage* message) _TogglePlay(fActiveStations.LastItem()); } break; - } case B_ABOUT_REQUESTED: be_app->AboutRequested(); diff --git a/source/Makefile b/source/Makefile index ac2b490..8ca8df8 100644 --- a/source/Makefile +++ b/source/Makefile @@ -8,7 +8,7 @@ ## file:///system/develop/documentation/makefile-engine.html # The name of the binary. -NAME = ../../dist/StreamRadio +NAME = StreamRadio # The type of binary, must be one of: # APP: Application @@ -17,6 +17,10 @@ NAME = ../../dist/StreamRadio # DRIVER: Kernel driver TYPE = APP +ARCHITECTURES="all !x86_gcc2" +SECONDARY_ARCHITECTURES="x86" +ARCH=$(shell uname -m) + # If you plan to use localization, specify the application's MIME signature. APP_MIME_SIG = application/x-vnd.Fishpond-StreamRadio @@ -39,15 +43,12 @@ SRCS = \ RadioSettings.cpp \ Station.cpp \ StationFinder.cpp \ - StationFinderListenLive.cpp \ StationFinderRadioNetwork.cpp \ StationListView.cpp \ StationPanel.cpp \ StreamIO.cpp \ StreamPlayer.cpp \ - Utils.cpp \ - - + Utils.cpp # Specify the resource definition files to use. Full or relative paths can be # used. @@ -124,7 +125,7 @@ SYMBOLS := # Includes debug information, which allows the binary to be debugged easily. # If set to "TRUE", debug info will be created. -DEBUGGER := +DEBUGGER := TRUE # Specify any additional compiler flags to be used. COMPILER_FLAGS = @@ -135,15 +136,23 @@ LINKER_FLAGS = # Specify the version of this binary. Example: # -app 3 4 0 d 0 -short 340 -long "340 "`echo -n -e '\302\251'`"1999 GNU GPL" # This may also be specified in a resource. -APP_VERSION := - -# (Only used when "TYPE" is "DRIVER"). Specify the desired driver install -# location in the /dev hierarchy. Example: -# DRIVER_PATH = video/usb -# will instruct the "driverinstall" rule to place a symlink to your driver's -# binary in ~/add-ons/kernel/drivers/dev/video/usb, so that your driver will -# appear at /dev/video/usb when loaded. The default is "misc". -DRIVER_PATH = +APP_VERSION := 1.0.1-1 + + +TARGET_DIR := ../dist +PACKAGE_DIR = $(TARGET_DIR)/package +PACKAGE_NAME = $(TARGET_DIR)/$(NAME)-$(APP_VERSION)-$(ARCH).hpkg + +package:: default + + rm -rf $(PACKAGE_DIR) + mkdir -p $(PACKAGE_DIR)/apps + mkdir -p $(PACKAGE_DIR)/data/deskbar/menu/Applications + cp $(TARGET) $(PACKAGE_DIR)/apps + ln -s ../../../../apps/$(NAME) $(PACKAGE_DIR)/data/deskbar/menu/Applications/$(NAME) + cat ./PackageInfo | sed 's/$$VERSION/$(APP_VERSION)/' | sed 's/$$ARCH/$(ARCH)/' > $(TARGET_DIR)/PackageInfo_$(ARCH) + package create -C $(PACKAGE_DIR) -i $(TARGET_DIR)/PackageInfo_$(ARCH) $(PACKAGE_NAME) + copyattr -n BEOS:ICON $(TARGET) $(PACKAGE_NAME) ## Include the Makefile-Engine DEVEL_DIRECTORY := \ diff --git a/source/PackageInfo b/source/PackageInfo new file mode 100644 index 0000000..618e10f --- /dev/null +++ b/source/PackageInfo @@ -0,0 +1,38 @@ +name streamradio +version 1.0.1-1 +architecture x86_64 +summary "Find and listen to internet radio stations" +description "StreamRadio helps you find and listen to internet radio streams." +packager "Builder mmlr_x86_64 " +vendor "Haiku Project" +licenses { + "GNU GPL v3" +} +copyrights { + "2017, 2026 Kai Niessen" + "2018-2026 HaikuArchives Team" +} +provides { + streamradio = 1.0.1 + app:streamradio = 1.0.1 +} +requires { + haiku >= r1~beta6_hrev59866_79-1 + lib:libstdc++ >= 6.0.32 + lib:libxml2 >= 16 +} +urls { + "https://github.com/HaikuArchives/StreamRadio" +} +global-writable-files { + "settings/Stations" +} +user-settings-files { + "settings/StreamRadio.settings" +} +source-urls { +# Download + "https://github.com/HaikuArchives/StreamRadio/archive/56a6422bc2a9148fb15b61342a29e9fa0d34efc1.tar.gz" +# Location 2 + "https://ports-mirror.haiku-os.org/streamradio/56a6422bc2a9148fb15b61342a29e9fa0d34efc1.tar.gz" +} diff --git a/source/RadioApp.cpp b/source/RadioApp.cpp index 9951305..0e332bb 100644 --- a/source/RadioApp.cpp +++ b/source/RadioApp.cpp @@ -21,7 +21,6 @@ #include "RadioApp.h" #include "StationFinder.h" -#include "StationFinderListenLive.h" #include "StationFinderRadioNetwork.h" @@ -107,7 +106,9 @@ RadioApp::AboutRequested() { BAboutWindow* about = new BAboutWindow(B_TRANSLATE_SYSTEM_NAME("StreamRadio"), kAppSignature); - const char* kAuthors[] = {"Fishpond", "Humdinger", "Jacob Secunda", "Javier Steinaker", NULL}; + const char* kAuthors[] = {"Fishpond", "Humdinger", "Jacob Secunda", + "Javier Steinaker", "Adrien Destugues", + "Máximo Castañeda", "Korli", "Jérôme Duval", NULL}; const char* kCopyright = "The HaikuArchives team"; @@ -127,13 +128,7 @@ main(int argc, char* argv[]) { StationFinderRadioNetwork::RegisterSelf(); - // FIXME: "listenlive.eu" no longer seems to exist, though it looks like - // "radiomap.eu" might be its successor? This plugin crashes after searching - // anyway... - // StationFinderListenLive::RegisterSelf(); - new RadioApp(); be_app->Run(); - delete be_app; } diff --git a/source/Station.cpp b/source/Station.cpp index 1c9b763..7b151ac 100644 --- a/source/Station.cpp +++ b/source/Station.cpp @@ -190,7 +190,7 @@ Station::Save() icon->AddChild(canvas); canvas->LockLooper(); - canvas->DrawBitmap(fLogo, fLogo->Bounds(), icon->Bounds()); + canvas->DrawBitmap(fLogo, fLogo->Bounds(), canvas->Bounds(), B_FILTER_BITMAP_BILINEAR); canvas->UnlockLooper(); icon->RemoveChild(canvas); stationInfo.SetIcon(icon, B_LARGE_ICON); @@ -226,7 +226,7 @@ Station::RetrieveStreamUrl() status_t status = B_ERROR; BString contentType("*/*"); - BMallocIO* plsData = HttpUtils::GetAll(fSource, NULL, 100000, &contentType, 2000); + BMallocIO* plsData = HttpUtils::GetAll(&fSource, NULL, 100000, &contentType, 2000); if (plsData != NULL) { status = ParseUrlReference((const char*)plsData->Buffer(), fSource); delete plsData; @@ -272,8 +272,9 @@ Station::Probe() // of streams using HTTPS and load balancing between two or more different // IP's should be small, anyway. - if (fStreamUrl.Protocol() == "https") { - buffer = HttpUtils::GetAll(fStreamUrl, &headers, 2 * 1000 * 1000, &contentType, 4096); + //if (fStreamUrl.Protocol() == "https") { + buffer = HttpUtils::GetAll(&fStreamUrl, &headers, 2 * 1000 * 1000, &contentType, 4096); + /* } else { BUrl resolvedUrl; status_t resolveStatus = HttpUtils::CheckPort(fStreamUrl, &resolvedUrl); @@ -282,7 +283,7 @@ Station::Probe() buffer = HttpUtils::GetAll(resolvedUrl, &headers, 2 * 1000 * 1000, &contentType, 4096); } - + */ #ifdef DEBUGGING for (int32 i = 0; i < headers.CountHeaders(); i++) TRACE("Header: %s\r\n", headers.HeaderAt(i).Header()); @@ -565,7 +566,7 @@ Station::LoadIndirectUrl(BString& shoutCastUrl) BString contentType("*/*"); - BMallocIO* dataIO = HttpUtils::GetAll(url, NULL, 10000, &contentType, 2000); + BMallocIO* dataIO = HttpUtils::GetAll(&url, NULL, 10000, &contentType, 2000); if (dataIO == NULL) return NULL; @@ -615,7 +616,7 @@ Station::LoadIndirectUrl(BString& shoutCastUrl) shoutCastUrl.RemoveCharsSet("#?"); finalUrl.SetUrlString(shoutCastUrl, true); - dataIO = HttpUtils::GetAll(finalUrl); + dataIO = HttpUtils::GetAll(&finalUrl); if (dataIO != NULL) { dataIO->Write(&"", 1); body = (char*)dataIO->Buffer(); @@ -633,7 +634,7 @@ Station::LoadIndirectUrl(BString& shoutCastUrl) contentType = "image/*"; - BMallocIO* iconIO = HttpUtils::GetAll(finalUrl, NULL, 10000, &contentType, 2000); + BMallocIO* iconIO = HttpUtils::GetAll(&finalUrl, NULL, 10000, &contentType, 2000); if (iconIO != NULL) { iconIO->Seek(0, SEEK_SET); station->fLogo = BTranslationUtils::GetBitmap(iconIO); diff --git a/source/StationFinder.cpp b/source/StationFinder.cpp index 8e95613..2a1b85b 100644 --- a/source/StationFinder.cpp +++ b/source/StationFinder.cpp @@ -7,7 +7,6 @@ #include #include #include -#include "HttpUtils.h" #include "RadioApp.h" #include "Utils.h" @@ -16,100 +15,31 @@ #define B_TRANSLATION_CONTEXT "StationFinder" -std::vector > StationFinderServices::sServices; - - -StationFinderServices::~StationFinderServices() -{ - sServices.clear(); -} - - -void -StationFinderServices::Register(char* serviceName, InstantiateFunc instantiate) -{ - std::pair service(serviceName, instantiate); - sServices.push_back(service); -} - - -int32 -StationFinderServices::CountItems() -{ - return sServices.size(); -} - - -StationFinderService* -StationFinderServices::Instantiate(char* name) +FindByCapability::FindByCapability(const char* name) + : fKeywords(true) { - for (uint32 i = 0; i < sServices.size(); i++) { - std::pair service = sServices[i]; - if (!strcmp(service.first, name)) - return service.second(); - } - - return NULL; + fName.SetTo(name); } -char* -StationFinderServices::Name(int i) +FindByCapability::FindByCapability(const char* name, BStringList* keyWords) + : FindByCapability(name) { - return sServices[i].first; + fKeywords.Add(*keyWords); } - -FindByCapability::FindByCapability(char* name) - : fName(name), - fKeywords() +FindByCapability::FindByCapability(const char* name, char* keyWords, char* delimiter) + : FindByCapability(name) { + BString tmp(keyWords); + tmp.Split(delimiter, true, fKeywords); } - -FindByCapability::FindByCapability(char* name, char* keyWords, char* delimiter) - : fName(name), - fKeywords() -{ - SetKeyWords(keyWords, delimiter); -} - - FindByCapability::~FindByCapability() { fKeywords.MakeEmpty(); } - -bool -FindByCapability::HasKeyWords() -{ - return !fKeywords.IsEmpty(); -} - - -const BStringList* -FindByCapability::KeyWords() -{ - return &fKeywords; -} - - -const char* -FindByCapability::Name() -{ - return fName.String(); -} - - -void -FindByCapability::SetKeyWords(char* keyWords, char* delimiter) -{ - BString tmp(keyWords); - tmp.Split(delimiter, true, fKeywords); -} - - StationFinderService::StationFinderService() : serviceName("unknown"), serviceHomePage("", true), @@ -127,14 +57,14 @@ StationFinderService::~StationFinderService() void -StationFinderService::Register(char* name, InstantiateFunc instantiate) +StationFinderService::Register(BString* name, InstantiateFunc instantiate) { - StationFinderServices::Register(name, instantiate); + stationFinderServices.Register(name, instantiate); } BBitmap* -StationFinderService::RetrieveLogo(BUrl url) +StationFinderService::RetrieveLogo(BUrl* url) { BBitmap* bm = NULL; BString contentType("image/*"); @@ -142,7 +72,7 @@ StationFinderService::RetrieveLogo(BUrl url) BMallocIO* bmData = HttpUtils::GetAll(url, NULL, 3000, &contentType); if (bmData != NULL) { bm = BTranslationUtils::GetBitmap(bmData); - if (bm != NULL && bm->InitCheck() != B_OK) { + if (bm != NULL && (bm->InitCheck() != B_OK || !bm->IsValid())) { delete bm; bm = NULL; } @@ -154,23 +84,25 @@ StationFinderService::RetrieveLogo(BUrl url) } -FindByCapability* -StationFinderService::RegisterSearchCapability(char* name) +uint32 +StationFinderService::RegisterSearchCapability(const char* name) { FindByCapability* newCapability = new FindByCapability(name); - findByCapabilities.AddItem(newCapability); - - return newCapability; + if (findByCapabilities.AddItem(newCapability)) + return findByCapabilities.IndexOf(newCapability); + else + return -1; } -FindByCapability* -StationFinderService::RegisterSearchCapability(char* name, char* keywords, char* delimiter) +uint32 +StationFinderService::RegisterSearchCapability(const char* name, BStringList* keywords) { - FindByCapability* newCapability = new FindByCapability(name, keywords, delimiter); - findByCapabilities.AddItem(newCapability); - - return newCapability; + FindByCapability* newCapability = new FindByCapability(name, keywords); + if (findByCapabilities.AddItem(newCapability)) + return findByCapabilities.IndexOf(newCapability); + else + return -1; } @@ -193,11 +125,11 @@ StationFinderWindow::StationFinderWindow(BWindow* parent) fDdServices = new BOptionPopUp("fDdServices", NULL, new BMessage(MSG_SELECT_SERVICE)); int currentServiceIndex = 0; const char* settingsServiceName = ((RadioApp*)be_app)->Settings.StationFinderName(); - for (int32 i = 0; i < StationFinderServices::CountItems(); i++) { - const char* serviceName = StationFinderServices::Name(i); - if (settingsServiceName && !strcmp(serviceName, settingsServiceName)) + for (int32 i = 0; i < stationFinderServices.CountItems(); i++) { + BString* serviceName = stationFinderServices.Name(i); + if (settingsServiceName && serviceName->Compare(settingsServiceName)) currentServiceIndex = i; - fDdServices->AddOption(serviceName, i); + fDdServices->AddOption(serviceName->String(), i); } fBnVisit = new BButton("fBnVisit", "", new BMessage(MSG_VISIT_SERVICE)); @@ -378,22 +310,21 @@ StationFinderWindow::MessageReceived(BMessage* msg) void StationFinderWindow::SelectService(int index) { - char* serviceName = StationFinderServices::Name(index); + BString* serviceName = stationFinderServices.Name(index); if (serviceName == NULL) return; - StationFinderService* selectedService = StationFinderServices::Instantiate(serviceName); - if (selectedService == NULL) + if (fCurrentService != NULL && *fCurrentService->Name() == *serviceName) return; - if (fCurrentService != NULL) { - delete fCurrentService; - fCurrentService = NULL; - } + StationFinderService* selectedService = stationFinderServices.Instantiate(index); + if (selectedService == NULL) + return; + delete fCurrentService; fCurrentService = selectedService; - ((RadioApp*)be_app)->Settings.SetStationFinderName(serviceName); + ((RadioApp*)be_app)->Settings.SetStationFinderName(serviceName->String()); while (fDdSearchBy->CountOptions() > 0) fDdSearchBy->RemoveOptionAt(0); diff --git a/source/StationFinder.h b/source/StationFinder.h index 177e341..9b69864 100644 --- a/source/StationFinder.h +++ b/source/StationFinder.h @@ -23,7 +23,7 @@ #include #include -#include +#include #include #include #include @@ -58,34 +58,49 @@ typedef BObjectList StationList; typedef class StationFinderService* (*InstantiateFunc)(); - -class StationFinderServices { +class StationFinderEntry { public: - StationFinderServices(){}; - ~StationFinderServices(); - - static void Register(char* serviceName, InstantiateFunc); - - static StationFinderService* Instantiate(char* s); + StationFinderEntry() { }; + StationFinderEntry(BString* name, InstantiateFunc instantiateFunc) + : Name(name->String()) + { + this->instantiate = instantiateFunc; + } + BString Name; + StationFinderService* Get() { return instance != NULL ? instance : instantiate(); } +private: + InstantiateFunc instantiate; + StationFinderService* instance = NULL; +}; - static int32 CountItems(); - static char* Name(int i); +class StationFinderServices : public BObjectList { +public: + StationFinderServices() : BObjectList(32) {}; + ~StationFinderServices() { MakeEmpty(); }; + + void Register(BString* serviceName, InstantiateFunc instantiate) { + AddItem(new StationFinderEntry(serviceName, instantiate)); + } + BString* Name(int index) { return &ItemAt(index)->Name; } + StationFinderService* Instantiate(int index) { return ItemAt(index)->Get(); } + int32 IndexOf(BString* name) { return BinarySearchIndexByKey(*name, CompareName); } private: - static std::vector > sServices; + static int CompareName(const BString* name, const StationFinderEntry* entry) { return name->Compare(entry->Name); } }; +static StationFinderServices stationFinderServices; + class FindByCapability { public: - FindByCapability(char* name); - FindByCapability(char* name, char* keyWords, char* delimiter); + FindByCapability(const char* name); + FindByCapability(const char* name, BStringList* keyWords); + FindByCapability(const char* name, char* keyWords, char* delimiter); ~FindByCapability(); - bool HasKeyWords(); - void SetKeyWords(char* keyWords, char* delimiter); - const BStringList* KeyWords(); - - const char* Name(); + bool HasKeyWords() { return !fKeywords.IsEmpty(); } + const BStringList* KeyWords() { return &fKeywords; } + const char* Name() { return fName.String(); } private: BString fName; @@ -97,67 +112,71 @@ class StationFinderService { public: // Overridden in specific StationFinder implementations - StationFinderService(); - virtual ~StationFinderService(); + StationFinderService(); + virtual ~StationFinderService(); - static void RegisterSelf(); - static StationFinderService* Instantiate(); + static void RegisterSelf(); + static StationFinderService* Instantiate(); - virtual StationList* FindBy( - int capabilityIndex, const char* searchFor, BLooper* resultUpdateTarget) - = 0; + virtual StationList* FindBy( + int capabilityIndex, const char* searchFor, BLooper* resultUpdateTarget) + = 0; // Provided by ancestor class - const char* Name() const { return serviceName.String(); } + BString* Name() + { return &serviceName; } - int CountCapabilities() const { return findByCapabilities.CountItems(); } - FindByCapability* Capability(int index) const { return findByCapabilities.ItemAt(index); } + int CountCapabilities() const + { return findByCapabilities.CountItems(); } + FindByCapability* Capability(int index) const + { return findByCapabilities.ItemAt(index); } - static void Register(char* name, InstantiateFunc instantiate); + static void Register(BString* name, InstantiateFunc instantiate); protected: // To be filled by specific StationFinder implementations - BString serviceName; - BUrl serviceHomePage; - BBitmap* serviceLogo; + BString serviceName; + BUrl serviceHomePage; + BBitmap* serviceLogo; #if B_HAIKU_VERSION > B_HAIKU_VERSION_1_BETA_5 - BObjectList findByCapabilities; + BObjectList + findByCapabilities; #else - BObjectList findByCapabilities; + BObjectList findByCapabilities; #endif // Helper functions - BBitmap* RetrieveLogo(BUrl url); - FindByCapability* RegisterSearchCapability(char* name); - FindByCapability* RegisterSearchCapability(char* name, char* keyWords, char* delimiter); + BBitmap* RetrieveLogo(BUrl* url); + uint32 RegisterSearchCapability(const char* name); + uint32 RegisterSearchCapability(const char* name, BStringList* keyWords); }; class StationFinderWindow : public BWindow { public: - StationFinderWindow(BWindow* parent); - virtual ~StationFinderWindow(); + StationFinderWindow(BWindow* parent); + virtual ~StationFinderWindow(); - void MessageReceived(BMessage* msg); - virtual bool QuitRequested(); + void MessageReceived(BMessage* msg); + virtual bool QuitRequested(); - void SelectService(int index); - void SelectCapability(int index); - void DoSearch(const char* text); + void SelectService(int index); + void SelectCapability(int index); + void DoSearch(const char* text); private: - StationFinderService* fCurrentService; - - BMessenger* fMessenger; - BTextControl* fTxSearch; - BOptionPopUp* fKwSearch; - BButton* fBnSearch; - BOptionPopUp* fDdServices; - BButton* fBnVisit; - BOptionPopUp* fDdSearchBy; - StationListView* fResultView; - BButton* fBnAdd; - - BGridLayout* fSearchGrid; + StationFinderService* fCurrentService; + + BMessenger* fMessenger; + BTextControl* fTxSearch; + BOptionPopUp* fKwSearch; + BButton* fBnSearch; + BOptionPopUp* fDdServices; + BButton* fBnVisit; + BOptionPopUp* fDdSearchBy; + StationListView* fResultView; + BButton* fBnAdd; + + BGridLayout* fSearchGrid; }; diff --git a/source/StationFinderListenLive.cpp b/source/StationFinderListenLive.cpp deleted file mode 100644 index 0e68ce8..0000000 --- a/source/StationFinderListenLive.cpp +++ /dev/null @@ -1,415 +0,0 @@ -/* - * Copyright (C) 2017 Kai Niessen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - - -#include "StationFinderListenLive.h" - -#define __USE_GNU -#include - -#include - -#include "Debug.h" - - -#undef B_TRANSLATION_CONTEXT -#define B_TRANSLATION_CONTEXT "StationFinderListenLive" - - -const char* kGenreList - = "Top 40|top40\r\ -Hot Adult Contemporary|hotac\r\ -Adult Contemporary|ac\r\ -Oldies/80s/90s|oldies\r\ -Country music|country\r\ -RnB/HipHop|rnb\r\ -Dance/Techno|dance\r\ -Rock/Classic Rock|rock\r\ -Alternative|alternative\r\ -Chillout/Lounge|chillout"; - -const char* kCountryList - = "Andorra|andorra\r\ -Armenia|armenia\r\ -Austria|austria\r\ -Azerbaijan|azerbaijan\r\ -Belarus|belarus\r\ -Belgium|belgium\r\ -Bosnia-Herzegovina|bosnia\r\ -Bulgaria|bulgaria\r\ -Croatia|croatia\r\ -Cyprus|cyprus\r\ -Czech Republic|czech-republic\r\ -Denmark|denmark\r\ -Estonia|estonia\r\ -Faroe Islands|faroe\r\ -Finland|finland\r\ -France|france\r\ -Georgia|georgia\r\ -Germany|germany\r\ -Gibraltar|gibraltar\r\ -Greece|greece\r\ -Hungary|hungary\r\ -Iceland|iceland\r\ -Ireland|ireland\r\ -Italy|italy\r\ -Kosovo|kosovo\r\ -Latvia|latvia\r\ -Liechtenstein|liechtenstein\r\ -Lithuania|lithuania\r\ -Luxembourg|luxembourg\r\ -Macedonia|macedonia\r\ -Malta|malta\r\ -Moldova|moldova\r\ -Monaco|monaco\r\ -Montenegro|montenegro\r\ -Netherlands|netherlands\r\ -Norway|norway\r\ -Poland|poland\r\ -Portugal|portugal\r\ -Romania|romania\r\ -Russia|russia\r\ -San Marino|san-marino\r\ -Serbia|serbia\r\ -Slovakia|slovakia\r\ -Slovenia|slovenia\r\ -Spain|spain\r\ -Sweden|sweden\r\ -Switzerland|switzerland\r\ -Turkey|turkey\r\ -Ukraine|ukraine\r\ -United Kingdom|uk\r\ -Vatican State|vatican"; - -const char* StationFinderListenLive::kBaseUrl = "http://listenlive.eu/"; - - -StationFinderListenLive::StationFinderListenLive() - : StationFinderService(), - fCountryKeywordAndPath(), - fGenreKeywordAndPath(), - fLookupThread(-1), - fPlsLookupList() -{ - serviceName.SetTo(B_TRANSLATE("listenlive.eu [experimental]")); - serviceHomePage.SetUrlString("http://www.listenlive.eu/", true); - - BString tmp(kCountryList); - tmp.Split("\r", true, fCountryKeywordAndPath); - FindByCapability* findByCountry = RegisterSearchCapability("Country"); - BStringList* keywords = (BStringList*)findByCountry->KeyWords(); - for (int32 i = 0; i < fCountryKeywordAndPath.CountStrings(); i++) { - BString keyword(fCountryKeywordAndPath.StringAt(i)); - keyword.Truncate(keyword.FindFirst('|')); - keywords->Add(keyword); - } - - tmp.SetTo(kGenreList); - tmp.Split("\r", true, fGenreKeywordAndPath); - FindByCapability* findByGenre = RegisterSearchCapability("Genre"); - keywords = (BStringList*)findByGenre->KeyWords(); - for (int32 i = 0; i < fGenreKeywordAndPath.CountStrings(); i++) { - BString keyword(fGenreKeywordAndPath.StringAt(i)); - keyword.Truncate(keyword.FindFirst('|')); - keywords->Add(keyword); - } -} - - -StationFinderService* -StationFinderListenLive::Instantiate() -{ - return new StationFinderListenLive(); -} - - -void -StationFinderListenLive::RegisterSelf() -{ - Register("listenlive.eu [experimental]", &StationFinderListenLive::Instantiate); -} - - -StationFinderListenLive::~StationFinderListenLive() -{ - fPlsLookupList.MakeEmpty(false); - if (fLookupThread >= 0) { - status_t dummy; - wait_for_thread(fLookupThread, &dummy); - } -} - - -StationList* -StationFinderListenLive::FindBy( - int capabilityIndex, const char* searchFor, BLooper* resultUpdateTarget) -{ - if (fLookupThread >= 0) - suspend_thread(fLookupThread); - - fPlsLookupList.MakeEmpty(false); - fLookupNotify = resultUpdateTarget; - - StationList* result = NULL; - - BString urlString(kBaseUrl); - strlwr((char*)searchFor); - int keywordIndex = Capability(capabilityIndex)->KeyWords()->IndexOf(searchFor); - BStringList* keywordAndPaths - = capabilityIndex ? &fGenreKeywordAndPath : &fCountryKeywordAndPath; - BString path(keywordAndPaths->StringAt(keywordIndex)); - path.Remove(0, path.FindFirst('|') + 1); - urlString << path << ".html"; - BUrl url(urlString, true); - - BMallocIO* data = HttpUtils::GetAll(url); - if (data != NULL) { - switch (capabilityIndex) { - case 0: // findByCountry - result = ParseCountryReturn(data, searchFor); - break; - - case 1: // findByGenre - result = ParseGenreReturn(data, searchFor); - break; - } - } - - data->Flush(); - delete data; - - if (!fPlsLookupList.IsEmpty()) { - if (fLookupThread < 0) - fLookupThread = spawn_thread(&_PlsLookupFunc, "plslookup", B_NORMAL_PRIORITY, this); - resume_thread(fLookupThread); - } - - return result; -} - - -StationList* -StationFinderListenLive::ParseCountryReturn(BMallocIO* data, const char* searchFor) -{ -#if B_HAIKU_VERSION > B_HAIKU_VERSION_1_BETA_5 - StationList* result = new StationList(20); -#else - StationList* result = new StationList(20, true); -#endif - if (result == NULL) - return result; - - regmatch_t matches[10]; - regex_t regex; - - // - // ORF Ö-1 - // Vienna - // Windows
-	// Media
MP3 - // 128 Kbps
192 Kbps - // Information/Culture - // - - int res = regcomp(®ex, - "\\s*\\s*([^<]*)\\s*\\s*\\s*" - "([^<]*)\\s*" - "([^<]|<[^/]|\\s*" - "\\s*([^<]*)([^<]|<[^/]|\\s*" - "([^<]*)\\s*", - RE_ICASE | RE_DOT_NEWLINE | RE_DOT_NOT_NULL | RE_NO_BK_PARENS | RE_NO_BK_VBAR - | RE_BACKSLASH_ESCAPE_IN_LISTS); - - char* doc = (char*)data->Buffer(); - off_t size; - data->GetSize(&size); - doc[size] = 0; - - while ((res = regexec(®ex, doc, 10, matches, 0)) == 0) { - doc[matches[1].rm_eo] = 0; - doc[matches[2].rm_eo] = 0; - doc[matches[3].rm_eo] = 0; - doc[matches[5].rm_eo] = 0; - doc[matches[6].rm_eo] = 0; - doc[matches[8].rm_eo] = 0; - printf("Station %s at %s in %s address %s, data rate=%s, genre=%s\n", - doc + matches[2].rm_so, doc + matches[1].rm_so, doc + matches[3].rm_so, - doc + matches[5].rm_so, doc + matches[6].rm_so, doc + matches[8].rm_so); - - Station* station = new Station(doc + matches[2].rm_so, NULL); - if (station == NULL) - continue; - - result->AddItem(station); - - BString source(doc + matches[5].rm_so); - if (source.EndsWith(".pls") || source.EndsWith(".m3u")) { - station->SetSource(BUrl(source, true)); - fPlsLookupList.AddItem(station); - } else - station->SetStreamUrl(BUrl(source, true)); - - for (int32 i = matches[6].rm_so; i < matches[6].rm_eo; i++) { - if (!strchr("0123456789.", doc[i])) { - doc[i] = 0; - station->SetBitRate(atof(doc + matches[6].rm_so) * 1000); - break; - } - } - - station->SetStation(doc + matches[1].rm_so); - station->SetGenre(doc + matches[8].rm_so); - - BString country; - country.SetTo(searchFor); - country.Append(" - "); - country.Append(doc + matches[3].rm_so); - station->SetCountry(country); - - doc += matches[0].rm_eo; - } - - return result; -} - - -StationList* -StationFinderListenLive::ParseGenreReturn(BMallocIO* data, const char* searchFor) -{ -#if B_HAIKU_VERSION > B_HAIKU_VERSION_1_BETA_5 - StationList* result = new StationList(20); -#else - StationList* result = new StationList(20, true); -#endif - if (result == NULL) - return result; - - regmatch_t matches[9]; - regex_t regex; - - // - // ORF Ö-1 - // Vienna - // Windows
-	// Media
MP3 - // 128 Kbps
192 Kbps - // Information/Culture - // - - // - // Radio - // Stephansdom Vienna Austria MP3
32 Kbps, 56 Kbps
128 Kbps - // - - int res = regcomp(®ex, - "\\s*\\s*([^<]*)\\s*\\s*\\s*" - "([^<]*)\\s*" - "([^<]*)\\s*" - "([^<]|<[^/]|\\s*" - "\\s*([^<]*)([^<]|<[^/]|\\s*" - "", - RE_ICASE | RE_DOT_NEWLINE | RE_DOT_NOT_NULL | RE_NO_BK_PARENS | RE_NO_BK_VBAR - | RE_BACKSLASH_ESCAPE_IN_LISTS); - - char* doc = (char*)data->Buffer(); - off_t size; - data->GetSize(&size); - doc[size] = 0; - - while ((res = regexec(®ex, doc, 9, matches, 0)) == 0) { - doc[matches[1].rm_eo] = 0; - doc[matches[2].rm_eo] = 0; - doc[matches[3].rm_eo] = 0; - doc[matches[4].rm_eo] = 0; - doc[matches[6].rm_eo] = 0; - doc[matches[7].rm_eo] = 0; - printf("Station %s at %s in %s - %s address %s, data rate=%s, genre=%s\n", - doc + matches[2].rm_so, doc + matches[1].rm_so, doc + matches[3].rm_so, - doc + matches[4].rm_so, doc + matches[6].rm_so, doc + matches[7].rm_so, searchFor); - - Station* station = new Station(doc + matches[2].rm_so, NULL); - if (station == NULL) - continue; - - result->AddItem(station); - - BString source(doc + matches[6].rm_so); - if (source.EndsWith(".pls") || source.EndsWith(".m3u")) { - station->SetSource(BUrl(source, true)); - fPlsLookupList.AddItem(station); - } else - station->SetStreamUrl(BUrl(source, true)); - - for (int32 i = matches[7].rm_so; i < matches[7].rm_eo; i++) { - if (!strchr("0123456789.", doc[i])) { - doc[i] = 0; - station->SetBitRate(atof(doc + matches[7].rm_so) * 1000); - break; - } - } - - station->SetStation(doc + matches[1].rm_so); - station->SetGenre(searchFor); - - BString country(doc + matches[4].rm_so); - country.Append(" - "); - country.Append(doc + matches[5].rm_so); - station->SetCountry(country); - - doc += matches[0].rm_eo; - } - - return result; -} - - -int32 -StationFinderListenLive::_PlsLookupFunc(void* data) -{ - StationFinderListenLive* _this = (StationFinderListenLive*)data; - while (!_this->fPlsLookupList.IsEmpty()) { - Station* station = _this->fPlsLookupList.FirstItem(); - TRACE("Looking up stream URL for station %s in %s\n", station->Name()->String(), - station->Source().UrlString().String()); - - Station* plsStation = Station::LoadIndirectUrl((BString&)station->Source().UrlString()); - if (plsStation != NULL) - station->SetStreamUrl(plsStation->StreamUrl()); - - BMessage* notification = new BMessage(MSG_UPDATE_STATION); - notification->AddPointer("station", station); - if (_this->fLookupNotify->LockLooper()) { - _this->fLookupNotify->PostMessage(notification); - _this->fLookupNotify->UnlockLooper(); - } - - _this->fPlsLookupList.RemoveItem(station, false); - } - - _this->fLookupThread = -1; - return B_OK; -} diff --git a/source/StationFinderListenLive.h b/source/StationFinderListenLive.h deleted file mode 100644 index ec88183..0000000 --- a/source/StationFinderListenLive.h +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2017 Kai Niessen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#ifndef _STATION_FINDER_LISTEN_LIVE_H -#define _STATION_FINDER_LISTEN_LIVE_H - - -#include - -#include "StationFinder.h" - - -class StationFinderListenLive : public StationFinderService { -public: - StationFinderListenLive(); - virtual ~StationFinderListenLive(); - - static void RegisterSelf(); - static StationFinderService* Instantiate(); - - virtual StationList* FindBy( - int capabilityIndex, const char* searchFor, BLooper* resultUpdateTarget); - - StationList* ParseCountryReturn(BMallocIO* data, const char* country); - StationList* ParseGenreReturn(BMallocIO* data, const char* genre); - -private: - static int32 _PlsLookupFunc(void* data); - -private: - static const char* kBaseUrl; - - BStringList fCountryKeywordAndPath; - BStringList fGenreKeywordAndPath; - thread_id fLookupThread; - BObjectList fPlsLookupList; - - BLooper* fLookupNotify; -}; - - -#endif // _STATION_FINDER_LISTEN_LIVE_H diff --git a/source/StationFinderRadioNetwork.cpp b/source/StationFinderRadioNetwork.cpp index d44eeed..3a3d9c5 100644 --- a/source/StationFinderRadioNetwork.cpp +++ b/source/StationFinderRadioNetwork.cpp @@ -22,20 +22,20 @@ #include #include - #include +#include + + -#include "Debug.h" #include "HttpUtils.h" #undef B_TRANSLATION_CONTEXT #define B_TRANSLATION_CONTEXT "StationFinderRadioNetwork" +BString StationFinderRadioNetwork::sBaseUrl("https://de1.api.radio-browser.info/"); -const char* StationFinderRadioNetwork::kBaseUrl = "https://all.api.radio-browser.info/"; - -BString StationFinderRadioNetwork::sCachedServerUrl = B_EMPTY_STRING; +const char* StationFinderRadioNetwork::serviceNameInternal = "Community Radio Browser"; IconLookup::IconLookup(Station* station, BUrl iconUrl) @@ -45,6 +45,33 @@ IconLookup::IconLookup(Station* station, BUrl iconUrl) } +BStringList* +StationFinderRadioNetwork::_GetKeywords(const char* path, int32 minStations) { + BString keywordUrlString(sBaseUrl); + keywordUrlString.Append(path); + BUrl keywordUrl(keywordUrlString, false); + + BMessage* parsedData = HttpUtils::GetMsgFromREST(&keywordUrl); + if (parsedData != NULL) { + BStringList* keywords = new BStringList(true); + char* name; + uint32 type; + int32 count; + for (int32 index = 0; parsedData->GetInfo(B_MESSAGE_TYPE, index, &name, &type, &count) == B_OK; index++) { + BMessage keywordMessage; + if (parsedData->FindMessage(name, &keywordMessage) == B_OK) { + const char* keyword = keywordMessage.FindString("name"); + double stationCount = keywordMessage.FindDouble("stationcount"); + if (stationCount > minStations) + keywords->Add(keyword); + } + } + delete parsedData; + return keywords; + } + return NULL; +}; + StationFinderRadioNetwork::StationFinderRadioNetwork() : StationFinderService(), fIconLookupThread(-1), @@ -54,16 +81,41 @@ StationFinderRadioNetwork::StationFinderRadioNetwork() fIconLookupList(100, true) #endif { - serviceName.SetTo(B_TRANSLATE("Community Radio Browser")); - serviceHomePage.SetUrlString("https://www.radio-browser.info", true); - - RegisterSearchCapability("Name"); - RegisterSearchCapability("Tag"); - RegisterSearchCapability("Language"); - RegisterSearchCapability("Country"); - RegisterSearchCapability("Country code"); - RegisterSearchCapability("State/Region"); - RegisterSearchCapability("Unique identifier"); + serviceName.SetTo(B_TRANSLATE(serviceNameInternal)); + serviceHomePage.SetUrlString("https://www.radio-browser.info", false); + + BStringList* keywords; + + // Register different search capabilities + searchCapabilityName = RegisterSearchCapability("Name"); + + // Tag Search (dropdown) + keywords = _GetKeywords("json/tags?limit=1000000&hidebroken=true", 20); + if (keywords != NULL) { + searchCapabilityTag = RegisterSearchCapability("Tag", keywords); + delete keywords; + } + + // Language Search (dropdown) + keywords = _GetKeywords("json/languages?limit=1000000&hidebroken=true", 20); + if (keywords != NULL) { + searchCapabilityLanguage = RegisterSearchCapability("Language", keywords); + delete keywords; + } + + keywords = _GetKeywords("json/countries?limit=1000000&hidebroken=true", 10); + if (keywords != NULL) { + searchCapabilityCountry = RegisterSearchCapability("Country", keywords); + delete keywords; + } + + keywords = _GetKeywords("json/codecs?limit=1000000&hidebroken=true", 10); + if (keywords != NULL && !keywords->IsEmpty()) { + searchCapabilityCodec = RegisterSearchCapability("Codec", keywords); + delete keywords; + } + + searchCapabilityUuid = RegisterSearchCapability("Unique identifier"); } @@ -84,7 +136,7 @@ StationFinderRadioNetwork::Instantiate() void StationFinderRadioNetwork::RegisterSelf() { - Register("Community Radio Browser", &StationFinderRadioNetwork::Instantiate); + Register(new BString(B_TRANSLATE(serviceNameInternal)), &StationFinderRadioNetwork::Instantiate); } @@ -100,69 +152,42 @@ StationFinderRadioNetwork::FindBy( StationList* result = new StationList(); if (result == NULL) return result; - - if (_CheckServer() != B_OK) - return result; - - printf("Connected to server: %s\n", sCachedServerUrl.String()); - - BString urlString(sCachedServerUrl); + + BString urlString(sBaseUrl); // Add the format and station section... urlString.Append("json/stations/"); - switch (capabilityIndex) { - case 0: // Name search - urlString.Append("byname/"); - break; - - case 1: // Tag search - urlString.Append("bytag/"); - break; - - case 2: // Language search - urlString.Append("bylanguage/"); - break; - - case 3: // Country search - urlString.Append("bycountry/"); - break; - - case 4: // Country code search - urlString.Append("bycountrycodeexact/"); - break; - - case 5: // State/Region search - urlString.Append("bystate/"); - break; - - case 6: // Unique identifier search - urlString.Append("byuuid/"); - break; - - default: // A very bad kind of search? Just do a name search... - urlString.Append("byname/"); - break; - } + if (capabilityIndex == searchCapabilityName) // Name search + urlString.Append("byname/"); + else if (capabilityIndex == searchCapabilityTag) // Tag search + urlString.Append("bytag/"); + else if (capabilityIndex == searchCapabilityLanguage) // Language search + urlString.Append("bylanguage/"); + else if (capabilityIndex == searchCapabilityCountry) // Country search + urlString.Append("bycountry/"); + else if (capabilityIndex == searchCapabilityCodec) // Codec search + urlString.Append("bycodec/"); + else if (capabilityIndex == searchCapabilityUuid) // Unique identifier search + urlString.Append("byuuid/"); + else // A very bad kind of search? Just do a name search... + urlString.Append("byname/"); BString searchForString(searchFor); searchForString = BUrl::UrlEncode(searchForString, true, true); urlString.Append(searchForString); + urlString.Append("?limit=2000000&hidebroken=true"); BUrl finalUrl(urlString, true); - BMessage parsedData; - BMallocIO* data = HttpUtils::GetAll(finalUrl); - if (data != NULL - && BJson::Parse((const char*)data->Buffer(), data->BufferLength(), parsedData) == B_OK) { - delete data; - + BMessage* parsedData = HttpUtils::GetMsgFromREST(&finalUrl, NULL, 5000); + if (parsedData != NULL) { char* name; uint32 type; int32 count; for (int32 index = 0; - parsedData.GetInfo(B_MESSAGE_TYPE, index, &name, &type, &count) == B_OK; index++) { + parsedData->GetInfo(B_MESSAGE_TYPE, index, &name, &type, &count) == B_OK; index++) { BMessage stationMessage; - if (parsedData.FindMessage(name, &stationMessage) == B_OK) { + if (parsedData->FindMessage(name, &stationMessage) == B_OK) { Station* station = new Station("unknown"); if (station == NULL) continue; @@ -177,11 +202,10 @@ StationFinderRadioNetwork::FindBy( station->SetStation(stationMessage.GetString("homepage", B_EMPTY_STRING)); BString iconUrl; - if (stationMessage.FindString("favicon", &iconUrl) == B_OK) { - if (!iconUrl.IsEmpty()) { - fIconLookupList.AddItem(new IconLookup(station, BUrl(iconUrl, true))); - } - } + + if (stationMessage.FindString("favicon", &iconUrl) == B_OK && !iconUrl.IsEmpty()) + fIconLookupList.AddItem(new IconLookup(station, BUrl(iconUrl, false))); + station->SetGenre(stationMessage.GetString("tags", B_EMPTY_STRING)); @@ -206,13 +230,13 @@ StationFinderRadioNetwork::FindBy( result->AddItem(station); } } + delete parsedData; if (!fIconLookupList.IsEmpty()) { fIconLookupThread = spawn_thread(&_IconLookupFunc, "iconlookup", B_LOW_PRIORITY, this); resume_thread(fIconLookupThread); } } else { - delete data; delete result; result = NULL; } @@ -227,8 +251,8 @@ StationFinderRadioNetwork::_IconLookupFunc(void* data) StationFinderRadioNetwork* _this = (StationFinderRadioNetwork*)data; while (_this->fIconLookupThread >= 0 && !_this->fIconLookupList.IsEmpty()) { IconLookup* item = _this->fIconLookupList.FirstItem(); - BBitmap* logo = _this->RetrieveLogo(item->fIconUrl); - if (logo != NULL) { + BBitmap* logo = _this->RetrieveLogo(&item->fIconUrl); + if (logo != NULL && logo->IsValid()) { item->fStation->SetLogo(logo); BMessage* notification = new BMessage(MSG_UPDATE_STATION); @@ -247,33 +271,6 @@ StationFinderRadioNetwork::_IconLookupFunc(void* data) } -status_t -StationFinderRadioNetwork::_CheckServer() -{ - // Just a quick check up on our cached server...if it exists. - BUrl cachedServerUrl(sCachedServerUrl, true); - if (!sCachedServerUrl.IsEmpty() - && HttpUtils::CheckPort(cachedServerUrl, &cachedServerUrl, 0) == B_OK) { - // It's still there! - return B_OK; - } - - // Try to find an active server! - BUrl testServerUrl(kBaseUrl, true); -#if 0 - status_t result = HttpUtils::CheckPort(testServerUrl, &testServerUrl, 0); - if (result != B_OK) { - // Oh no...this is, uh, pretty bad. - return result; - } -#endif - - // Cache it! - sCachedServerUrl.SetTo(testServerUrl.UrlString()); - - return B_OK; -} - void StationFinderRadioNetwork::_WaitForIconLookupThread() { diff --git a/source/StationFinderRadioNetwork.h b/source/StationFinderRadioNetwork.h index 1195d19..1c49b12 100644 --- a/source/StationFinderRadioNetwork.h +++ b/source/StationFinderRadioNetwork.h @@ -43,15 +43,24 @@ class StationFinderRadioNetwork : public StationFinderService { int capabilityIndex, const char* searchFor, BLooper* resultUpdateTarget); private: + static const char* serviceNameInternal; + + static BStringList* _GetKeywords(const char* path, int32 minStations); static int32 _IconLookupFunc(void* data); - status_t _CheckServer(); void _WaitForIconLookupThread(); private: - static const char* kBaseUrl; - static BString sCachedServerUrl; + static BString sBaseUrl; thread_id fIconLookupThread; + + int searchCapabilityName = -1; + int searchCapabilityTag = -1; + int searchCapabilityLanguage = -1; + int searchCapabilityCountry = -1; + int searchCapabilityCodec = -1; + int searchCapabilityUuid = -1; + #if B_HAIKU_VERSION > B_HAIKU_VERSION_1_BETA_5 BObjectList fIconLookupList; #else diff --git a/source/StreamIO.cpp b/source/StreamIO.cpp index 389cb96..ca6a66a 100644 --- a/source/StreamIO.cpp +++ b/source/StreamIO.cpp @@ -78,10 +78,11 @@ StreamIO::StreamIO(Station* station, BLooper* metaListener) // of streams using HTTPS and load balancing between two or more different // IP's should be small, anyway. - if (url.Protocol() == "https") { + // if (url.Protocol() == "https") { fReq = dynamic_cast( BUrlProtocolRoster::MakeRequest(url.UrlString().String(), this, this)); - } else { + /* + } else { BUrl* newUrl = new BUrl(); if (newUrl == NULL) return; @@ -94,7 +95,7 @@ StreamIO::StreamIO(Station* station, BLooper* metaListener) BUrlProtocolRoster::MakeRequest(newUrl->UrlString().String(), this, this)); delete newUrl; } - + */ if (fReq == NULL) return; @@ -150,7 +151,7 @@ StreamIO::WriteAt(off_t position, const void* buffer, size_t size) ssize_t StreamIO::ReadAt(off_t position, void* buffer, size_t size) { - if (fLimit == 0 || position < fLimit) { + if (fLimit == 0 || (size_t)position < fLimit) { ssize_t read = BAdapterIO::ReadAt(position, buffer, size); if (read > 0) { TRACE("Read %" B_PRIdSSIZE " of %" B_PRIuSIZE " bytes from position %" B_PRIdOFF @@ -256,7 +257,7 @@ StreamIO::_DataWithMetaReceived(const char* data, size_t size, int next) while (size > 0) { if (fUntilMetaEnd != 0) { // We are reading metadata - if (fUntilMetaEnd <= size) { + if ((size_t)fUntilMetaEnd <= size) { // The metadata ends before the buffer memcpy(fMetaBuffer + fMetaSize, (void*)data, fUntilMetaEnd); @@ -283,7 +284,7 @@ StreamIO::_DataWithMetaReceived(const char* data, size_t size, int next) } else { // No metadata right now, feed content to consumer DataFunc nextFunc = fDataFuncs.Item(next); - if (size <= fUntilMetaStart) { + if (size <= (size_t)fUntilMetaStart) { written += (*this.*nextFunc)(data, size, next + 1); fUntilMetaStart -= size; @@ -324,7 +325,7 @@ ssize_t StreamIO::_DataUnsyncedReceived(const char* data, size_t size, int next) { off_t frameStart; - for (frameStart = 0; frameStart < size; frameStart++) { + for (frameStart = 0; frameStart < (off_t)size; frameStart++) { if (fFrameSync == none) { if (data[frameStart] == kMpegHeader1) { fFrameSync = first; @@ -401,7 +402,7 @@ StreamIO::_ProcessMeta() text[matches[1].rm_eo] = 0; text[matches[2].rm_eo] = 0; - if (text + matches[2].rm_so && !(text + matches[2].rm_so)[0]) + if (text[matches[2].rm_so] == 0) msg->AddString(strlwr(text + matches[1].rm_so), fIcyName); else msg->AddString(strlwr(text + matches[1].rm_so), text + matches[2].rm_so); diff --git a/source/StreamPlayer.cpp b/source/StreamPlayer.cpp index 3c66982..6a279d8 100644 --- a/source/StreamPlayer.cpp +++ b/source/StreamPlayer.cpp @@ -46,6 +46,8 @@ StreamPlayer::StreamPlayer(Station* station, BLooper* notify) if (fInitStatus != B_OK) { MSG("Error retrieving stream from %s - %s\n", station->StreamUrl().UrlString().String(), strerror(fInitStatus)); + delete fStream; + fStream = NULL; return; } } @@ -181,7 +183,8 @@ StreamPlayer::_GetDecodedChunk( if (player->fFlushCount++ > 1000) { player->fFlushCount = 0; - player->fStream->FlushRead(); + if (player->fStream) + player->fStream->FlushRead(); } } @@ -189,6 +192,8 @@ StreamPlayer::_GetDecodedChunk( status_t StreamPlayer::_StartPlayThreadFunc(StreamPlayer* _this) { + if (!_this->fStream) return B_ERROR; + _this->Lock(); _this->_SetState(StreamPlayer::Buffering); diff --git a/source/Utils.cpp b/source/Utils.cpp index c4ca259..34f6797 100644 --- a/source/Utils.cpp +++ b/source/Utils.cpp @@ -18,7 +18,6 @@ #include "Utils.h" -#include #include #include #include @@ -28,14 +27,11 @@ #include -static char sUserAgent[32] = {0}; - - BBitmap* Utils::ResourceBitmap(int32 id) { size_t size; - const char* data = (const char*)be_app->AppResources()->FindResource('BBMP', id, &size); + const char* data = (const char*)be_app->AppResources()->LoadResource('BBMP', id, &size); if (size != 0 && data != NULL) { BMessage msg; if (msg.Unflatten(data) == B_OK) @@ -44,30 +40,3 @@ Utils::ResourceBitmap(int32 id) return NULL; } - - -const char* -Utils::UserAgent() -{ - if (sUserAgent[0] != 0) - return sUserAgent; - - app_info appInfo; - be_app->GetAppInfo(&appInfo); - BFile file(&appInfo.ref, B_READ_ONLY); - if (file.InitCheck() == B_OK) { - BAppFileInfo appFileInfo(&file); - struct version_info version; - if (appFileInfo.GetVersionInfo(&version, B_APP_VERSION_KIND) == B_OK) - snprintf(sUserAgent, sizeof(sUserAgent), - "StreamRadio/%" B_PRIu32 ".%" B_PRIu32 ".%" B_PRIu32, version.major, version.middle, - version.minor); - } - - if (sUserAgent[0] == 0) { - // There was an error trying to get the version info - sprintf(sUserAgent, "StreamRadio"); - } - - return sUserAgent; -} diff --git a/source/Utils.h b/source/Utils.h index 6213c64..7600711 100644 --- a/source/Utils.h +++ b/source/Utils.h @@ -30,11 +30,7 @@ class Utils { public: - Utils(){}; - virtual ~Utils(){}; - static BBitmap* ResourceBitmap(int32 id); - static const char* UserAgent(); }; From defca3328afdb4768b028c2b9c5063fbabd65fa4 Mon Sep 17 00:00:00 2001 From: fishpond-haiku Date: Mon, 14 Sep 2026 16:00:06 +0200 Subject: [PATCH 3/3] Re-add resize flag to MainWindow Signed-off-by: fishpond-haiku --- source/MainWindow.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/source/MainWindow.cpp b/source/MainWindow.cpp index c6fbe4f..2b91cd0 100644 --- a/source/MainWindow.cpp +++ b/source/MainWindow.cpp @@ -1,6 +1,6 @@ /* Copyright (C) 2008-2010 Lukas Sommer < SommerLuk at gmail dot com > - Copyright (C) 2017 Kai Niessen + Copyright (C) 2008-2026 Kai Niessen Copyright (C) 2020 Jacob Secunda This program is free software; you can redistribute it and/or @@ -36,7 +36,6 @@ #include #include - #include "RadioApp.h" @@ -45,14 +44,15 @@ MainWindow::MainWindow() - : BWindow(BRect(0, 0, 400, 200), B_TRANSLATE_SYSTEM_NAME("StreamRadio"), B_DOCUMENT_WINDOW, 0), + : BWindow(BRect(0, 0, 400, 200), B_TRANSLATE_SYSTEM_NAME("StreamRadio"), B_DOCUMENT_WINDOW, + B_AUTO_UPDATE_SIZE_LIMITS), fStationFinder(NULL) { fSettings = &((RadioApp*)be_app)->Settings; fAllowParallelPlayback = fSettings->GetAllowParallelPlayback(); fMenuParallelPlayback = new BMenuItem( - B_TRANSLATE("Allow parallel playback"), new BMessage(MSG_PARALLEL_PLAYBACK)); + B_TRANSLATE("Allow parallel playback"), new BMessage(MSG_PARALLEL_PLAYBACK)); fMenuParallelPlayback->SetMarked(fAllowParallelPlayback); fMainMenu = new BMenuBar(Bounds(), "MainMenu"); @@ -129,8 +129,9 @@ MainWindow::MessageReceived(BMessage* message) StationListViewItem* stationItem; BString result; while (message->FindRef("refs", index++, &ref) == B_OK) { - Station* station = Station::Load(ref.name, new BEntry(&ref)); - if ((station = Station::Load(ref.name, new BEntry(&ref)))) { + BEntry entry(&ref); + Station* station = Station::Load(ref.name, &entry); + if (station) { Station* existingStation = fSettings->Stations->FindItem(station->Name()); if (existingStation) { delete station; @@ -144,7 +145,8 @@ MainWindow::MessageReceived(BMessage* message) B_TRANSLATE("Added station %s to list"), station->Name()->String()); } } else - result.SetToFormat(B_TRANSLATE("File could not be loaded as a station")); + result.SetToFormat(B_TRANSLATE("File %s could not be loaded as a station"), + ref.name); fStatusBar->SetText(result.String()); }