Commit 59d51cf7 authored by Felix Häcker's avatar Felix Häcker

Add error notification type; Add error handling in api::Client; Fix #378

parent 1c3e2f4f
......@@ -301,6 +301,26 @@ dependencies = [
"version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "failure"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"backtrace 0.3.34 (registry+https://github.com/rust-lang/crates.io-index)",
"failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "failure_derive"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.15.42 (registry+https://github.com/rust-lang/crates.io-index)",
"synstructure 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "fnv"
version = "1.0.6"
......@@ -1725,6 +1745,8 @@ dependencies = [
"chrono 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
"diesel 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"diesel_migrations 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"gdk 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gettext-rs 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
"gio 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
......@@ -1837,6 +1859,17 @@ dependencies = [
"unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "synstructure"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.15.42 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "tempfile"
version = "3.1.0"
......@@ -2258,6 +2291,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum either 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5527cfe0d098f36e3f8839852688e63c8fff1c90b2b405aef730615f9a7bcf7b"
"checksum env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "aafcde04e90a5226a6443b7aabdb016ba2f8307c847d524724bd9b346dd1a2d3"
"checksum error-chain 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3ab49e9dcb602294bc42f9a7dfc9bc6e936fca4418ea300dbfb84fe16de0b7d9"
"checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2"
"checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1"
"checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3"
"checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
"checksum foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
......@@ -2415,6 +2450,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad"
"checksum syn 0.15.42 (registry+https://github.com/rust-lang/crates.io-index)" = "eadc09306ca51a40555dd6fc2b415538e9e18bc9f870e47b1a524a79fe2dcf5e"
"checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6"
"checksum synstructure 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "02353edf96d6e4dc81aea2d8490a7e9db177bf8acb0e951c24940bf866cb313f"
"checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
"checksum termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "96d6098003bde162e4277c70665bd87c326f5a0c3f3fbfb285787fa482d54e6e"
"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"
......
......@@ -37,3 +37,5 @@ xdg = "2.2.0"
soup = { git="https://gitlab.gnome.org/haecker-felix/soup-rs", features = ["futures"] }
indexmap = { git = "https://github.com/bluss/indexmap.git", rev = "380e55c6a330b03f554f59ebd01c075bf396dd47" }
gettext-rs= { version = "0.4", features = ["gettext-system"] }
failure = "0.1.5"
failure_derive = "0.1.5"
......@@ -7,6 +7,8 @@
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">start</property>
<property name="margin_left">6</property>
<property name="margin_right">6</property>
<property name="reveal_child">True</property>
<child>
<object class="GtkFrame">
......@@ -21,16 +23,92 @@
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="margin_start">3</property>
<property name="margin_end">3</property>
<property name="spacing">6</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkSpinner" id="spinner">
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="active">True</property>
<property name="valign">center</property>
<property name="border_width">6</property>
<property name="spacing">6</property>
<child>
<object class="GtkButton" id="close_button">
<property name="width_request">34</property>
<property name="height_request">34</property>
<property name="can_focus">True</property>
<property name="focus_on_click">False</property>
<property name="receives_default">True</property>
<property name="no_show_all">True</property>
<property name="valign">start</property>
<property name="margin_left">6</property>
<property name="relief">none</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">window-close-symbolic</property>
</object>
</child>
<style>
<class name="image-button"/>
<class name="flat"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="spinner">
<property name="width_request">34</property>
<property name="height_request">34</property>
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="margin_right">6</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkSpinner">
<property name="width_request">20</property>
<property name="height_request">20</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="active">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="text_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">center</property>
<property name="label" translatable="yes">text</property>
<property name="wrap">True</property>
<property name="max_width_chars">70</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
......@@ -39,48 +117,61 @@
</packing>
</child>
<child>
<object class="GtkButton" id="close_button">
<property name="can_focus">True</property>
<property name="focus_on_click">False</property>
<property name="receives_default">True</property>
<object class="GtkBox" id="error_box">
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="relief">none</property>
<property name="border_width">6</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkImage">
<object class="GtkSeparator">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">window-close-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">6</property>
<property name="margin_bottom">6</property>
<property name="label" translatable="yes">Error details</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="error_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">label</property>
<property name="wrap">True</property>
<property name="max_width_chars">70</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<style>
<class name="flat image-button"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="text_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="margin_start">12</property>
<property name="margin_end">12</property>
<property name="label" translatable="yes">text</property>
<property name="wrap">True</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
......@@ -94,8 +185,5 @@
</style>
</object>
</child>
<style>
<class name="top"/>
</style>
</object>
</interface>
use gio::prelude::*;
use gio::{NONE_CANCELLABLE, DataInputStream};
use glib::prelude::*;
use glib::GString;
use soup::prelude::*;
use soup::Session;
......@@ -25,62 +27,70 @@ impl Client {
Client { server, session }
}
pub async fn send_station_request(self, request: StationRequest) -> Vec<Station> {
let url = self.build_url(STATION_SEARCH, Some(&request.url_encode()));
pub async fn send_station_request(self, request: StationRequest) -> Result<Vec<Station>, Error> {
let url = self.build_url(STATION_SEARCH, Some(&request.url_encode()))?;
debug!("Station request URL: {}", url);
let data = self.send_message(url).await.unwrap().0;
let data = self.send_message(url).await?;
// Parse text to Vec<Station>
let stations: Vec<Station> = serde_json::from_str(data.as_str()).unwrap();
let stations: Vec<Station> = serde_json::from_str(data.as_str())?;
debug!("Found {} station(s)!", stations.len());
stations
Ok(stations)
}
pub async fn get_stations_by_identifiers(self, identifiers: Vec<StationIdentifier>) -> Vec<Station> {
pub async fn get_stations_by_identifiers(self, identifiers: Vec<StationIdentifier>) -> Result<Vec<Station>, Error> {
let mut stations = Vec::new();
for identifier in identifiers {
let url = self.build_url(&format!("{}{}", STATION_BY_ID, identifier.station_id), None);
let url = self.build_url(&format!("{}{}", STATION_BY_ID, identifier.station_id), None)?;
debug!("Request station by ID URL: {}", url);
let data = self.send_message(url).await.unwrap().0;
let data = self.send_message(url).await?;
// Parse text to Vec<Station>
let mut s: Vec<Station> = serde_json::from_str(data.as_str()).unwrap();
let mut s: Vec<Station> = serde_json::from_str(data.as_str())?;
stations.append(&mut s);
}
debug!("Found {} station(s)!", stations.len());
stations
Ok(stations)
}
pub async fn get_stream_url(self, station: Station) -> StationUrl {
let url = self.build_url(&format!("{}{}", PLAYABLE_STATION_URL, station.id), None);
pub async fn get_stream_url(self, station: Station) -> Result<StationUrl, Error> {
let url = self.build_url(&format!("{}{}", PLAYABLE_STATION_URL, station.id), None)?;
debug!("Request playable URL: {}", url);
let data = self.send_message(url).await.unwrap().0;
let data = self.send_message(url).await?;
// Parse text to StationUrl
let result: Vec<StationUrl> = serde_json::from_str(data.as_str()).unwrap();
let result: Vec<StationUrl> = serde_json::from_str(data.as_str())?;
debug!("Playable URL is: {}", result[0].url);
result[0].clone()
Ok(result[0].clone())
}
// Create and send soup message, return the received data.
async fn send_message(&self, url: Url) -> Result<(GString, usize), gio::Error> {
async fn send_message(&self, url: Url) -> std::result::Result<GString, Error> {
// Create SOUP message
let message = soup::Message::new("GET", &url.to_string()).unwrap();
// Send created message
let input_stream = self.session.send_async_future(&message).await.unwrap();
// Create DataInputStream and read read received data
let data_input_stream = gio::DataInputStream::new(&input_stream);
data_input_stream.read_upto_async_future("", glib::PRIORITY_LOW).await
dbg!(url.clone());
match soup::Message::new("GET", &url.to_string()){
Some(message) => {
// Send created message
let input_stream = self.session.send_async_future(&message).await?;
// Create DataInputStream and read read received data
let data_input_stream: DataInputStream = gio::DataInputStream::new(&input_stream);
//TODO: Crash here, if stream is empty
let result = data_input_stream.read_upto_async_future("", glib::PRIORITY_LOW).await?;
Ok(result.0)
},
// Return error when message cannot be created
None => Err(Error::SoupMessageError),
}
}
fn build_url(&self, param: &str, options: Option<&str>) -> Url {
let mut url = self.server.join(param).unwrap();
fn build_url(&self, param: &str, options: Option<&str>) -> Result<Url, Error> {
let mut url = self.server.join(param)?;
options.map(|options| url.set_query(Some(options)));
url
Ok(url)
}
}
#[derive(Fail, Debug)]
pub enum Error {
#[fail(display = "Serde error: {}", _0)]
SerdeError(#[cause] serde_json::error::Error),
#[fail(display = "URL parser error: {}", _0)]
UrlParseError(#[cause] url::ParseError),
#[fail(display = "GLib Error: {}", _0)]
GLibError(#[cause] glib::error::Error),
#[fail(display = "Could not create Soup message.")]
SoupMessageError,
}
// Maps a type to a variant of the Error enum
// Source: https://gitlab.gnome.org/World/podcasts/blob/945b40249cdf41d9c9766938f455e204ff88906e/podcasts-data/src/errors.rs#L94
macro_rules! easy_from_impl {
($outer_type:ty, $from:ty => $to:expr) => (
impl From<$from> for $outer_type {
fn from(err: $from) -> Self {
$to(err)
}
}
);
($outer_type:ty, $from:ty => $to:expr, $($f:ty => $t:expr),+) => (
easy_from_impl!($outer_type, $from => $to);
easy_from_impl!($outer_type, $($f => $t),+);
);
}
easy_from_impl!(
Error,
serde_json::error::Error => Error::SerdeError,
glib::error::Error => Error::GLibError,
url::ParseError => Error::UrlParseError
);
......@@ -3,12 +3,14 @@ static STATION_BY_ID: &'static str = "json/stations/byid/";
static PLAYABLE_STATION_URL: &'static str = "json/url/";
mod client;
mod error;
mod object;
mod station;
mod station_request;
mod station_url;
pub use client::Client;
pub use error::Error;
pub use object::Object;
pub use station::Station;
pub use station_request::StationRequest;
......
......@@ -271,12 +271,20 @@ impl App {
let client = Client::new(Url::parse("http://www.radio-browser.info/webservice/").unwrap());
let sender = self.sender.clone();
let fut = client.get_stations_by_identifiers(ids).map(move |stations| {
sender.send(Action::LibraryAddStations(stations.clone())).unwrap();
spinner_notification.hide();
let message = format!("Imported {} stations!", stations.len());
let notification = Notification::new_info(&message);
sender.send(Action::ViewShowNotification(notification)).unwrap();
match stations{
Ok(stations) => {
sender.send(Action::LibraryAddStations(stations.clone())).unwrap();
let message = format!("Imported {} stations!", stations.len());
let notification = Notification::new_info(&message);
sender.send(Action::ViewShowNotification(notification)).unwrap();
},
Err(err) => {
let notification = Notification::new_error("Could not receive station data.", &err.to_string());
sender.send(Action::ViewShowNotification(notification.clone()));
}
}
});
let ctx = glib::MainContext::default();
......
......@@ -9,11 +9,12 @@ use std::path::PathBuf;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use crate::api::{Client, Station};
use crate::api::{Client, Station, Error};
use crate::app::Action;
use crate::audio::controller::{Controller, MiniController, MprisController, SidebarController};
use crate::audio::gstreamer_backend::{GstreamerBackend, GstreamerMessage};
use crate::audio::{PlaybackState, Song, SongBackend};
use crate::ui::Notification;
use crate::path;
use crate::utils;
......@@ -47,6 +48,8 @@ pub struct Player {
gst_backend: Arc<Mutex<GstreamerBackend>>,
song_backend: Rc<RefCell<SongBackend>>,
sender: Sender<Action>,
}
impl Player {
......@@ -87,6 +90,7 @@ impl Player {
controller,
gst_backend,
song_backend,
sender,
};
player.setup_signals(gst_receiver);
......@@ -101,11 +105,21 @@ impl Player {
}
let gst_backend = self.gst_backend.clone();
let sender = self.sender.clone();
let client = Client::new(Url::parse("http://www.radio-browser.info/webservice/").unwrap());
// get asynchronously the stream url and play it
let fut = client.get_stream_url(station).map(move |station_url| {
debug!("new source uri to record: {}", station_url.url);
gst_backend.lock().unwrap().new_source_uri(&station_url.url);
match station_url {
Ok(station_url) => {
debug!("new source uri to record: {}", station_url.url);
gst_backend.lock().unwrap().new_source_uri(&station_url.url);
},
Err(err) => {
let notification = Notification::new_error("Could not play station", &err.to_string());
sender.send(Action::ViewShowNotification(notification)).unwrap();
}
}
});
let ctx = glib::MainContext::default();
......
......@@ -90,9 +90,16 @@ impl Library {
let ctx = glib::MainContext::default();
let flowbox = self.flowbox.clone();
let sender = self.sender.clone();
let fut = self.client.clone().get_stations_by_identifiers(identifiers).map(move |stations| {
flowbox.add_stations(stations);
notification.hide();
match stations{
Ok(stations) => flowbox.add_stations(stations),
Err(err) => {
let notification = Notification::new_error("Could not receive station data.", &err.to_string());
sender.send(Action::ViewShowNotification(notification.clone()));
}
}
});
ctx.spawn_local(fut);
}
......
......@@ -8,7 +8,7 @@ use std::rc::Rc;
use crate::api::{Client, StationRequest};
use crate::app::Action;
use crate::ui::StationFlowBox;
use crate::ui::{StationFlowBox, Notification};
pub struct Search {
pub widget: gtk::Box,
......@@ -59,6 +59,7 @@ impl Search {
let id = self.timeout_id.clone();
let client = self.client.clone();
let flowbox = self.flowbox.clone();
let sender = self.sender.clone();
let id = glib::source::timeout_add_seconds_local(1, move || {
*id.borrow_mut() = None;
......@@ -67,10 +68,18 @@ impl Search {
let client = client.clone();
let flowbox = flowbox.clone();
let request = request.clone();
let sender = sender.clone();
let fut = client.send_station_request(request).map(move |stations| {
debug!("{:?}", stations);
flowbox.clear();
flowbox.add_stations(stations);
match stations{
Ok(s) => {
flowbox.clear();
flowbox.add_stations(s);
},
Err(err) => {
let notification = Notification::new_error("Could not receive station data.", &err.to_string());
sender.send(Action::ViewShowNotification(notification.clone()));
}
}
});
let ctx = glib::MainContext::default();
......
......@@ -12,6 +12,8 @@ extern crate matches;
#[macro_use]
extern crate diesel;
#[macro_use]
extern crate failure_derive;
#[macro_use]
extern crate diesel_migrations;
use gettextrs::*;
......
......@@ -41,6 +41,7 @@ run_command(
# Source code itself
sources = files(
'api/client.rs',
'api/error.rs',
'api/mod.rs',
'api/object.rs',
'api/station.rs',
......
......@@ -4,9 +4,11 @@ use std::rc::Rc;
#[derive(Debug, Clone)]
pub struct Notification {
revealer: gtk::Revealer,
spinner: gtk::Spinner,
spinner: gtk::Box,
text_label: gtk::Label,
error_label: gtk::Label,
close_button: gtk::Button,
error_box: gtk::Box,
}
impl Default for Notification{
......@@ -14,9 +16,11 @@ impl Default for Notification{
let builder = gtk::Builder::new_from_resource("/de/haeckerfelix/Shortwave/gtk/notification.ui");
let revealer: gtk::Revealer = builder.get_object("revealer").unwrap();
let spinner: gtk::Spinner = builder.get_object("spinner").unwrap();
let spinner: gtk::Box = builder.get_object("spinner").unwrap();
let text_label: gtk::Label = builder.get_object("text_label").unwrap();
let error_label: gtk::Label = builder.get_object("error_label").unwrap();
let close_button: gtk::Button = builder.get_object("close_button").unwrap();
let error_box: gtk::Box = builder.get_object("error_box").unwrap();
// Hide notification when close button gets clicked
let r = revealer.clone();
......@@ -29,7 +33,9 @@ impl Default for Notification{
revealer,
spinner,
text_label,
error_label,
close_button,
error_box,
}
}
}
......@@ -55,6 +61,18 @@ impl Notification {
Rc::new(notification)
}
// Returns new error notification
pub fn new_error (text: &str, error: &str) -> Rc<Self> {
let notification = Self::default();
notification.text_label.set_text(text);
notification.error_label.set_text(error);
notification.close_button.set_visible(true);
notification.error_box.set_visible(true);
Rc::new(notification)
}
pub fn show(&self, overlay: &gtk::Overlay) {
overlay.add_overlay(&self.revealer);
self.revealer.set_reveal_child(true);
......
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