Add TuneIn search/browse/playback

We might peek into https://github.com/core-hacked/tunein-api for more advanced use cases
This commit is contained in:
Tobias Gesellchen
2026-04-19 21:59:55 +02:00
parent 56256de47b
commit 56e82d5a01
14 changed files with 1430 additions and 784 deletions
+33 -2
View File
@@ -12,8 +12,13 @@ import (
// Link represents a navigational link with URL and client usage preferences.
type Link struct {
Href string `json:"href" xml:"href,attr"`
UseInternalClient string `json:"useInternalClient,omitempty" xml:"useInternalClient,attr,omitempty"`
Href string `json:"href" xml:"href,attr"`
UseInternalClient string `json:"useInternalClient,omitempty" xml:"useInternalClient,attr,omitempty"`
ContainerArt string `json:"containerArt,omitempty" xml:"-"`
Filters interface{} `json:"filters,omitempty" xml:"-"`
Name string `json:"name,omitempty" xml:"-"`
Templated *bool `json:"templated,omitempty" xml:"-"`
Type string `json:"type,omitempty" xml:"-"`
}
// Links contains various navigation links used by BMX services.
@@ -28,6 +33,32 @@ type Links struct {
BmxFavorite *Link `json:"bmx_favorite,omitempty" xml:"bmx_favorite,omitempty"`
BmxNowPlaying *Link `json:"bmx_nowplaying,omitempty" xml:"bmx_nowplaying,omitempty"`
BmxTrack *Link `json:"bmx_track,omitempty" xml:"bmx_track,omitempty"`
BmxSearch *Link `json:"bmx_search,omitempty" xml:"-"`
BmxPlayback *Link `json:"bmx_playback,omitempty" xml:"-"`
BmxPreset *Link `json:"bmx_preset,omitempty" xml:"-"`
}
// BmxNavItem represents a single item in a TuneIn browse or search result.
type BmxNavItem struct {
Links *Links `json:"_links,omitempty"`
ImageUrl string `json:"imageUrl,omitempty"`
Name string `json:"name"`
Subtitle string `json:"subtitle"`
}
// BmxNavSection represents a group of navigation items with a layout hint.
type BmxNavSection struct {
Links *Links `json:"_links,omitempty"`
Items []BmxNavItem `json:"items"`
Layout string `json:"layout,omitempty"`
Name string `json:"name"`
}
// BmxNavResponse is the top-level response for TuneIn navigate and search endpoints.
type BmxNavResponse struct {
Links *Links `json:"_links,omitempty"`
BmxSections []BmxNavSection `json:"bmx_sections"`
Layout string `json:"layout"`
}
// IconSet represents a collection of icons with different sizes for media content.
+480 -2
View File
@@ -12,16 +12,494 @@ import (
"net/url"
"strconv"
"strings"
"time"
"github.com/gesellix/bose-soundtouch/pkg/models"
)
// TuneIn endpoint templates used to resolve station and stream URLs.
const (
TuneInDescribe = "https://opml.radiotime.com/describe.ashx?id=%s"
TuneInStream = "http://opml.radiotime.com/Tune.ashx?id=%s&formats=mp3,aac,ogg"
TuneInDescribe = "https://opml.radiotime.com/describe.ashx?id=%s"
TuneInStream = "http://opml.radiotime.com/Tune.ashx?id=%s&formats=mp3,aac,ogg"
TuneInNavigateAshx = "http://opml.radiotime.com/?render=json"
TuneInSearchAPI = "https://api.radiotime.com/profiles?fulltextsearch=true&version=1.3&query="
)
var tuneInClient = &http.Client{Timeout: 10 * time.Second}
func fetchJSON(fetchURL string) (map[string]interface{}, error) {
resp, err := tuneInClient.Get(fetchURL)
if err != nil {
return nil, err
}
defer func() { _ = resp.Body.Close() }()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return nil, err
}
return result, nil
}
func decodeBase64URI(encoded string) (string, error) {
b, err := base64.URLEncoding.DecodeString(encoded)
if err != nil {
b, err = base64.StdEncoding.DecodeString(encoded)
}
if err != nil {
return "", err
}
return string(b), nil
}
// TuneInNavigate returns a live browse response for the given encoded TuneIn URI.
// Pass subsection as nil for a full page, or a pointer to an int for a single subsection.
func TuneInNavigate(encodedURI string, subsection *int) (*models.BmxNavResponse, error) {
var (
tuneInURI string
bmxSearchLink *models.Link
)
if encodedURI != "" {
decoded, err := decodeBase64URI(encodedURI)
if err != nil {
return nil, err
}
tuneInURI = decoded
} else {
tuneInURI = TuneInNavigateAshx
templated := true
bmxSearchLink = &models.Link{
Filters: []interface{}{},
Href: "/v1/search?q={query}",
Templated: &templated,
}
}
var (
sections []models.BmxNavSection
err error
)
if strings.HasPrefix(tuneInURI, "http://opml.radiotime.com/") {
sections, err = tuneInSectionsAshx(tuneInURI, subsection)
} else {
sections, err = tuneInSectionsJSONAPI(tuneInURI, subsection)
}
if err != nil {
return nil, err
}
var subsectionPart, uriPart string
if subsection != nil {
subsectionPart = fmt.Sprintf("/sub/%d", *subsection)
}
if encodedURI != "" {
uriPart = "/" + encodedURI
}
return &models.BmxNavResponse{
Links: &models.Links{
Self: &models.Link{Href: fmt.Sprintf("/v1/navigate%s%s", subsectionPart, uriPart)},
BmxSearch: bmxSearchLink,
},
BmxSections: sections,
Layout: "classic",
}, nil
}
func tuneInSectionsAshx(tuneInURI string, subsection *int) ([]models.BmxNavSection, error) {
data, err := fetchJSON(tuneInURI)
if err != nil {
return nil, err
}
layout := "list"
var (
sections []models.BmxNavSection
topItems []models.BmxNavItem
)
body, _ := data["body"].([]interface{})
for idx, rawItem := range body {
item, ok := rawItem.(map[string]interface{})
if !ok {
continue
}
itemType, _ := item["type"].(string)
if itemType == "link" {
topItems = append(topItems, tuneInNavigateLink(item))
continue
}
if subsection != nil && *subsection != idx {
continue
}
if len(body) == 1 || subsection != nil {
layout = "responsiveGrid"
} else {
layout = "ribbon"
}
maxCount := 5
if layout == "responsiveGrid" {
maxCount = 500
}
sectionTitle, _ := item["text"].(string)
var sectionItems []models.BmxNavItem
count := 0
children, _ := item["children"].([]interface{})
for _, rawChild := range children {
child, ok := rawChild.(map[string]interface{})
if !ok {
continue
}
childType, _ := child["type"].(string)
switch childType {
case "audio":
sectionItems = append(sectionItems, tuneInNavigatePlayItem(child))
case "link":
sectionItems = append(sectionItems, tuneInNavigateLink(child))
}
count++
if count >= maxCount {
break
}
}
encURI := base64.URLEncoding.EncodeToString([]byte(tuneInURI))
sections = append(sections, models.BmxNavSection{
Links: &models.Links{Self: &models.Link{Href: fmt.Sprintf("/v1/navigate/sub/%d/%s", idx, encURI)}},
Items: sectionItems,
Layout: layout,
Name: sectionTitle,
})
}
head, _ := data["head"].(map[string]interface{})
title, _ := head["title"].(string)
var subsectionPart string
if subsection != nil {
subsectionPart = fmt.Sprintf("sub/%d/", *subsection)
}
encURI := base64.URLEncoding.EncodeToString([]byte(tuneInURI))
sections = append(sections, models.BmxNavSection{
Links: &models.Links{Self: &models.Link{Href: fmt.Sprintf("/v1/navigate/%s%s", subsectionPart, encURI)}},
Items: topItems,
Layout: layout,
Name: title,
})
return sections, nil
}
func tuneInSectionsJSONAPI(tuneInURI string, subsection *int) ([]models.BmxNavSection, error) {
data, err := fetchJSON(tuneInURI)
if err != nil {
return nil, err
}
var sections []models.BmxNavSection
items, _ := data["Items"].([]interface{})
for idx, rawItem := range items {
item, ok := rawItem.(map[string]interface{})
if !ok {
continue
}
if subsection != nil && *subsection != idx {
continue
}
itemType, _ := item["Type"].(string)
containerType, _ := item["ContainerType"].(string)
if itemType == "Container" && containerType != "NotPlayableStations" {
sections = append(sections, tuneInSearchSection(item, idx, "", "shortList"))
}
}
return sections, nil
}
func tuneInNavigatePlayItem(item map[string]interface{}) models.BmxNavItem {
guideID, _ := item["guide_id"].(string)
imageURL, _ := item["image"].(string)
text, _ := item["text"].(string)
subtext, _ := item["subtext"].(string)
playbackHref := fmt.Sprintf("/v1/playback/station/%s", guideID)
return models.BmxNavItem{
Links: &models.Links{
BmxPlayback: &models.Link{Href: playbackHref, Type: "stationurl"},
BmxPreset: &models.Link{ContainerArt: imageURL, Href: guideID, Name: text, Type: "stationurl"},
},
ImageUrl: imageURL,
Name: text,
Subtitle: subtext,
}
}
func tuneInNavigateLink(item map[string]interface{}) models.BmxNavItem {
rawURL, _ := item["URL"].(string)
imageURL, _ := item["image"].(string)
text, _ := item["text"].(string)
subtext, _ := item["subtext"].(string)
encURL := base64.URLEncoding.EncodeToString([]byte(rawURL + "&render=json"))
return models.BmxNavItem{
Links: &models.Links{BmxNavigate: &models.Link{Href: fmt.Sprintf("/v1/navigate/%s", encURL)}},
ImageUrl: imageURL,
Name: text,
Subtitle: subtext,
}
}
// TuneInSearch returns live search results from TuneIn for the given query.
func TuneInSearch(query string) (*models.BmxNavResponse, error) {
tuneInURI := TuneInSearchAPI + url.QueryEscape(query)
templated := true
bmxSearchLink := &models.Link{
Filters: []interface{}{},
Href: "/v1/search?q={query}",
Templated: &templated,
}
data, err := fetchJSON(tuneInURI)
if err != nil {
return nil, err
}
var sections []models.BmxNavSection
items, _ := data["Items"].([]interface{})
for idx, rawItem := range items {
item, ok := rawItem.(map[string]interface{})
if !ok {
continue
}
itemType, _ := item["Type"].(string)
containerType, _ := item["ContainerType"].(string)
if itemType == "Container" && containerType != "NotPlayableStations" {
sections = append(sections, tuneInSearchSection(item, idx, query, "shortList"))
}
}
return &models.BmxNavResponse{
Links: &models.Links{
Self: &models.Link{Href: fmt.Sprintf("/v1/search?q=%s", query)},
BmxSearch: bmxSearchLink,
},
BmxSections: sections,
Layout: "classic",
}, nil
}
func tuneInSearchSection(item map[string]interface{}, idx int, query, layout string) models.BmxNavSection {
pivots, _ := item["Pivots"].(map[string]interface{})
more, _ := pivots["More"].(map[string]interface{})
pivotURL, _ := more["Url"].(string)
var href string
if pivotURL != "" {
href = fmt.Sprintf("/v1/navigate/%s", base64.URLEncoding.EncodeToString([]byte(pivotURL)))
} else {
encodedQuery := base64.URLEncoding.EncodeToString([]byte(TuneInSearchAPI + query))
href = fmt.Sprintf("/v1/navigate/sub/%d/%s", idx, encodedQuery)
}
var sectionItems []models.BmxNavItem
children, _ := item["Children"].([]interface{})
for _, rawChild := range children {
child, ok := rawChild.(map[string]interface{})
if !ok {
continue
}
childType, _ := child["Type"].(string)
switch childType {
case "Station":
sectionItems = append(sectionItems, tuneInSearchPlayItem(child))
case "Topic":
sectionItems = append(sectionItems, tuneInSearchTopic(child))
case "Program":
sectionItems = append(sectionItems, tuneInSearchProfile(child, "Program"))
case "Artist":
sectionItems = append(sectionItems, tuneInSearchProfile(child, "Artist"))
case "Category":
actions, _ := child["Actions"].(map[string]interface{})
browse, _ := actions["Browse"].(map[string]interface{})
categoryHref, _ := browse["Url"].(string)
encHref := base64.URLEncoding.EncodeToString([]byte(categoryHref))
image, _ := child["Image"].(string)
title, _ := child["Title"].(string)
subtitle, _ := child["Subtitle"].(string)
sectionItems = append(sectionItems, models.BmxNavItem{
Links: &models.Links{BmxNavigate: &models.Link{Href: fmt.Sprintf("/v1/navigate/%s", encHref)}},
ImageUrl: image,
Name: title,
Subtitle: subtitle,
})
}
}
title, _ := item["Title"].(string)
return models.BmxNavSection{
Links: &models.Links{Self: &models.Link{Href: href}},
Items: sectionItems,
Layout: layout,
Name: title,
}
}
func tuneInSearchPlayItem(item map[string]interface{}) models.BmxNavItem {
guideID, _ := item["GuideId"].(string)
image, _ := item["Image"].(string)
title, _ := item["Title"].(string)
subtitle, _ := item["Subtitle"].(string)
href := fmt.Sprintf("/v1/playback/station/%s", guideID)
return models.BmxNavItem{
Links: &models.Links{
BmxPlayback: &models.Link{Href: href, Type: "stationurl"},
BmxPreset: &models.Link{ContainerArt: image, Href: href, Name: title, Type: "stationurl"},
},
ImageUrl: image,
Name: title,
Subtitle: subtitle,
}
}
func tuneInSearchTopic(item map[string]interface{}) models.BmxNavItem {
guideID, _ := item["GuideId"].(string)
image, _ := item["Image"].(string)
title, _ := item["Title"].(string)
subtitle, _ := item["Subtitle"].(string)
encodedName := base64.URLEncoding.EncodeToString([]byte(title))
href := fmt.Sprintf("/v1/playback/episodes/%s?encoded_name=%s", guideID, encodedName)
return models.BmxNavItem{
Links: &models.Links{
BmxPlayback: &models.Link{Href: href, Type: "tracklisturl"},
BmxPreset: &models.Link{ContainerArt: image, Href: href, Name: title, Type: "tracklisturl"},
},
ImageUrl: image,
Name: title,
Subtitle: subtitle,
}
}
func tuneInSearchProfile(item map[string]interface{}, name string) models.BmxNavItem {
guideID, _ := item["GuideId"].(string)
image, _ := item["Image"].(string)
title, _ := item["Title"].(string)
subtitle, _ := item["Subtitle"].(string)
actions, _ := item["Actions"].(map[string]interface{})
profile, _ := actions["Profile"].(map[string]interface{})
apiURL, _ := profile["Url"].(string)
apiURLEncoded := base64.URLEncoding.EncodeToString([]byte(apiURL))
return models.BmxNavItem{
Links: &models.Links{
BmxNavigate: &models.Link{Href: fmt.Sprintf("/v1/navigate/profiles/%s/%s/%s", name, guideID, apiURLEncoded)},
BmxPreset: &models.Link{ContainerArt: image, Href: fmt.Sprintf("/v1/preset/program/%s", guideID), Name: title, Type: "tracklisturl"},
},
ImageUrl: image,
Name: title,
Subtitle: subtitle,
}
}
// TuneInNavigateProfile returns a profile (artist/program) navigation response.
func TuneInNavigateProfile(encodedURI string) (*models.BmxNavResponse, error) {
tuneInURI, err := decodeBase64URI(encodedURI)
if err != nil {
return nil, err
}
profileData, err := fetchJSON(tuneInURI)
if err != nil {
return nil, err
}
profileItem, _ := profileData["Item"].(map[string]interface{})
profileTitle, _ := profileItem["Title"].(string)
profileImage, _ := profileItem["Image"].(string)
profileSubtitle, _ := profileItem["Subtitle"].(string)
sections := []models.BmxNavSection{
{
Items: []models.BmxNavItem{{Name: profileTitle, ImageUrl: profileImage, Subtitle: profileSubtitle}},
Layout: "hero",
Name: "",
},
}
pivots, _ := profileItem["Pivots"].(map[string]interface{})
contents, _ := pivots["Contents"].(map[string]interface{})
contentsURL, _ := contents["Url"].(string)
if contentsURL != "" {
if contentsData, fetchErr := fetchJSON(contentsURL); fetchErr == nil {
contentsItems, _ := contentsData["Items"].([]interface{})
for idx, rawItem := range contentsItems {
item, ok := rawItem.(map[string]interface{})
if !ok {
continue
}
itemType, _ := item["Type"].(string)
containerType, _ := item["ContainerType"].(string)
if itemType == "Container" && containerType != "NotPlayableStations" {
sections = append(sections, tuneInSearchSection(item, idx, "", "list"))
}
}
}
}
return &models.BmxNavResponse{
Links: &models.Links{Self: &models.Link{Href: fmt.Sprintf("/v1/navigate/profiles/%s", encodedURI)}},
BmxSections: sections,
Layout: "classic",
}, nil
}
// TuneInPlayback resolves a live radio station and returns a Bose-compatible
// playback response with primary stream and variants.
func TuneInPlayback(stationID string) (*models.BmxPlaybackResponse, error) {
+77 -4
View File
@@ -6,6 +6,7 @@ import (
"encoding/json"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/gesellix/bose-soundtouch/pkg/service/bmx"
@@ -247,24 +248,96 @@ func (s *Server) HandleTuneInReport(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("{}"))
}
// HandleTuneInNavigate returns TuneIn navigation information.
// HandleTuneInNavigate returns live TuneIn navigation results.
// Path variants handled via chi wildcard:
// - (empty) → top-level browse
// - {encodedURI} → browse the given TuneIn URI
// - sub/{n}/{encodedURI} → single subsection of a browse page
// - profiles/{type}/{id}/{encodedURI} → artist/program profile page
func (s *Server) HandleTuneInNavigate(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") == "" {
s.writeBMXUnauthorized(w)
return
}
wildcard := chi.URLParam(r, "*")
resp, err := parseTuneInNavigatePath(wildcard)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(tuneInNavigateJSON)
if encErr := json.NewEncoder(w).Encode(resp); encErr != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
}
}
// HandleTuneInSearch returns TuneIn search results.
func parseTuneInNavigatePath(wildcard string) (interface{}, error) {
if wildcard == "" {
return bmx.TuneInNavigate("", nil)
}
firstSlash := strings.Index(wildcard, "/")
if firstSlash == -1 {
return bmx.TuneInNavigate(wildcard, nil)
}
prefix := wildcard[:firstSlash]
rest := wildcard[firstSlash+1:]
switch prefix {
case "sub":
secondSlash := strings.Index(rest, "/")
if secondSlash == -1 {
return bmx.TuneInNavigate(rest, nil)
}
n, err := strconv.Atoi(rest[:secondSlash])
if err != nil {
return bmx.TuneInNavigate(wildcard, nil)
}
return bmx.TuneInNavigate(rest[secondSlash+1:], &n)
case "profiles":
// profiles/{type}/{id}/{encodedURI}
parts := strings.SplitN(rest, "/", 3)
if len(parts) < 3 {
return bmx.TuneInNavigate(wildcard, nil)
}
return bmx.TuneInNavigateProfile(parts[2])
default:
return bmx.TuneInNavigate(wildcard, nil)
}
}
// HandleTuneInSearch returns live TuneIn search results for the given query.
func (s *Server) HandleTuneInSearch(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") == "" {
s.writeBMXUnauthorized(w)
return
}
query := r.URL.Query().Get("q")
if query == "" {
http.Error(w, "query parameter 'q' is required", http.StatusBadRequest)
return
}
resp, err := bmx.TuneInSearch(query)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(tuneInSearchJSON)
if encErr := json.NewEncoder(w).Encode(resp); encErr != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
}
}
@@ -1,6 +1,7 @@
package handlers
import (
"encoding/base64"
"encoding/json"
"net/http"
"net/http/httptest"
@@ -32,7 +33,9 @@ func TestHandleTuneInNavigate(t *testing.T) {
})
t.Run("Sub navigate", func(t *testing.T) {
req := httptest.NewRequest("GET", "/bmx/tunein/v1/navigate/some-path", nil)
// Use the top-level OPML URL as a valid encoded navigate target
encodedURI := base64.URLEncoding.EncodeToString([]byte("http://opml.radiotime.com/?render=json"))
req := httptest.NewRequest("GET", "/bmx/tunein/v1/navigate/"+encodedURI, nil)
req.Header.Set("Authorization", "Bearer mock-token")
w := httptest.NewRecorder()
-6
View File
@@ -23,12 +23,6 @@ var bmxServicesJSON []byte
//go:embed static/bmx_services_availability.json
var bmxServicesAvailabilityJSON []byte
//go:embed static/tunein_navigate.json
var tuneInNavigateJSON []byte
//go:embed static/tunein_search.json
var tuneInSearchJSON []byte
// Upstream source available at https://worldwide.bose.com/updates/soundtouch?serialnumber=_serial_
// which results in a redirect to https://downloads.bose.com/ced/soundtouch/mr4_22097fe2/index.xml
//
@@ -1,332 +0,0 @@
{
"_links": {
"bmx_search": {
"filters": [],
"href": "/v1/search?q={query}",
"templated": true
},
"self": {
"href": "/v1/navigate"
}
},
"bmx_sections": [
{
"_links": {
"self": {
"href": "/v1/navigate/aHR0cHM6Ly9hcGkucmFkaW90aW1lLmNvbS9jYXRlZ29yaWVzL2xvY2FsP3NlcmlhbD1jY2JlOTM0My1iNjQxLTQyMzEtYWFhMC05Mjc1MGY2OGMyNjcmdmVyc2lvbj0xLjMmdmlld01vZGVsPUZhbHNlJml0ZW1Ub2tlbj1CZ2dJQUFJQUFnQUJBQUVBQVFFQUFRZ0FBQQ=="
}
},
"items": [
{
"_links": {
"bmx_playback": {
"href": "/v1/playback/station/s25260",
"type": "stationurl"
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/s25260/images/logog.jpg?t=638151901560000000",
"href": "/v1/playback/station/s25260",
"name": "1LIVE",
"type": "stationurl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/s25260/images/logog.jpg?t=638151901560000000",
"name": "1LIVE",
"subtitle": "Für den Sektor"
},
{
"_links": {
"bmx_playback": {
"href": "/v1/playback/station/s42828",
"type": "stationurl"
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/s42828/images/logog.png?t=636575935889670000",
"href": "/v1/playback/station/s42828",
"name": "Deutschlandfunk",
"type": "stationurl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/s42828/images/logog.png?t=636575935889670000",
"name": "Deutschlandfunk",
"subtitle": "Soundcheck"
},
{
"_links": {
"bmx_playback": {
"href": "/v1/playback/station/s213886",
"type": "stationurl"
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/s213886/images/logog.jpg?t=639098687370000000",
"href": "/v1/playback/station/s213886",
"name": "WDR 2 Rheinland",
"type": "stationurl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/s213886/images/logog.jpg?t=639098687370000000",
"name": "WDR 2 Rheinland",
"subtitle": "Wir sind der Westen"
},
{
"_links": {
"bmx_playback": {
"href": "/v1/playback/station/s16252",
"type": "stationurl"
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/s16252/images/logog.png?t=636674275828970000",
"href": "/v1/playback/station/s16252",
"name": "Radio Köln",
"type": "stationurl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/s16252/images/logog.png?t=636674275828970000",
"name": "Radio Köln",
"subtitle": "News, Wetter, Verkehr und der beste Mix"
},
{
"_links": {
"bmx_playback": {
"href": "/v1/playback/station/s99166",
"type": "stationurl"
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/s99166/images/logog.jpg?t=639098688990000000",
"href": "/v1/playback/station/s99166",
"name": "WDR 2 Ruhrgebiet",
"type": "stationurl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/s99166/images/logog.jpg?t=639098688990000000",
"name": "WDR 2 Ruhrgebiet",
"subtitle": "Wir sind der Westen"
},
{
"_links": {
"bmx_playback": {
"href": "/v1/playback/station/s20301",
"type": "stationurl"
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/s20301/images/logog.jpg?t=639083982470000000",
"href": "/v1/playback/station/s20301",
"name": "WDR 5",
"type": "stationurl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/s20301/images/logog.jpg?t=639083982470000000",
"name": "WDR 5",
"subtitle": "WDR 5 - Mitreden. Mitfühlen. Miterleben."
}
],
"layout": "ribbon",
"name": "Local Radio"
},
{
"_links": {
"self": {
"href": "/v1/navigate/aHR0cHM6Ly9hcGkucmFkaW90aW1lLmNvbS9jYXRlZ29yaWVzL3RyZW5kaW5nP3NlcmlhbD1jY2JlOTM0My1iNjQxLTQyMzEtYWFhMC05Mjc1MGY2OGMyNjcmdmVyc2lvbj0xLjMmdmlld01vZGVsPUZhbHNlJml0ZW1Ub2tlbj1CZ2dJQUFZQUJnQUJBQUVBQVFFQUFRZ0FBQQ=="
}
},
"items": [
{
"_links": {
"bmx_playback": {
"href": "/v1/playback/station/s110052",
"type": "stationurl"
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/s110052/images/logog.jpg?t=639015950340000000",
"href": "/v1/playback/station/s110052",
"name": "CNBC",
"type": "stationurl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/s110052/images/logog.jpg?t=639015950340000000",
"name": "CNBC",
"subtitle": "Unlocked #105 - Southern Mansion & Tiny Home CNULK00105R1H"
},
{
"_links": {
"bmx_playback": {
"href": "/v1/playback/station/s7016",
"type": "stationurl"
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/s7016/images/logog.png?t=637977437790000000",
"href": "/v1/playback/station/s7016",
"name": "ABC NewsRadio",
"type": "stationurl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/s7016/images/logog.png?t=637977437790000000",
"name": "ABC NewsRadio",
"subtitle": "Continuous national coverage of opinion-free, independent and fa"
},
{
"_links": {
"bmx_playback": {
"href": "/v1/playback/station/s20431",
"type": "stationurl"
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/s20431/images/logog.jpg?t=638113795120000000",
"href": "/v1/playback/station/s20431",
"name": "FOX News Radio",
"type": "stationurl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/s20431/images/logog.jpg?t=638113795120000000",
"name": "FOX News Radio",
"subtitle": "Kennedy Saves the World"
},
{
"_links": {
"bmx_playback": {
"href": "/v1/playback/station/s24939",
"type": "stationurl"
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/s24939/images/logog.png?t=639107339520000000",
"href": "/v1/playback/station/s24939",
"name": "BBC Radio 1",
"type": "stationurl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/s24939/images/logog.png?t=639107339520000000",
"name": "BBC Radio 1",
"subtitle": "The biggest new pop and all-day vibes"
},
{
"_links": {
"bmx_playback": {
"href": "/v1/playback/station/s3022",
"type": "stationurl"
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/s3022/images/logog.jpg?t=637281897030000000",
"href": "/v1/playback/station/s3022",
"name": "CNA938",
"type": "stationurl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/s3022/images/logog.jpg?t=637281897030000000",
"name": "CNA938",
"subtitle": "Asia First Weekend with Justine Moss"
}
],
"layout": "ribbon",
"name": "Trending"
},
{
"_links": {
"self": {
"href": "/v1/navigate/aHR0cHM6Ly9hcGkucmFkaW90aW1lLmNvbS9jYXRlZ29yaWVzL3Nwb3J0cz9zZXJpYWw9Y2NiZTkzNDMtYjY0MS00MjMxLWFhYTAtOTI3NTBmNjhjMjY3JnZlcnNpb249MS4zJnZpZXdNb2RlbD1GYWxzZSZpdGVtVG9rZW49QmdnSUFBZ0FDQUFCQUFFQUFRRUFBUWdBQUE="
}
},
"items": [
{
"_links": {
"bmx_playback": {
"href": "/v1/playback/station/s354710",
"type": "stationurl"
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/z8181/images/logog.jpg?t=639107567180000000",
"href": "/v1/playback/station/s354710",
"name": "Download the free TuneIn app",
"type": "stationurl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/z8181/images/logog.jpg?t=639107567180000000",
"name": "Download the free TuneIn app",
"subtitle": "Download the free TuneIn app"
}
],
"layout": "ribbon",
"name": "Sports"
},
{
"_links": {
"self": {
"href": "/v1/navigate/"
}
},
"items": [
{
"_links": {
"bmx_navigate": {
"href": "/v1/navigate/aHR0cHM6Ly9hcGkucmFkaW90aW1lLmNvbS9jYXRlZ29yaWVzL2MxMDAwMzU1MjY_c2VyaWFsPWNjYmU5MzQzLWI2NDEtNDIzMS1hYWEwLTkyNzUwZjY4YzI2NyZ2ZXJzaW9uPTEuMyZ2aWV3TW9kZWw9RmFsc2UmaXRlbVRva2VuPUJnZ0lBQVFBQkFBQkFBRUFBUUVBQVFnQUFB"
}
},
"imageUrl": "https://media.bose.io/bmx-icons/tunein/top-menu/speaker.png",
"name": "Apple Music Radio Stations",
"subtitle": ""
},
{
"_links": {
"bmx_navigate": {
"href": "/v1/navigate/aHR0cHM6Ly9hcGkucmFkaW90aW1lLmNvbS9jYXRlZ29yaWVzL2MxMDAwMDAwODg_c2VyaWFsPWNjYmU5MzQzLWI2NDEtNDIzMS1hYWEwLTkyNzUwZjY4YzI2NyZ2ZXJzaW9uPTEuMyZ2aWV3TW9kZWw9RmFsc2UmaXRlbVRva2VuPUJnZ0lBQVVBQlFBQkFBRUFBUUVBQVFnQUFB"
}
},
"imageUrl": "https://media.bose.io/bmx-icons/tunein/top-menu/podcasts.png",
"name": "Podcasts",
"subtitle": ""
},
{
"_links": {
"bmx_navigate": {
"href": "/v1/navigate/aHR0cHM6Ly9hcGkucmFkaW90aW1lLmNvbS9jYXRlZ29yaWVzL211c2ljP3NlcmlhbD1jY2JlOTM0My1iNjQxLTQyMzEtYWFhMC05Mjc1MGY2OGMyNjcmdmVyc2lvbj0xLjMmdmlld01vZGVsPUZhbHNlJml0ZW1Ub2tlbj1CZ2dJQUFjQUJ3QUJBQUVBQVFFQUFRZ0FBQQ=="
}
},
"imageUrl": "https://media.bose.io/bmx-icons/tunein/top-menu/note.png",
"name": "Music",
"subtitle": ""
},
{
"_links": {
"bmx_navigate": {
"href": "/v1/navigate/aHR0cHM6Ly9hcGkucmFkaW90aW1lLmNvbS9jYXRlZ29yaWVzL2M1NzkyMj9zZXJpYWw9Y2NiZTkzNDMtYjY0MS00MjMxLWFhYTAtOTI3NTBmNjhjMjY3JnZlcnNpb249MS4zJnZpZXdNb2RlbD1GYWxzZSZpdGVtVG9rZW49QmdnSUFBa0FDUUFCQUFFQUFRRUFBUWdBQUE="
}
},
"imageUrl": "https://media.bose.io/bmx-icons/tunein/top-menu/news.png",
"name": "News & Talk",
"subtitle": ""
},
{
"_links": {
"bmx_navigate": {
"href": "/v1/navigate/aHR0cHM6Ly9hcGkucmFkaW90aW1lLmNvbS9jYXRlZ29yaWVzL3RhbGs_c2VyaWFsPWNjYmU5MzQzLWI2NDEtNDIzMS1hYWEwLTkyNzUwZjY4YzI2NyZ2ZXJzaW9uPTEuMyZ2aWV3TW9kZWw9RmFsc2UmaXRlbVRva2VuPUJnZ0lBQW9BQ2dBQkFBRUFBUUVBQVFnQUFB"
}
},
"imageUrl": "https://media.bose.io/bmx-icons/tunein/top-menu/microphone.png",
"name": "Talk",
"subtitle": ""
},
{
"_links": {
"bmx_navigate": {
"href": "/v1/navigate/aHR0cHM6Ly9hcGkucmFkaW90aW1lLmNvbS9jYXRlZ29yaWVzL3JlZ2lvbnM_c2VyaWFsPWNjYmU5MzQzLWI2NDEtNDIzMS1hYWEwLTkyNzUwZjY4YzI2NyZ2ZXJzaW9uPTEuMyZ2aWV3TW9kZWw9RmFsc2UmaXRlbVRva2VuPUJnZ0lBQXNBQ3dBQkFBRUFBUUVBQVFnQUFB"
}
},
"imageUrl": "https://media.bose.io/bmx-icons/tunein/top-menu/location.png",
"name": "By Location",
"subtitle": ""
},
{
"_links": {
"bmx_navigate": {
"href": "/v1/navigate/aHR0cHM6Ly9hcGkucmFkaW90aW1lLmNvbS9jYXRlZ29yaWVzL2xhbmd1YWdlcz9zZXJpYWw9Y2NiZTkzNDMtYjY0MS00MjMxLWFhYTAtOTI3NTBmNjhjMjY3JnZlcnNpb249MS4zJnZpZXdNb2RlbD1GYWxzZSZpdGVtVG9rZW49QmdnSUFBd0FEQUFCQUFFQUFRRUFBUWdBQUE="
}
},
"imageUrl": "https://media.bose.io/bmx-icons/tunein/top-menu/bubble.png",
"name": "By Language",
"subtitle": ""
}
],
"name": ""
}
],
"layout": "classic"
}
@@ -1,437 +0,0 @@
{
"_links": {
"self": {
"href": "/v1/search?q=music"
}
},
"bmx_sections": [
{
"_links": {
"self": {
"href": "/v1/navigate/aHR0cHM6Ly9hcGkucmFkaW90aW1lLmNvbS9wcm9maWxlcz9mdWxsdGV4dFNlYXJjaD10cnVlJmZpbHRlcj1wJTNBc2hvdyZxdWVyeT1tdXNpYyZzZXJpYWw9Y2NiZTkzNDMtYjY0MS00MjMxLWFhYTAtOTI3NTBmNjhjMjY3JnZlcnNpb249MS4zJml0ZW1Ub2tlbj1CZ1FFQUFFQUFRQUFBQUFBREF3QUFRUVRWZ0FBQUJOV0FBQUE="
}
},
"items": [
{
"_links": {
"bmx_navigate": {
"href": "/v1/navigate/profiles/Program/p783819/aHR0cHM6Ly9hcGkucmFkaW90aW1lLmNvbS9wcm9maWxlcy9wNzgzODE5P3NlcmlhbD1jY2JlOTM0My1iNjQxLTQyMzEtYWFhMC05Mjc1MGY2OGMyNjcmdmVyc2lvbj0xLjMmaXRlbVRva2VuPUJnUUVBQUVBQVFBQkFBRUFEQXdBQVFRVFZnQUFBQk5XQUFBQQ=="
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/p783819/images/logog.png?t=637208895200000000",
"href": "/v1/preset/program/p783819",
"name": "Must-Hear Music",
"type": "tracklisturl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/p783819/images/logog.png?t=637208895200000000",
"name": "Must-Hear Music",
"subtitle": "Billboard staffers discuss new music from artists across a variety of genres.Hosted on Acast. See acast.com/privacy for more information."
},
{
"_links": {
"bmx_navigate": {
"href": "/v1/navigate/profiles/Program/p813639/aHR0cHM6Ly9hcGkucmFkaW90aW1lLmNvbS9wcm9maWxlcy9wODEzNjM5P3NlcmlhbD1jY2JlOTM0My1iNjQxLTQyMzEtYWFhMC05Mjc1MGY2OGMyNjcmdmVyc2lvbj0xLjMmaXRlbVRva2VuPUJnUUVBQUlBQWdBQkFBRUFEQXdBQVFRVFZnQUFBQk5XQUFBQQ=="
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/p813639/images/logog.png?t=635834647084430000",
"href": "/v1/preset/program/p813639",
"name": "Music Awards 2016",
"type": "tracklisturl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/p813639/images/logog.png?t=635834647084430000",
"name": "Music Awards 2016",
"subtitle": "United States"
},
{
"_links": {
"bmx_navigate": {
"href": "/v1/navigate/profiles/Program/p967555/aHR0cHM6Ly9hcGkucmFkaW90aW1lLmNvbS9wcm9maWxlcy9wOTY3NTU1P3NlcmlhbD1jY2JlOTM0My1iNjQxLTQyMzEtYWFhMC05Mjc1MGY2OGMyNjcmdmVyc2lvbj0xLjMmaXRlbVRva2VuPUJnUUVBQU1BQXdBQkFBRUFEQXdBQVFRVFZnQUFBQk5XQUFBQQ=="
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/p967555/images/logog.png?t=637217441150000000",
"href": "/v1/preset/program/p967555",
"name": "The Great Albums",
"type": "tracklisturl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/p967555/images/logog.png?t=637217441150000000",
"name": "The Great Albums",
"subtitle": "Two indie rock musicians, Bill Lambusta and Brian Erickson, dive into great rock and pop music through the lens of the medium they care for most - the album. Every episode features a track-by-track review, discussions about the sounds they love, and..."
},
{
"_links": {
"bmx_navigate": {
"href": "/v1/navigate/profiles/Program/p939903/aHR0cHM6Ly9hcGkucmFkaW90aW1lLmNvbS9wcm9maWxlcy9wOTM5OTAzP3NlcmlhbD1jY2JlOTM0My1iNjQxLTQyMzEtYWFhMC05Mjc1MGY2OGMyNjcmdmVyc2lvbj0xLjMmaXRlbVRva2VuPUJnUUVBQVFBQkFBQkFBRUFEQXdBQVFRVFZnQUFBQk5XQUFBQQ=="
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/p939903/images/logog.png?t=638291015710000000",
"href": "/v1/preset/program/p939903",
"name": "He Sang/She Sang",
"type": "tracklisturl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/p939903/images/logog.png?t=638291015710000000",
"name": "He Sang/She Sang",
"subtitle": "He Sang/She Sang is a new podcast from WQXR for the opera-curious and opera superfans who want to know what all those big voices are really singing about. The podcast follows the radio broadcast season of the Metropolitan Opera with a weekly..."
},
{
"_links": {
"bmx_navigate": {
"href": "/v1/navigate/profiles/Program/p860133/aHR0cHM6Ly9hcGkucmFkaW90aW1lLmNvbS9wcm9maWxlcy9wODYwMTMzP3NlcmlhbD1jY2JlOTM0My1iNjQxLTQyMzEtYWFhMC05Mjc1MGY2OGMyNjcmdmVyc2lvbj0xLjMmaXRlbVRva2VuPUJnUUVBQVVBQlFBQkFBRUFEQXdBQVFRVFZnQUFBQk5XQUFBQQ=="
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/p860133/images/logog.png?t=638863003740000000",
"href": "/v1/preset/program/p860133",
"name": "Drink Champs",
"type": "tracklisturl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/p860133/images/logog.png?t=638863003740000000",
"name": "Drink Champs",
"subtitle": "Legendary Queens rapper-turned show host N.O.R.E. teams up with Miami hip-hop pioneer DJ EFN for a night of boozy conversation and boisterous storytelling. The hosts and guests engage together in fun, light-hearted conversation - looking back at their..."
},
{
"_links": {
"bmx_navigate": {
"href": "/v1/navigate/profiles/Program/p4696142/aHR0cHM6Ly9hcGkucmFkaW90aW1lLmNvbS9wcm9maWxlcy9wNDY5NjE0Mj9zZXJpYWw9Y2NiZTkzNDMtYjY0MS00MjMxLWFhYTAtOTI3NTBmNjhjMjY3JnZlcnNpb249MS4zJml0ZW1Ub2tlbj1CZ1FFQUFZQUJnQUJBQUVBREF3QUFRUVRWZ0FBQUJOV0FBQUE="
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/p4696142/images/logog.png?t=639004873490000000",
"href": "/v1/preset/program/p4696142",
"name": "Les pepites musicales de RFI",
"type": "tracklisturl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/p4696142/images/logog.png?t=639004873490000000",
"name": "Les pepites musicales de RFI",
"subtitle": "Toute lannée, nos reporters croisent des artistes du continent et dailleurs. Dans leurs maisons, dans les coulisses des concerts, les chambres dhôtel ou dans la rue se nouent des rencontres uniques où lon parle de soi, du son et du monde. RFI vous..."
},
{
"_links": {
"bmx_navigate": {
"href": "/v1/navigate/profiles/Program/p4696122/aHR0cHM6Ly9hcGkucmFkaW90aW1lLmNvbS9wcm9maWxlcy9wNDY5NjEyMj9zZXJpYWw9Y2NiZTkzNDMtYjY0MS00MjMxLWFhYTAtOTI3NTBmNjhjMjY3JnZlcnNpb249MS4zJml0ZW1Ub2tlbj1CZ1FFQUFjQUJ3QUJBQUVBREF3QUFRUVRWZ0FBQUJOV0FBQUE="
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/p4696122/images/logog.png?t=639004867290000000",
"href": "/v1/preset/program/p4696122",
"name": "Afro-Club et Afro-Club Deluxe",
"type": "tracklisturl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/p4696122/images/logog.png?t=639004867290000000",
"name": "Afro-Club et Afro-Club Deluxe",
"subtitle": "Le son de la nouvelle génération sur RFI ! À partir du 30/3/2026, du lundi au vendredi, de 20h10 à 21h00 TU, DJ Face Maker (Hervé Mandina) vous donne accès au Top 20 des artistes d'Afrique, des Caraïbes et des diasporas afros qui font vibrer les..."
},
{
"_links": {
"bmx_navigate": {
"href": "/v1/navigate/profiles/Program/p4696123/aHR0cHM6Ly9hcGkucmFkaW90aW1lLmNvbS9wcm9maWxlcy9wNDY5NjEyMz9zZXJpYWw9Y2NiZTkzNDMtYjY0MS00MjMxLWFhYTAtOTI3NTBmNjhjMjY3JnZlcnNpb249MS4zJml0ZW1Ub2tlbj1CZ1FFQUFnQUNBQUJBQUVBREF3QUFRUVRWZ0FBQUJOV0FBQUE="
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/p4696123/images/logog.png?t=639004867620000000",
"href": "/v1/preset/program/p4696123",
"name": "Bonnes Pulsations du Monde",
"type": "tracklisturl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/p4696123/images/logog.png?t=639004867620000000",
"name": "Bonnes Pulsations du Monde",
"subtitle": "BPM Bonnes Pulsations du Monde, cest une sélection de chansons qui font lactualité sur les 5 continents. DAbidjan à Caracas, de Paris à Shanghai, quest-ce qui fait vibrer la planète ? Une fois par mois, BPM vous emmène à la rencontre dun..."
},
{
"_links": {
"bmx_navigate": {
"href": "/v1/navigate/profiles/Program/p1119668/aHR0cHM6Ly9hcGkucmFkaW90aW1lLmNvbS9wcm9maWxlcy9wMTExOTY2OD9zZXJpYWw9Y2NiZTkzNDMtYjY0MS00MjMxLWFhYTAtOTI3NTBmNjhjMjY3JnZlcnNpb249MS4zJml0ZW1Ub2tlbj1CZ1FFQUFrQUNRQUJBQUVBREF3QUFRUVRWZ0FBQUJOV0FBQUE="
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/p1119668/images/logog.png?t=636592099583900000",
"href": "/v1/preset/program/p1119668",
"name": "Y'all Access",
"type": "tracklisturl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/p1119668/images/logog.png?t=636592099583900000",
"name": "Y'all Access",
"subtitle": "Kelly Sutton has your All Access pass to all the VIP events around Music City! Party hop, hit the red carpets and go behind the scenes thanks to your \"Y'all Access\" pass!"
},
{
"_links": {
"bmx_navigate": {
"href": "/v1/navigate/profiles/Program/p946296/aHR0cHM6Ly9hcGkucmFkaW90aW1lLmNvbS9wcm9maWxlcy9wOTQ2Mjk2P3NlcmlhbD1jY2JlOTM0My1iNjQxLTQyMzEtYWFhMC05Mjc1MGY2OGMyNjcmdmVyc2lvbj0xLjMmaXRlbVRva2VuPUJnUUVBQW9BQ2dBQkFBRUFEQXdBQVFRVFZnQUFBQk5XQUFBQQ=="
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/p946296/images/logog.png?t=638360999970000000",
"href": "/v1/preset/program/p946296",
"name": "The Popcast With Knox and Jamie",
"type": "tracklisturl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/p946296/images/logog.png?t=638360999970000000",
"name": "The Popcast With Knox and Jamie",
"subtitle": "A weekly pop culture podcast seeking to educate on things that entertain, but do not matter.Hosted on Acast. See acast.com/privacy for more information."
}
],
"layout": "shortList",
"name": "Shows"
},
{
"_links": {
"self": {
"href": "/v1/navigate/aHR0cHM6Ly9hcGkucmFkaW90aW1lLmNvbS9wcm9maWxlcz9mdWxsdGV4dFNlYXJjaD10cnVlJmZpbHRlcj1zJnF1ZXJ5PW11c2ljJnNlcmlhbD1jY2JlOTM0My1iNjQxLTQyMzEtYWFhMC05Mjc1MGY2OGMyNjcmdmVyc2lvbj0xLjMmaXRlbVRva2VuPUJnUUVBQUlBQWdBQUFBQUFDd3NBQVFRVFZRQUFBQk5WQUFBQQ=="
}
},
"items": [
{
"_links": {
"bmx_playback": {
"href": "/v1/playback/station/s309467",
"type": "stationurl"
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/s309467/images/logog.jpg?t=637348332440000000",
"href": "/v1/playback/station/s309467",
"name": "Kidsradio.com",
"type": "stationurl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/s309467/images/logog.jpg?t=637348332440000000",
"name": "Kidsradio.com",
"subtitle": "Greece"
},
{
"_links": {
"bmx_playback": {
"href": "/v1/playback/station/s301791",
"type": "stationurl"
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/s301791/images/logog.png?t=636480577103430000",
"href": "/v1/playback/station/s301791",
"name": "90s90s Dance",
"type": "stationurl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/s301791/images/logog.png?t=636480577103430000",
"name": "90s90s Dance",
"subtitle": "90s90s Dance: Der Dancesound der 90er."
},
{
"_links": {
"bmx_playback": {
"href": "/v1/playback/station/s281990",
"type": "stationurl"
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/s281990/images/logog.png?t=638156899930000000",
"href": "/v1/playback/station/s281990",
"name": "90s90s DAB",
"type": "stationurl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/s281990/images/logog.png?t=638156899930000000",
"name": "90s90s DAB",
"subtitle": "90s90s ist das Radio für den coolen Sound der 90er. Deutschlandweit im Digitalradio DAB+"
},
{
"_links": {
"bmx_playback": {
"href": "/v1/playback/station/s308474",
"type": "stationurl"
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/s308474/images/logog.png?t=637014668910000000",
"href": "/v1/playback/station/s308474",
"name": "90s90s In The Mix",
"type": "stationurl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/s308474/images/logog.png?t=637014668910000000",
"name": "90s90s In The Mix",
"subtitle": "90s90s In The Mix: Der Sound der 90er nonstop gemixt das Real 90s-DJ-Radio"
},
{
"_links": {
"bmx_playback": {
"href": "/v1/playback/station/s323852",
"type": "stationurl"
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/s323852/images/logog.png?t=638197456570000000",
"href": "/v1/playback/station/s323852",
"name": "90s90s DANCE RADIO",
"type": "stationurl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/s323852/images/logog.png?t=638197456570000000",
"name": "90s90s DANCE RADIO",
"subtitle": "Kein Musikstil hat die Musikszene Deutschlands und das Leben von jungen Menschen so geprägt wie der"
},
{
"_links": {
"bmx_playback": {
"href": "/v1/playback/station/s306625",
"type": "stationurl"
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/s306625/images/logog.png?t=636673358641530000",
"href": "/v1/playback/station/s306625",
"name": "90s90s Techno",
"type": "stationurl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/s306625/images/logog.png?t=636673358641530000",
"name": "90s90s Techno",
"subtitle": "Die Geburtsstunde von Techno - der typische 90s-Dancesound in ei"
},
{
"_links": {
"bmx_playback": {
"href": "/v1/playback/station/s174864",
"type": "stationurl"
},
"bmx_preset": {
"containerArt": "http://cdn-radiotime-logos.tunein.com/s174864g.png",
"href": "/v1/playback/station/s174864",
"name": "Highway 65 Radio",
"type": "stationurl"
}
},
"imageUrl": "http://cdn-radiotime-logos.tunein.com/s174864g.png",
"name": "Highway 65 Radio",
"subtitle": "Connecting listeners to the Country Music scene and lifestyle"
},
{
"_links": {
"bmx_playback": {
"href": "/v1/playback/station/s323853",
"type": "stationurl"
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/s323853/images/logog.png?t=638197456800000000",
"href": "/v1/playback/station/s323853",
"name": "80s80s DANCE",
"type": "stationurl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/s323853/images/logog.png?t=638197456800000000",
"name": "80s80s DANCE",
"subtitle": "80s80s DANCE liefert den perfekten Dance-Sound aus den 80ern in einem eigenen Radio."
},
{
"_links": {
"bmx_playback": {
"href": "/v1/playback/station/s306908",
"type": "stationurl"
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/s306908/images/logog.png?t=636758140220000000",
"href": "/v1/playback/station/s306908",
"name": "90s90s RnB",
"type": "stationurl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/s306908/images/logog.png?t=636758140220000000",
"name": "90s90s RnB",
"subtitle": "Hip-Hop-Soul, neuer Funk und ein Schwung sexuell aufgeladener Ja"
},
{
"_links": {
"bmx_playback": {
"href": "/v1/playback/station/s306584",
"type": "stationurl"
},
"bmx_preset": {
"containerArt": "http://cdn-profiles.tunein.com/s306584/images/logog.png?t=636643926846930000",
"href": "/v1/playback/station/s306584",
"name": "90s90s Grunge",
"type": "stationurl"
}
},
"imageUrl": "http://cdn-profiles.tunein.com/s306584/images/logog.png?t=636643926846930000",
"name": "90s90s Grunge",
"subtitle": "Wütende Musik der 90er: Grunge. Was in Seattle in den USA begann"
}
],
"layout": "shortList",
"name": "Stations"
},
{
"_links": {
"self": {
"href": "/v1/navigate/sub/2/aHR0cHM6Ly9hcGkucmFkaW90aW1lLmNvbS9wcm9maWxlcz9mdWxsdGV4dHNlYXJjaD10cnVlJnZlcnNpb249MS4zJnNlcmlhbD1jY2JlOTM0My1iNjQxLTQyMzEtYWFhMC05Mjc1MGY2OGMyNjcmcXVlcnk9bXVzaWM="
}
},
"items": [
{
"_links": {
"bmx_navigate": {
"href": "/v1/navigate/profiles/Artist/m1038098/aHR0cHM6Ly9hcGkucmFkaW90aW1lLmNvbS9wcm9maWxlcy9tMTAzODA5OD9zZXJpYWw9Y2NiZTkzNDMtYjY0MS00MjMxLWFhYTAtOTI3NTBmNjhjMjY3JnZlcnNpb249MS4zJml0ZW1Ub2tlbj1CZ1FFQUFFQUFRQURBQU1BRGc0QUFRUVRDd0FBQUJNTEFBQUE="
},
"bmx_preset": {
"containerArt": "http://cdn-radiotime-logos.tunein.com/s0q.png",
"href": "/v1/preset/program/m1038098",
"name": "Music Music Music",
"type": "tracklisturl"
}
},
"imageUrl": "http://cdn-radiotime-logos.tunein.com/s0q.png",
"name": "Music Music Music",
"subtitle": "Gospel, Caribbean Music"
},
{
"_links": {
"bmx_navigate": {
"href": "/v1/navigate/profiles/Artist/m1444080/aHR0cHM6Ly9hcGkucmFkaW90aW1lLmNvbS9wcm9maWxlcy9tMTQ0NDA4MD9zZXJpYWw9Y2NiZTkzNDMtYjY0MS00MjMxLWFhYTAtOTI3NTBmNjhjMjY3JnZlcnNpb249MS4zJml0ZW1Ub2tlbj1CZ1FFQUFJQUFnQURBQU1BRGc0QUFRUVRDd0FBQUJNTEFBQUE="
},
"bmx_preset": {
"containerArt": "http://cdn-radiotime-logos.tunein.com/s0q.png",
"href": "/v1/preset/program/m1444080",
"name": "No Music",
"type": "tracklisturl"
}
},
"imageUrl": "http://cdn-radiotime-logos.tunein.com/s0q.png",
"name": "No Music",
"subtitle": "Variety"
},
{
"_links": {
"bmx_navigate": {
"href": "/v1/navigate/profiles/Artist/m236951/aHR0cHM6Ly9hcGkucmFkaW90aW1lLmNvbS9wcm9maWxlcy9tMjM2OTUxP3NlcmlhbD1jY2JlOTM0My1iNjQxLTQyMzEtYWFhMC05Mjc1MGY2OGMyNjcmdmVyc2lvbj0xLjMmaXRlbVRva2VuPUJnUUVBQU1BQXdBREFBTUFEZzRBQVFRVEN3QUFBQk1MQUFBQQ=="
},
"bmx_preset": {
"containerArt": "http://cdn-albums.tunein.com/gn/40QJ66TZ3Wq.jpg",
"href": "/v1/preset/program/m236951",
"name": "The Music",
"type": "tracklisturl"
}
},
"imageUrl": "http://cdn-albums.tunein.com/gn/40QJ66TZ3Wq.jpg",
"name": "The Music",
"subtitle": "Gospel, Rock"
},
{
"_links": {
"bmx_navigate": {
"href": "/v1/navigate/profiles/Artist/m404700/aHR0cHM6Ly9hcGkucmFkaW90aW1lLmNvbS9wcm9maWxlcy9tNDA0NzAwP3NlcmlhbD1jY2JlOTM0My1iNjQxLTQyMzEtYWFhMC05Mjc1MGY2OGMyNjcmdmVyc2lvbj0xLjMmaXRlbVRva2VuPUJnUUVBQVFBQkFBREFBTUFEZzRBQVFRVEN3QUFBQk1MQUFBQQ=="
},
"bmx_preset": {
"containerArt": "http://cdn-albums.tunein.com/gn/JDJC8456C0q.jpg",
"href": "/v1/preset/program/m404700",
"name": "Music Go Music",
"type": "tracklisturl"
}
},
"imageUrl": "http://cdn-albums.tunein.com/gn/JDJC8456C0q.jpg",
"name": "Music Go Music",
"subtitle": ""
}
],
"layout": "shortList",
"name": "Suggestions (Artist)"
}
],
"layout": "classic"
}