Commit 2b5a4f62 authored by Felix Häcker's avatar Felix Häcker

first commit

parents
# Generated by Cargo
# will have compiled files and executables
/target/
# These are backup files generated by rustfmt
**/*.rs.bk
\ No newline at end of file
This diff is collapsed.
[package]
name = "rustio"
authors = ["Felix Häcker <haecker.felix1207@gmail.com>"]
license = "GPL-3.0"
version = "0.0.1"
description = "Rust API wrapper for radio-browser.info"
[dependencies]
reqwest = "0.8.5"
serde = "1.0.43"
serde_json = "1.0"
serde_derive = "1.0.43"
log = "0.4"
failure = "0.1.1"
glib = "0.5.0"
[dependencies.gtk]
version = "0.4.0"
features = ["v3_22"]
\ No newline at end of file
# rustio
A radio-browser.info API wrapper for rust.
## Todo
- [x] Server communication (JSON deserialize)
- [x] Basic elements (station, country, language, state, tag)
- [x] Server stats
- [x] Audio playback using gstreamer
- [ ] Error handling
- [ ] Editable stations
- [ ] Create new stations
- [ ] Advanced station search
- ...
extern crate serde;
extern crate serde_json;
extern crate reqwest;
extern crate gtk;
use gtk::prelude::*;
use country::Country;
use station::Station;
use std::env;
use std::collections::HashMap;
use std::sync::mpsc::Sender;
use std::sync::mpsc::channel;
use std::thread;
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Deserialize)]
pub struct StationUrlResult{
url: String,
}
const BASE_URL: &'static str = "https://www.radio-browser.info/webservice/";
const LANGUAGES: &'static str = "json/languages/";
const COUNTRIES: &'static str = "json/countries/";
const STATES: &'static str = "json/states/";
const TAGS: &'static str = "json/tags/";
const PLAYABLE_STATION_URL: &'static str = "v2/json/url/";
const STATION_BY_ID: &'static str = "json/stations/byid/";
const SEARCH: &'static str ="json/stations/search";
pub enum ClientUpdate {
NewStations(Vec<Station>),
Clear,
}
pub struct Client {
current_search_id: Rc<RefCell<u64>>,
}
impl Client {
pub fn new() -> Client {
Client {
current_search_id: Rc::new(RefCell::new(0)),
}
}
pub fn create_reqwest_client() -> reqwest::Client{
let proxy: Option<String> = match env::var("http_proxy") {
Ok(proxy) => Some(proxy),
Err(_) => None,
};
match proxy {
Some(proxy_address) => {
info!("Use Proxy: {}", proxy_address);
let proxy = reqwest::Proxy::all(&proxy_address).unwrap();
reqwest::Client::builder().proxy(proxy).build().unwrap()
},
None => reqwest::Client::new(),
}
}
pub fn get_all_languages(&self) -> Vec<Country>{
let url = format!("{}{}", BASE_URL, LANGUAGES);
Self::send_get_request(url).unwrap().json().unwrap()
}
pub fn get_all_countries(&self) -> Vec<Country>{
let url = format!("{}{}", BASE_URL, LANGUAGES);
Self::send_get_request(url).unwrap().json().unwrap()
}
pub fn get_all_states(&self) -> Vec<Country>{
let url = format!("{}{}", BASE_URL, STATES);
Self::send_get_request(url).unwrap().json().unwrap()
}
pub fn get_all_tags(&self) -> Vec<Country>{
let url = format!("{}{}", BASE_URL, TAGS);
Self::send_get_request(url).unwrap().json().unwrap()
}
pub fn get_station_by_id(&self, id: i32) -> Result<Station,&str> {
let url = format!("{}{}{}", BASE_URL, STATION_BY_ID, id);
let mut result : Vec<Station> = Self::send_get_request(url).unwrap().json().unwrap();
if result.len() > 0 {
Ok(result.remove(0))
}else {
Err("ID points to an empty station")
}
}
pub fn get_playable_station_url(station: &Station) -> String{
let url = format!("{}{}{}", BASE_URL, PLAYABLE_STATION_URL, station.id);
let result: StationUrlResult = Self::send_get_request(url).unwrap().json().unwrap();
result.url
}
pub fn search(&mut self, params: HashMap<String, String>, sender: Sender<ClientUpdate>){
// Generate a new search ID. It is possible, that the old thread is still running,
// while a new one already have started. With this ID we can check, if the search request is still up-to-date.
*self.current_search_id.borrow_mut() += 1;
debug!("Start new search with ID {}", self.current_search_id.borrow());
sender.send(ClientUpdate::Clear);
// Do the actual search in a new thread
let (search_sender, search_receiver) = channel();
let url = format!("{}{}", BASE_URL, SEARCH);
thread::spawn(move || search_sender.send(Self::send_post_request(url, params).unwrap().json().unwrap())); //TODO: don't unwrap
// Start a loop, and wait for a message from the thread.
let current_search_id = self.current_search_id.clone();
let search_id = *self.current_search_id.borrow();
let sender = sender.clone();
gtk::timeout_add(100, move|| {
if search_id != *current_search_id.borrow() { // Compare with current search id
debug!("Search ID changed -> cancel this search loop. (This: {} <-> Current: {})", search_id, current_search_id.borrow());
return Continue(false);
}
match search_receiver.try_recv(){
Ok(stations) => {
sender.send(ClientUpdate::NewStations(stations));
Continue(false)
}
Err(_) => Continue(true),
}
});
}
fn send_post_request(url: String, params: HashMap<String, String>) -> Result<reqwest::Response, reqwest::Error>{
debug!("Post request -> {:?} ({:?})", url, params);
let client = Self::create_reqwest_client();
client.post(&url).form(&params).send()
}
fn send_get_request(url: String) -> Result<reqwest::Response, reqwest::Error>{
debug!("Get request -> {:?}", url);
let client = Self::create_reqwest_client();
client.get(&url).send()
}
}
#[derive(Deserialize)]
pub struct Country {
pub name: String,
pub value: String,
pub stationcount: String,
}
\ No newline at end of file
use reqwest;
use std::io;
#[derive(Fail, Debug)]
pub enum Error {
#[fail(display = "Reqwest error: {}", _0)]
RequestError(#[cause] reqwest::Error),
#[fail(display = "Input/Output error: {}", _0)]
IoError(#[cause] io::Error),
#[fail(display = "Unexpected server response: {}", _0)]
UnexpectedResponse(reqwest::StatusCode),
#[fail(display = "url error: {}", _0)]
UrlError(reqwest::UrlError),
#[fail(display = "Parse error")]
ParseError,
}
impl From<reqwest::Error> for Error {
fn from(err: reqwest::Error) -> Self {
Error::RequestError(err)
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Self {
Error::IoError(err)
}
}
impl From<reqwest::UrlError> for Error {
fn from(err: reqwest::UrlError) -> Self {
Error::UrlError(err)
}
}
#[derive(Deserialize)]
pub struct Language {
pub name: String,
pub value: String,
pub stationcount: String,
}
\ No newline at end of file
#[macro_use] extern crate serde_derive;
#[macro_use] extern crate log;
#[macro_use] extern crate failure;
extern crate serde;
extern crate serde_json;
extern crate reqwest;
extern crate gtk; // TODO: Don't require gtk
extern crate glib;
pub mod error;
pub mod client;
pub mod station;
pub mod country;
pub mod state;
pub mod language;
pub mod tag;
pub mod stats;
#[derive(Deserialize)]
pub struct State {
pub name: String,
pub value: String,
pub stationcount: String,
}
\ No newline at end of file
#[derive(Serialize, Deserialize, Debug, Clone, Eq, Hash)]
pub struct Station {
pub name: String,
pub language: String,
pub country: String,
pub state: String,
pub tags: String,
pub codec: String,
pub votes: String,
pub homepage: String,
pub favicon: String,
pub id: String,
pub changeuuid: String,
pub stationuuid: String,
pub url: String,
pub ip: String,
pub bitrate: String,
pub hls: String,
pub lastchangetime: String,
pub lastcheckok: String,
pub lastchecktime: String,
pub lastcheckoktime: String,
pub clicktimestamp: String,
pub clickcount: String,
pub clicktrend: String,
}
impl Station{}
impl PartialEq for Station {
fn eq(&self, other: &Station) -> bool {
self.id == other.id
}
}
#[derive(Deserialize)]
pub struct Stats {
pub stations: String,
pub stations_broken: String,
pub tags: String,
pub clicks_last_hour: String,
pub clicks_last_day: String,
pub languages: String,
pub countries: String,
}
impl Stats{
pub fn print(&self){
println!("Stations: {} ({} broken)\nTags: {}\n\
Languages: {}\n\
Countries: {}\n\
Clicks: {} last hour / {} last day",
self.stations,
self.stations_broken,
self.tags,
self.languages,
self.countries,
self.clicks_last_hour,
self.clicks_last_day);
}
}
\ No newline at end of file
#[derive(Deserialize)]
pub struct Tag {
pub name: String,
pub value: String,
pub stationcount: String,
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment