Browse and listen to thousands of radio stations across the globe right from your terminal ๐ŸŒŽ ๐Ÿ“ป ๐ŸŽตโœจ
radio rust tokio web-radio command-line-tool tui

implement `browse` api

+208 -27
+5 -1
proto/objects/v1alpha1/category.proto
··· 2 2 3 3 package objects.v1alpha1; 4 4 5 + import "objects/v1alpha1/station.proto"; 6 + 5 7 message Category { 6 - 8 + string id = 1; 9 + string name = 2; 10 + repeated objects.v1alpha1.Station stations = 3; 7 11 }
+19 -1
proto/objects/v1alpha1/station.proto
··· 3 3 package objects.v1alpha1; 4 4 5 5 message Station { 6 - 6 + string id = 1; 7 + string name = 2; 8 + string playing = 3; 9 + } 10 + 11 + message StationLinkDetails { 12 + uint32 bitrate = 1; 13 + string element = 2; 14 + string is_ad_clipped_content_enabled = 3; 15 + bool is_direct = 4; 16 + string is_hls_advanced = 5; 17 + string live_seek_stream = 6; 18 + string media_type = 7; 19 + uint32 player_height = 8; 20 + uint32 player_width = 9; 21 + string playlist_type = 10; 22 + uint32 position = 11; 23 + uint32 reliability = 12; 24 + string url = 13; 7 25 }
+12 -3
proto/tunein/v1alpha1/browse.proto
··· 2 2 3 3 package tunein.v1alpha1; 4 4 5 + import "objects/v1alpha1/category.proto"; 6 + import "objects/v1alpha1/station.proto"; 7 + 5 8 message GetCategoriesRequest {} 6 9 7 - message GetCategoriesResponse {} 10 + message GetCategoriesResponse { 11 + repeated objects.v1alpha1.Category categories = 1; 12 + } 8 13 9 14 message BrowseCategoryRequest { 10 15 string category_id = 1; 11 16 } 12 17 13 - message BrowseCategoryResponse {} 18 + message BrowseCategoryResponse { 19 + repeated objects.v1alpha1.Category categories = 1; 20 + } 14 21 15 22 message GetStationDetailsRequest { 16 23 string id = 1; 17 24 } 18 25 19 - message GetStationDetailsResponse {} 26 + message GetStationDetailsResponse { 27 + repeated objects.v1alpha1.StationLinkDetails station_link_details = 1; 28 + } 20 29 21 30 service BrowseService { 22 31 rpc GetCategories(GetCategoriesRequest) returns (GetCategoriesResponse) {}
+46 -2
src/api/objects.v1alpha1.rs
··· 1 1 #[allow(clippy::derive_partial_eq_without_eq)] 2 2 #[derive(Clone, PartialEq, ::prost::Message)] 3 - pub struct Category {} 3 + pub struct Station { 4 + #[prost(string, tag = "1")] 5 + pub id: ::prost::alloc::string::String, 6 + #[prost(string, tag = "2")] 7 + pub name: ::prost::alloc::string::String, 8 + #[prost(string, tag = "3")] 9 + pub playing: ::prost::alloc::string::String, 10 + } 4 11 #[allow(clippy::derive_partial_eq_without_eq)] 5 12 #[derive(Clone, PartialEq, ::prost::Message)] 6 - pub struct Station {} 13 + pub struct StationLinkDetails { 14 + #[prost(uint32, tag = "1")] 15 + pub bitrate: u32, 16 + #[prost(string, tag = "2")] 17 + pub element: ::prost::alloc::string::String, 18 + #[prost(string, tag = "3")] 19 + pub is_ad_clipped_content_enabled: ::prost::alloc::string::String, 20 + #[prost(bool, tag = "4")] 21 + pub is_direct: bool, 22 + #[prost(string, tag = "5")] 23 + pub is_hls_advanced: ::prost::alloc::string::String, 24 + #[prost(string, tag = "6")] 25 + pub live_seek_stream: ::prost::alloc::string::String, 26 + #[prost(string, tag = "7")] 27 + pub media_type: ::prost::alloc::string::String, 28 + #[prost(uint32, tag = "8")] 29 + pub player_height: u32, 30 + #[prost(uint32, tag = "9")] 31 + pub player_width: u32, 32 + #[prost(string, tag = "10")] 33 + pub playlist_type: ::prost::alloc::string::String, 34 + #[prost(uint32, tag = "11")] 35 + pub position: u32, 36 + #[prost(uint32, tag = "12")] 37 + pub reliability: u32, 38 + #[prost(string, tag = "13")] 39 + pub url: ::prost::alloc::string::String, 40 + } 41 + #[allow(clippy::derive_partial_eq_without_eq)] 42 + #[derive(Clone, PartialEq, ::prost::Message)] 43 + pub struct Category { 44 + #[prost(string, tag = "1")] 45 + pub id: ::prost::alloc::string::String, 46 + #[prost(string, tag = "2")] 47 + pub name: ::prost::alloc::string::String, 48 + #[prost(message, repeated, tag = "3")] 49 + pub stations: ::prost::alloc::vec::Vec<Station>, 50 + }
+14 -3
src/api/tunein.v1alpha1.rs
··· 3 3 pub struct GetCategoriesRequest {} 4 4 #[allow(clippy::derive_partial_eq_without_eq)] 5 5 #[derive(Clone, PartialEq, ::prost::Message)] 6 - pub struct GetCategoriesResponse {} 6 + pub struct GetCategoriesResponse { 7 + #[prost(message, repeated, tag = "1")] 8 + pub categories: ::prost::alloc::vec::Vec<super::super::objects::v1alpha1::Category>, 9 + } 7 10 #[allow(clippy::derive_partial_eq_without_eq)] 8 11 #[derive(Clone, PartialEq, ::prost::Message)] 9 12 pub struct BrowseCategoryRequest { ··· 12 15 } 13 16 #[allow(clippy::derive_partial_eq_without_eq)] 14 17 #[derive(Clone, PartialEq, ::prost::Message)] 15 - pub struct BrowseCategoryResponse {} 18 + pub struct BrowseCategoryResponse { 19 + #[prost(message, repeated, tag = "1")] 20 + pub categories: ::prost::alloc::vec::Vec<super::super::objects::v1alpha1::Category>, 21 + } 16 22 #[allow(clippy::derive_partial_eq_without_eq)] 17 23 #[derive(Clone, PartialEq, ::prost::Message)] 18 24 pub struct GetStationDetailsRequest { ··· 21 27 } 22 28 #[allow(clippy::derive_partial_eq_without_eq)] 23 29 #[derive(Clone, PartialEq, ::prost::Message)] 24 - pub struct GetStationDetailsResponse {} 30 + pub struct GetStationDetailsResponse { 31 + #[prost(message, repeated, tag = "1")] 32 + pub station_link_details: ::prost::alloc::vec::Vec< 33 + super::super::objects::v1alpha1::StationLinkDetails, 34 + >, 35 + } 25 36 /// Generated client implementations. 26 37 pub mod browse_service_client { 27 38 #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)]
+65
src/lib.rs
··· 1 1 pub mod api { 2 2 #[path = ""] 3 3 pub mod tunein { 4 + use tunein::types::CategoryDetails; 5 + 6 + use super::objects::v1alpha1::{Category, Station, StationLinkDetails}; 7 + 4 8 #[path = "tunein.v1alpha1.rs"] 5 9 pub mod v1alpha1; 10 + impl From<CategoryDetails> for Category { 11 + fn from(category: CategoryDetails) -> Self { 12 + Self { 13 + id: category.guide_id.unwrap_or_default(), 14 + name: category.text, 15 + stations: category 16 + .children 17 + .map(|c| { 18 + c.into_iter() 19 + .map(|x| Station { 20 + id: x.guide_id.unwrap_or_default(), 21 + name: x.text, 22 + playing: x.playing.unwrap_or_default(), 23 + }) 24 + .collect() 25 + }) 26 + .unwrap_or(vec![]), 27 + } 28 + } 29 + } 30 + 31 + impl From<tunein::types::Station> for Category { 32 + fn from(s: tunein::types::Station) -> Self { 33 + Self { 34 + id: s.guide_id.unwrap_or_default(), 35 + name: s.text, 36 + stations: s 37 + .children 38 + .map(|c| { 39 + c.into_iter() 40 + .map(|x| Station { 41 + id: x.guide_id.unwrap_or_default(), 42 + name: x.text, 43 + playing: x.playing.unwrap_or_default(), 44 + }) 45 + .collect() 46 + }) 47 + .unwrap_or(vec![]), 48 + } 49 + } 50 + } 51 + 52 + impl From<tunein::types::StationLinkDetails> for StationLinkDetails { 53 + fn from(s: tunein::types::StationLinkDetails) -> Self { 54 + Self { 55 + bitrate: s.bitrate, 56 + element: s.element, 57 + is_ad_clipped_content_enabled: s.is_ad_clipped_content_enabled, 58 + is_direct: s.is_direct, 59 + is_hls_advanced: s.is_hls_advanced, 60 + live_seek_stream: s.live_seek_stream, 61 + media_type: s.media_type, 62 + player_height: s.player_height, 63 + player_width: s.player_width, 64 + playlist_type: s.playlist_type.unwrap_or_default(), 65 + position: s.position, 66 + reliability: s.reliability, 67 + url: s.url, 68 + } 69 + } 70 + } 6 71 } 7 72 8 73 #[path = ""]
+47 -17
src/server/browse.rs
··· 1 1 use std::str::FromStr; 2 2 3 - use tunein::{types::Category, TuneInClient}; 4 - use tunein_cli::api::tunein::v1alpha1::{ 5 - browse_service_server::BrowseService, BrowseCategoryRequest, BrowseCategoryResponse, 6 - GetCategoriesRequest, GetCategoriesResponse, GetStationDetailsRequest, 7 - GetStationDetailsResponse, 3 + use tunein::{ 4 + types, 5 + TuneInClient, 6 + }; 7 + use tunein_cli::api::{ 8 + objects::v1alpha1::{Category, StationLinkDetails}, 9 + tunein::v1alpha1::{ 10 + browse_service_server::BrowseService, BrowseCategoryRequest, BrowseCategoryResponse, 11 + GetCategoriesRequest, GetCategoriesResponse, GetStationDetailsRequest, 12 + GetStationDetailsResponse, 13 + }, 8 14 }; 9 15 10 16 pub struct Browse { ··· 23 29 impl BrowseService for Browse { 24 30 async fn get_categories( 25 31 &self, 26 - request: tonic::Request<GetCategoriesRequest>, 32 + _request: tonic::Request<GetCategoriesRequest>, 27 33 ) -> Result<tonic::Response<GetCategoriesResponse>, tonic::Status> { 28 - self.client 34 + let result = self 35 + .client 29 36 .browse(None) 30 37 .await 31 38 .map_err(|e| tonic::Status::internal(e.to_string()))?; 32 - Ok(tonic::Response::new(GetCategoriesResponse {})) 39 + 40 + Ok(tonic::Response::new(GetCategoriesResponse { 41 + categories: result.into_iter().map(Category::from).collect(), 42 + })) 33 43 } 34 44 35 45 async fn browse_category( ··· 38 48 ) -> Result<tonic::Response<BrowseCategoryResponse>, tonic::Status> { 39 49 let req = request.into_inner(); 40 50 let category_id = req.category_id; 41 - let results = match Category::from_str(&category_id) { 42 - Ok(category) => self 43 - .client 44 - .browse(Some(category)) 45 - .await 46 - .map_err(|e| tonic::Status::internal(e.to_string()))?, 47 - Err(e) => return Err(tonic::Status::internal(e.to_string())), 51 + let categories: Vec<Category> = match types::Category::from_str(&category_id) { 52 + Ok(category) => { 53 + let results = self 54 + .client 55 + .browse(Some(category)) 56 + .await 57 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 58 + results.into_iter().map(Category::from).collect() 59 + } 60 + Err(_) => { 61 + let results = self 62 + .client 63 + .browse_by_id(&category_id) 64 + .await 65 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 66 + results.into_iter().map(Category::from).collect() 67 + } 48 68 }; 49 - Ok(tonic::Response::new(BrowseCategoryResponse {})) 69 + Ok(tonic::Response::new(BrowseCategoryResponse { categories })) 50 70 } 51 71 52 72 async fn get_station_details( 53 73 &self, 54 74 request: tonic::Request<GetStationDetailsRequest>, 55 75 ) -> Result<tonic::Response<GetStationDetailsResponse>, tonic::Status> { 56 - Ok(tonic::Response::new(GetStationDetailsResponse {})) 76 + let req = request.into_inner(); 77 + let station_id = req.id; 78 + let station = self 79 + .client 80 + .get_station(&station_id) 81 + .await 82 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 83 + 84 + Ok(tonic::Response::new(GetStationDetailsResponse { 85 + station_link_details: station.into_iter().map(StationLinkDetails::from).collect(), 86 + })) 57 87 } 58 88 }