Compare commits
2 commits
25f082ee6f
...
9599ee669d
Author | SHA1 | Date | |
---|---|---|---|
9599ee669d | |||
cdff5c0e48 |
4 changed files with 82 additions and 57 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2651,6 +2651,7 @@ dependencies = [
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"idna",
|
"idna",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -24,7 +24,7 @@ serde = { version = "1.0.214", features = ["derive"] }
|
||||||
tokio = { version = "1", features = ["rt-multi-thread"] }
|
tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||||
tracing = { version = "0.1.40", default-features = false, features = ["attributes", "std"] }
|
tracing = { version = "0.1.40", default-features = false, features = ["attributes", "std"] }
|
||||||
tracing-subscriber = "0.3.18"
|
tracing-subscriber = "0.3.18"
|
||||||
url = "2.5.2"
|
url = { version = "2.5.2", features = ["serde"] }
|
||||||
zbus = { version = "5.0.1", features = ["chrono"] }
|
zbus = { version = "5.0.1", features = ["chrono"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use chrono::{offset::Utc, DateTime};
|
use chrono::{offset::Utc, DateTime};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
@ -82,3 +83,27 @@ pub struct AlbumID3 {
|
||||||
pub played: Option<DateTime<Utc>>,
|
pub played: Option<DateTime<Utc>>,
|
||||||
// TODO: opensubsonic extensions?
|
// TODO: opensubsonic extensions?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SearchResults3Outer {
|
||||||
|
pub search_results3: SearchResults3,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct SearchResults3 {
|
||||||
|
pub artist: Option<Vec<ArtistID3>>,
|
||||||
|
pub album: Option<Vec<AlbumID3>>,
|
||||||
|
pub song: Option<Vec<Child>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ArtistID3 {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub cover_art: Option<String>,
|
||||||
|
pub artist_image_url: Option<Url>,
|
||||||
|
pub album_count: Option<u32>,
|
||||||
|
pub starred: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
111
src/ui/window.rs
111
src/ui/window.rs
|
@ -90,7 +90,6 @@ mod imp {
|
||||||
mpv.observe_property(4, "idle-active").unwrap();
|
mpv.observe_property(4, "idle-active").unwrap();
|
||||||
mpv.observe_property(6, "playlist-count").unwrap();
|
mpv.observe_property(6, "playlist-count").unwrap();
|
||||||
mpv.observe_property(7, "duration").unwrap();
|
mpv.observe_property(7, "duration").unwrap();
|
||||||
mpv.observe_property(8, "path").unwrap();
|
|
||||||
|
|
||||||
// "Useful to drain property changes before a new file is loaded."
|
// "Useful to drain property changes before a new file is loaded."
|
||||||
mpv.add_hook(0, "on_before_start_file", 0).unwrap();
|
mpv.add_hook(0, "on_before_start_file", 0).unwrap();
|
||||||
|
@ -185,11 +184,13 @@ mod imp {
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
Event::PropertyChange(event) => window.imp().on_property_change(event),
|
Event::PropertyChange(event) => window.imp().on_property_change(event),
|
||||||
Event::StartFile(_) => window.imp().on_start_file(),
|
|
||||||
Event::Hook(event) => window.imp().on_hook(event),
|
Event::Hook(event) => window.imp().on_hook(event),
|
||||||
Event::LogMessage(event) => window.imp().on_log_message(event),
|
Event::LogMessage(event) => window.imp().on_log_message(event),
|
||||||
Event::Seek => window.imp().on_seek(),
|
|
||||||
|
Event::StartFile(_) => window.imp().on_start_file(),
|
||||||
|
Event::FileLoaded => window.imp().on_file_loaded(),
|
||||||
Event::PlaybackRestart => window.imp().on_playback_restart(),
|
Event::PlaybackRestart => window.imp().on_playback_restart(),
|
||||||
|
Event::Seek => window.imp().on_seek(),
|
||||||
Event::EndFile(event) => window.imp().on_end_file(event),
|
Event::EndFile(event) => window.imp().on_end_file(event),
|
||||||
|
|
||||||
Event::Unknown(_) => {
|
Event::Unknown(_) => {
|
||||||
|
@ -454,60 +455,10 @@ mod imp {
|
||||||
self.obj().notify("duration");
|
self.obj().notify("duration");
|
||||||
}
|
}
|
||||||
|
|
||||||
8 => {
|
|
||||||
assert_eq!(event.name, "path");
|
|
||||||
// sanity check
|
|
||||||
match self.mpv.get_property::<String>("path") {
|
|
||||||
Ok(path) => {
|
|
||||||
assert_eq!(path, self.obj().song().unwrap().stream_url())
|
|
||||||
}
|
|
||||||
Err(err) if err.is_property_unavailable() => {}
|
|
||||||
Err(err) => panic!("{err:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_start_file(&self) {
|
|
||||||
event!(Level::INFO, "start file event");
|
|
||||||
self.obj().notify("song");
|
|
||||||
self.buffering_start();
|
|
||||||
|
|
||||||
let window = self.obj().clone();
|
|
||||||
let song_id = window.song().unwrap().id();
|
|
||||||
if let Some(handle) = self
|
|
||||||
.loading_cover_handle
|
|
||||||
.replace(Some(glib::spawn_future_local(async move {
|
|
||||||
let api = window.imp().api.borrow().as_ref().unwrap().clone();
|
|
||||||
let bytes = api
|
|
||||||
.cover_art(&song_id, None) // full size
|
|
||||||
.await
|
|
||||||
.expect("could not load cover art for song {song_id}");
|
|
||||||
|
|
||||||
match window.song() {
|
|
||||||
Some(song) if song.id() == song_id => {
|
|
||||||
let texture = gdk::Texture::from_bytes(&glib::Bytes::from_owned(bytes))
|
|
||||||
.expect("could not create texture from cover art for {song_id}");
|
|
||||||
window.set_playing_cover_art(Some(texture));
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
event!(
|
|
||||||
Level::WARN,
|
|
||||||
"was too late to fetch cover for song {song_id}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})))
|
|
||||||
{
|
|
||||||
handle.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure this is reported as 0
|
|
||||||
self.obj().notify("time-pos");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_hook(&self, event: crate::mpv::event::HookEvent) {
|
fn on_hook(&self, event: crate::mpv::event::HookEvent) {
|
||||||
match event.reply_userdata {
|
match event.reply_userdata {
|
||||||
0 => {
|
0 => {
|
||||||
|
@ -552,13 +503,61 @@ mod imp {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_start_file(&self) {
|
||||||
|
event!(Level::INFO, "StartFile");
|
||||||
|
self.obj().notify("song");
|
||||||
|
self.buffering_start();
|
||||||
|
|
||||||
|
let window = self.obj().clone();
|
||||||
|
let song_id = window.song().unwrap().id();
|
||||||
|
if let Some(handle) = self
|
||||||
|
.loading_cover_handle
|
||||||
|
.replace(Some(glib::spawn_future_local(async move {
|
||||||
|
let api = window.imp().api.borrow().as_ref().unwrap().clone();
|
||||||
|
let bytes = api
|
||||||
|
.cover_art(&song_id, None) // full size
|
||||||
|
.await
|
||||||
|
.expect("could not load cover art for song {song_id}");
|
||||||
|
|
||||||
|
match window.song() {
|
||||||
|
Some(song) if song.id() == song_id => {
|
||||||
|
let texture = gdk::Texture::from_bytes(&glib::Bytes::from_owned(bytes))
|
||||||
|
.expect("could not create texture from cover art for {song_id}");
|
||||||
|
window.set_playing_cover_art(Some(texture));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
event!(
|
||||||
|
Level::WARN,
|
||||||
|
"was too late to fetch cover for song {song_id}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
{
|
||||||
|
handle.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure this is reported as 0
|
||||||
|
self.obj().notify("time-pos");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_file_loaded(&self) {
|
||||||
|
event!(Level::INFO, "FileLoaded");
|
||||||
|
// sanity check
|
||||||
|
// i think "path" is only available after this event is dispatched
|
||||||
|
assert_eq!(
|
||||||
|
self.mpv.get_property::<String>("path").unwrap(),
|
||||||
|
self.obj().song().unwrap().stream_url()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn on_seek(&self) {
|
fn on_seek(&self) {
|
||||||
event!(Level::INFO, "seek event");
|
event!(Level::INFO, "Seek");
|
||||||
self.buffering_start();
|
self.buffering_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_playback_restart(&self) {
|
fn on_playback_restart(&self) {
|
||||||
event!(Level::INFO, "playback restart event");
|
event!(Level::INFO, "PlaybackRestart");
|
||||||
self.buffering_end();
|
self.buffering_end();
|
||||||
self.obj().notify("time-pos");
|
self.obj().notify("time-pos");
|
||||||
|
|
||||||
|
@ -570,7 +569,7 @@ mod imp {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_end_file(&self, event: crate::mpv::event::EndFileEvent) {
|
fn on_end_file(&self, event: crate::mpv::event::EndFileEvent) {
|
||||||
event!(Level::INFO, "end file event: {event:?}");
|
event!(Level::INFO, "EndFile: {event:?}");
|
||||||
self.obj().notify("song");
|
self.obj().notify("song");
|
||||||
self.buffering_end();
|
self.buffering_end();
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue