Compare commits
No commits in common. "main" and "images" have entirely different histories.
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +0,0 @@
|
|||
/target
|
||||
/.idea
|
3352
Cargo.lock
generated
3352
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
23
Cargo.toml
23
Cargo.toml
|
@ -1,23 +0,0 @@
|
|||
[package]
|
||||
name = "mpd_info_screen"
|
||||
version = "0.4.9"
|
||||
edition = "2021"
|
||||
description = "Displays info on currently playing music from an MPD daemon"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/Stephen-Seo/mpd_info_screen"
|
||||
resolver = "2"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
image = "0.24"
|
||||
ggez = "0.9.3"
|
||||
freetype = { version = "0.7", optional = true }
|
||||
wgpu = "0.16"
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = { version = "0.69", optional = true }
|
||||
|
||||
[features]
|
||||
unicode_support = ["dep:freetype", "dep:bindgen"]
|
21
LICENSE
21
LICENSE
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021-2022 Stephen Seo
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
112
README.md
112
README.md
|
@ -1,112 +0,0 @@
|
|||
# mpd info screen
|
||||
|
||||
[![mpd info screen crates.io version badge](https://img.shields.io/crates/v/mpd_info_screen)](https://crates.io/crates/mpd_info_screen)
|
||||
[![mpd info screen license badge](https://img.shields.io/github/license/Stephen-Seo/mpd_info_screen)](https://choosealicense.com/licenses/mit/)
|
||||
|
||||
[Github Repository](https://github.com/Stephen-Seo/mpd_info_screen)
|
||||
|
||||
![mpd info screen preview image](https://git.seodisparate.com/stephenseo/mpd_info_screen/raw/branch/images/images/mpd_info_screen_preview_image.jpg)
|
||||
|
||||
A Rust program that displays info about the currently running MPD server.
|
||||
|
||||
The window shows albumart (may be embedded in the audio file, or is a "cover.jpg" in the same directory as the song file), a "time-remaining"
|
||||
counter, and the filename currently being played
|
||||
|
||||
## Known Bugs ❗❗
|
||||
|
||||
Currently there are no known bugs. Please report any bugs you find to the
|
||||
[issue tracker](https://github.com/Stephen-Seo/mpd_info_screen/issues).
|
||||
|
||||
## Unicode Support
|
||||
|
||||
By default, unicode characters will not display properly. Build the project with
|
||||
the `unicode_support` feature enabled to enable fetching fonts from the local
|
||||
filesystem to display unicode characters properly (if the system is missing a
|
||||
font, then it will still be displayed incorrectly). Note that your system must
|
||||
have `fontconfig` and `freetype` installed (most Linux systems should have these
|
||||
installed already).
|
||||
|
||||
cargo build --release --features unicode_support
|
||||
|
||||
or through crates.io:
|
||||
|
||||
cargo install --features unicode_support mpd_info_screen
|
||||
|
||||
# Usage
|
||||
|
||||
|
||||
Displays info on currently playing music from an MPD daemon
|
||||
|
||||
Usage: mpd_info_screen [OPTIONS] <HOST> [PORT]
|
||||
|
||||
Arguments:
|
||||
<HOST>
|
||||
[PORT] [default: 6600]
|
||||
|
||||
Options:
|
||||
-p <PASSWORD>
|
||||
|
||||
--disable-show-title
|
||||
disable title display
|
||||
--disable-show-artist
|
||||
disable artist display
|
||||
--disable-show-album
|
||||
disable album display
|
||||
--disable-show-filename
|
||||
disable filename display
|
||||
--pprompt
|
||||
input password via prompt
|
||||
--pfile <PASSWORD_FILE>
|
||||
read password from file
|
||||
--no-scale-fill
|
||||
don't scale-fill the album art to the window
|
||||
-l, --log-level <LOG_LEVEL>
|
||||
[default: error] [possible values: error, warning, debug, verbose]
|
||||
-t, --text-bg-opacity <TEXT_BG_OPACITY>
|
||||
sets the opacity of the text background (0-255) [default: 190]
|
||||
-h, --help
|
||||
Print help
|
||||
-V, --version
|
||||
Print version
|
||||
|
||||
|
||||
Note that presing the Escape key when the window is focused closes the program.
|
||||
|
||||
Also note that pressing the H key while displaying text will hide the text.
|
||||
|
||||
# Issues / TODO
|
||||
|
||||
- [x] UTF-8 Non-ascii font support (Use the `unicode_support` feature to enable; only tested in linux)
|
||||
- [x] Support for album art not embedded but in the same directory
|
||||
|
||||
## MPD Version
|
||||
|
||||
To get album art from the image embedded with the audio file, the "readpicture"
|
||||
protocol command is queried from MPD, which was added in version 0.22 of MPD.
|
||||
It is uncertain when the "albumart" protocol command was added (this command
|
||||
fetches album art that resides in cover.jpg/cover.png in the same directory as
|
||||
the audio file). This means that older versions of MPD may not return album art
|
||||
to display.
|
||||
|
||||
# Legal stuff
|
||||
|
||||
Uses dependency [ggez](https://github.com/ggez/ggez) which is licensed under the
|
||||
MIT license.
|
||||
|
||||
Uses dependency [image](https://crates.io/crates/image) which is licensed under
|
||||
MIT license.
|
||||
|
||||
Uses dependency [clap](https://crates.io/crates/clap) which is licensed under
|
||||
Apache-2.0 or MIT licenses.
|
||||
|
||||
## Unicode Support Dependencies
|
||||
|
||||
Uses dependency
|
||||
[fontconfig](https://www.freedesktop.org/wiki/Software/fontconfig/) which is
|
||||
[licensed with this license](https://www.freedesktop.org/software/fontconfig/fontconfig-devel/ln12.html).
|
||||
|
||||
Uses dependency [freetype](https://freetype.org) which is
|
||||
[licensed with this license](https://freetype.org/license.html).
|
||||
|
||||
Uses dependency [bindgen](https://crates.io/crates/bindgen) which is licenced
|
||||
under the BSD-3-Clause.
|
25
build.rs
25
build.rs
|
@ -1,25 +0,0 @@
|
|||
#[cfg(feature = "unicode_support")]
|
||||
use std::env;
|
||||
#[cfg(feature = "unicode_support")]
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(not(feature = "unicode_support"))]
|
||||
fn main() {}
|
||||
|
||||
#[cfg(feature = "unicode_support")]
|
||||
fn main() {
|
||||
println!("cargo:rustc-link-search=/usr/lib");
|
||||
println!("cargo:rustc-link-lib=fontconfig");
|
||||
println!("cargo:rerun-if-changed=src/bindgen_wrapper.h");
|
||||
|
||||
let bindings = bindgen::Builder::default()
|
||||
.header("src/bindgen_wrapper.h")
|
||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
|
||||
.generate()
|
||||
.expect("Unable to generate bindings");
|
||||
|
||||
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
bindings
|
||||
.write_to_file(out_path.join("unicode_support_bindings.rs"))
|
||||
.expect("Couldn't write bindings");
|
||||
}
|
BIN
images/mpd_info_screen_preview_image.jpg
Normal file
BIN
images/mpd_info_screen_preview_image.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 147 KiB |
|
@ -1 +0,0 @@
|
|||
#include <fontconfig/fontconfig.h>
|
|
@ -1,69 +0,0 @@
|
|||
use std::fmt::Display;
|
||||
use clap::ValueEnum;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LogState {
|
||||
Error,
|
||||
Warning,
|
||||
Debug,
|
||||
Verbose,
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LogLevel {
|
||||
Error,
|
||||
Warning,
|
||||
Debug,
|
||||
Verbose,
|
||||
}
|
||||
|
||||
pub fn log<T>(msg: T, state: LogState, level: LogLevel)
|
||||
where
|
||||
T: Display,
|
||||
{
|
||||
if state == LogState::Error {
|
||||
log_error(msg);
|
||||
} else if state == LogState::Warning {
|
||||
if level != LogLevel::Error {
|
||||
log_warning(msg);
|
||||
}
|
||||
} else if state == LogState::Debug {
|
||||
if level == LogLevel::Debug || level == LogLevel::Verbose {
|
||||
log_debug(msg);
|
||||
}
|
||||
} else if state == LogState::Verbose {
|
||||
if level == LogLevel::Verbose {
|
||||
log_verbose(msg);
|
||||
}
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_error<T>(msg: T)
|
||||
where
|
||||
T: Display,
|
||||
{
|
||||
println!("Error: {msg}");
|
||||
}
|
||||
|
||||
pub fn log_warning<T>(msg: T)
|
||||
where
|
||||
T: Display,
|
||||
{
|
||||
println!("Warning: {msg}");
|
||||
}
|
||||
|
||||
pub fn log_debug<T>(msg: T)
|
||||
where
|
||||
T: Display,
|
||||
{
|
||||
println!("Debug: {msg}");
|
||||
}
|
||||
|
||||
pub fn log_verbose<T>(msg: T)
|
||||
where
|
||||
T: Display,
|
||||
{
|
||||
println!("Verbose: {msg}");
|
||||
}
|
1089
src/display.rs
1089
src/display.rs
File diff suppressed because it is too large
Load diff
190
src/main.rs
190
src/main.rs
|
@ -1,190 +0,0 @@
|
|||
mod debug_log;
|
||||
mod display;
|
||||
mod mpd_handler;
|
||||
#[cfg(feature = "unicode_support")]
|
||||
mod unicode_support;
|
||||
|
||||
use ggez::conf::{WindowMode, WindowSetup};
|
||||
use ggez::event::winit_event::{ElementState, KeyboardInput, ModifiersState};
|
||||
use ggez::event::{self, ControlFlow, EventHandler};
|
||||
use ggez::input::keyboard::{self, KeyInput};
|
||||
use ggez::{ContextBuilder, GameError};
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::path::PathBuf;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
use clap::Parser;
|
||||
|
||||
use debug_log::log;
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
pub struct Opt {
|
||||
host: Ipv4Addr,
|
||||
#[arg(default_value = "6600")]
|
||||
port: u16,
|
||||
#[arg(short = 'p')]
|
||||
password: Option<String>,
|
||||
#[arg(long = "disable-show-title", help = "disable title display")]
|
||||
disable_show_title: bool,
|
||||
#[arg(long = "disable-show-artist", help = "disable artist display")]
|
||||
disable_show_artist: bool,
|
||||
#[arg(long = "disable-show-album", help = "disable album display")]
|
||||
disable_show_album: bool,
|
||||
#[arg(long = "disable-show-filename", help = "disable filename display")]
|
||||
disable_show_filename: bool,
|
||||
#[arg(long = "pprompt", help = "input password via prompt")]
|
||||
enable_prompt_password: bool,
|
||||
#[arg(long = "pfile", help = "read password from file")]
|
||||
password_file: Option<PathBuf>,
|
||||
#[arg(
|
||||
long = "no-scale-fill",
|
||||
help = "don't scale-fill the album art to the window"
|
||||
)]
|
||||
do_not_fill_scale_album_art: bool,
|
||||
#[arg(
|
||||
short = 'l',
|
||||
long = "log-level",
|
||||
default_value = "error",
|
||||
)]
|
||||
log_level: debug_log::LogLevel,
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
help = "sets the opacity of the text background (0-255)",
|
||||
default_value = "190"
|
||||
)]
|
||||
text_bg_opacity: u8,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), String> {
|
||||
let mut opt = Opt::parse();
|
||||
println!("Got host addr == {}, port == {}", opt.host, opt.port);
|
||||
|
||||
// Read password from file if exists, error otherwise.
|
||||
if let Some(psswd_file_path) = opt.password_file.as_ref() {
|
||||
let mut file = File::open(psswd_file_path).expect("pfile/password_file should exist");
|
||||
let mut content: String = String::new();
|
||||
|
||||
file.read_to_string(&mut content)
|
||||
.expect("Should be able to read from pfile/password_file");
|
||||
|
||||
if content.ends_with("\r\n") {
|
||||
content.truncate(content.len() - 2);
|
||||
} else if content.ends_with('\n') {
|
||||
content.truncate(content.len() - 1);
|
||||
}
|
||||
|
||||
opt.password = Some(content);
|
||||
}
|
||||
|
||||
let (mut ctx, event_loop) = ContextBuilder::new("mpd_info_screen", "Stephen Seo")
|
||||
.window_setup(WindowSetup {
|
||||
title: "mpd info screen".into(),
|
||||
..Default::default()
|
||||
})
|
||||
.window_mode(WindowMode {
|
||||
resizable: true,
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.expect("Failed to create ggez context");
|
||||
|
||||
// mount "/" read-only so that fonts can be loaded via absolute paths
|
||||
ctx.fs.mount(&PathBuf::from("/"), true);
|
||||
|
||||
let mut display = display::MPDDisplay::new(&mut ctx, opt.clone());
|
||||
|
||||
let mut modifiers_state: ModifiersState = ModifiersState::default();
|
||||
|
||||
event_loop.run(move |mut event, _window_target, control_flow| {
|
||||
if !ctx.continuing {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
return;
|
||||
}
|
||||
|
||||
*control_flow = ControlFlow::WaitUntil(Instant::now() + Duration::from_millis(100));
|
||||
|
||||
let ctx = &mut ctx;
|
||||
|
||||
event::process_event(ctx, &mut event);
|
||||
match event {
|
||||
event::winit_event::Event::WindowEvent { event, .. } => match event {
|
||||
event::winit_event::WindowEvent::CloseRequested => ctx.request_quit(),
|
||||
event::winit_event::WindowEvent::ModifiersChanged(state) => {
|
||||
modifiers_state = state;
|
||||
}
|
||||
event::winit_event::WindowEvent::KeyboardInput {
|
||||
device_id: _,
|
||||
input:
|
||||
KeyboardInput {
|
||||
virtual_keycode: Some(keycode),
|
||||
state,
|
||||
..
|
||||
},
|
||||
is_synthetic: _,
|
||||
} => {
|
||||
if keycode == keyboard::KeyCode::Escape {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
return;
|
||||
}
|
||||
let ki = KeyInput {
|
||||
scancode: 0,
|
||||
keycode: Some(keycode),
|
||||
mods: From::from(modifiers_state),
|
||||
};
|
||||
if state == ElementState::Pressed {
|
||||
display.key_down_event(ctx, ki, false).ok();
|
||||
} else {
|
||||
display.key_up_event(ctx, ki).ok();
|
||||
}
|
||||
}
|
||||
event::winit_event::WindowEvent::Resized(phys_size) => {
|
||||
display
|
||||
.resize_event(ctx, phys_size.width as f32, phys_size.height as f32)
|
||||
.ok();
|
||||
}
|
||||
event::winit_event::WindowEvent::ReceivedCharacter(ch) => {
|
||||
display.text_input_event(ctx, ch).ok();
|
||||
}
|
||||
x => log(
|
||||
format!("Other window event fired: {x:?}"),
|
||||
debug_log::LogState::Verbose,
|
||||
opt.log_level,
|
||||
),
|
||||
},
|
||||
event::winit_event::Event::MainEventsCleared => {
|
||||
ctx.time.tick();
|
||||
|
||||
let mut game_result: Result<(), GameError> = display.update(ctx);
|
||||
if game_result.is_err() {
|
||||
println!("Error update: {}", game_result.unwrap_err());
|
||||
*control_flow = ControlFlow::Exit;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.gfx.begin_frame().unwrap();
|
||||
game_result = display.draw(ctx);
|
||||
if game_result.is_err() {
|
||||
println!("Error draw: {}", game_result.unwrap_err());
|
||||
*control_flow = ControlFlow::Exit;
|
||||
return;
|
||||
}
|
||||
ctx.gfx.end_frame().unwrap();
|
||||
|
||||
ctx.mouse.reset_delta();
|
||||
|
||||
// sleep to force ~5 fps
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
ggez::timer::yield_now();
|
||||
}
|
||||
x => log(
|
||||
format!("Device event fired: {x:?}"),
|
||||
debug_log::LogState::Verbose,
|
||||
opt.log_level,
|
||||
),
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,918 +0,0 @@
|
|||
use crate::debug_log::{log, LogLevel, LogState};
|
||||
use std::fmt::Write;
|
||||
use std::io::{self, Read, Write as IOWrite};
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream};
|
||||
use std::str::FromStr;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex, RwLock, RwLockReadGuard};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const SLEEP_DURATION: Duration = Duration::from_millis(100);
|
||||
const POLL_DURATION: Duration = Duration::from_secs(5);
|
||||
const BUF_SIZE: usize = 1024 * 4;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
enum PollState {
|
||||
None,
|
||||
Password,
|
||||
CurrentSong,
|
||||
Status,
|
||||
ReadPicture,
|
||||
ReadPictureInDir,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum MPDPlayState {
|
||||
Playing,
|
||||
Paused,
|
||||
Stopped,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InfoFromShared {
|
||||
pub filename: String,
|
||||
pub title: String,
|
||||
pub artist: String,
|
||||
pub album: String,
|
||||
pub length: f64,
|
||||
pub pos: f64,
|
||||
pub error_text: String,
|
||||
pub mpd_play_state: MPDPlayState,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MPDHandler {
|
||||
state: Arc<RwLock<MPDHandlerState>>,
|
||||
}
|
||||
|
||||
type SelfThreadT = Option<Arc<Mutex<thread::JoinHandle<Result<(), String>>>>>;
|
||||
|
||||
pub struct MPDHandlerState {
|
||||
art_data: Vec<u8>,
|
||||
art_data_size: usize,
|
||||
art_data_type: String,
|
||||
current_song_filename: String,
|
||||
current_song_title: String,
|
||||
current_song_artist: String,
|
||||
current_song_album: String,
|
||||
current_song_length: f64,
|
||||
current_song_position: f64,
|
||||
current_binary_size: usize,
|
||||
poll_state: PollState,
|
||||
stream: TcpStream,
|
||||
password: String,
|
||||
error_text: String,
|
||||
can_authenticate: bool,
|
||||
is_authenticated: bool,
|
||||
can_get_album_art: bool,
|
||||
can_get_album_art_in_dir: bool,
|
||||
can_get_status: bool,
|
||||
is_init: bool,
|
||||
did_check_overtime: bool,
|
||||
force_get_status: bool,
|
||||
force_get_current_song: bool,
|
||||
song_title_get_time: Instant,
|
||||
song_pos_get_time: Instant,
|
||||
song_length_get_time: Instant,
|
||||
self_thread: SelfThreadT,
|
||||
dirty_flag: Arc<AtomicBool>,
|
||||
pub stop_flag: Arc<AtomicBool>,
|
||||
log_level: LogLevel,
|
||||
mpd_play_state: MPDPlayState,
|
||||
}
|
||||
|
||||
fn check_next_chars(
|
||||
buf: &[u8],
|
||||
idx: usize,
|
||||
saved: &mut Vec<u8>,
|
||||
) -> Result<(char, u8), (String, u8)> {
|
||||
if idx >= buf.len() {
|
||||
return Err((String::from("idx out of bounds"), 0u8));
|
||||
}
|
||||
if buf[idx] & 0b10000000 == 0 {
|
||||
let result_str = String::from_utf8(vec![buf[idx]]);
|
||||
if let Ok(mut s) = result_str {
|
||||
let popped_char = s.pop();
|
||||
if s.is_empty() {
|
||||
Ok((popped_char.unwrap(), 1u8))
|
||||
} else {
|
||||
Err((String::from("Not one-byte UTF-8 char"), 0u8))
|
||||
}
|
||||
} else {
|
||||
Err((String::from("Not one-byte UTF-8 char"), 0u8))
|
||||
}
|
||||
} else if buf[idx] & 0b11100000 == 0b11000000 {
|
||||
if idx + 1 >= buf.len() {
|
||||
saved.push(buf[idx]);
|
||||
return Err((
|
||||
String::from("Is two-byte UTF-8, but not enough bytes provided"),
|
||||
1u8,
|
||||
));
|
||||
}
|
||||
let result_str = String::from_utf8(vec![buf[idx], buf[idx + 1]]);
|
||||
if let Ok(mut s) = result_str {
|
||||
let popped_char = s.pop();
|
||||
if s.is_empty() {
|
||||
Ok((popped_char.unwrap(), 2u8))
|
||||
} else {
|
||||
Err((String::from("Not two-byte UTF-8 char"), 0u8))
|
||||
}
|
||||
} else {
|
||||
Err((String::from("Not two-byte UTF-8 char"), 0u8))
|
||||
}
|
||||
} else if buf[idx] & 0b11110000 == 0b11100000 {
|
||||
if idx + 2 >= buf.len() {
|
||||
for c in buf.iter().skip(idx) {
|
||||
saved.push(*c);
|
||||
}
|
||||
return Err((
|
||||
String::from("Is three-byte UTF-8, but not enough bytes provided"),
|
||||
(idx + 3 - buf.len()) as u8,
|
||||
));
|
||||
}
|
||||
let result_str = String::from_utf8(vec![buf[idx], buf[idx + 1], buf[idx + 2]]);
|
||||
if let Ok(mut s) = result_str {
|
||||
let popped_char = s.pop();
|
||||
if s.is_empty() {
|
||||
Ok((popped_char.unwrap(), 3u8))
|
||||
} else {
|
||||
Err((String::from("Not three-byte UTF-8 char"), 0u8))
|
||||
}
|
||||
} else {
|
||||
Err((String::from("Not three-byte UTF-8 char"), 0u8))
|
||||
}
|
||||
} else if buf[idx] & 0b11111000 == 0b11110000 {
|
||||
if idx + 3 >= buf.len() {
|
||||
for c in buf.iter().skip(idx) {
|
||||
saved.push(*c);
|
||||
}
|
||||
return Err((
|
||||
String::from("Is four-byte UTF-8, but not enough bytes provided"),
|
||||
(idx + 4 - buf.len()) as u8,
|
||||
));
|
||||
}
|
||||
let result_str = String::from_utf8(vec![buf[idx], buf[idx + 1], buf[idx + 2]]);
|
||||
if let Ok(mut s) = result_str {
|
||||
let popped_char = s.pop();
|
||||
if s.is_empty() {
|
||||
Ok((popped_char.unwrap(), 4u8))
|
||||
} else {
|
||||
Err((String::from("Not four-byte UTF-8 char"), 0u8))
|
||||
}
|
||||
} else {
|
||||
Err((String::from("Not four-byte UTF-8 char"), 0u8))
|
||||
}
|
||||
} else {
|
||||
Err((String::from("Invalid UTF-8 char"), 0u8))
|
||||
}
|
||||
}
|
||||
|
||||
fn read_line(
|
||||
buf: &mut Vec<u8>,
|
||||
saved: &mut Vec<u8>,
|
||||
init: bool,
|
||||
) -> Result<String, (String, String)> {
|
||||
let count = buf.len();
|
||||
let mut result = String::new();
|
||||
|
||||
if count == 0 {
|
||||
return Err((
|
||||
String::from("Empty string passed to read_line"),
|
||||
String::new(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut buf_to_read: Vec<u8> = Vec::with_capacity(saved.len() + buf.len());
|
||||
|
||||
if !saved.is_empty() {
|
||||
buf_to_read.append(saved);
|
||||
}
|
||||
buf_to_read.append(buf);
|
||||
|
||||
let mut prev_three: Vec<char> = Vec::with_capacity(4);
|
||||
|
||||
let mut skip_count = 0;
|
||||
for idx in 0..count {
|
||||
if skip_count > 0 {
|
||||
skip_count -= 1;
|
||||
continue;
|
||||
}
|
||||
let next_char_result = check_next_chars(&buf_to_read, idx, saved);
|
||||
if let Ok((c, s)) = next_char_result {
|
||||
if !init {
|
||||
prev_three.push(c);
|
||||
if prev_three.len() > 3 {
|
||||
prev_three.remove(0);
|
||||
}
|
||||
if ['O', 'K', '\n'] == prev_three.as_slice() && idx + 1 == count {
|
||||
buf_to_read = buf_to_read.split_off(2);
|
||||
result = String::from("OK");
|
||||
buf.append(&mut buf_to_read);
|
||||
//println!("Warning: OK was reached"); // DEBUG
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
if c == '\n' {
|
||||
buf_to_read = buf_to_read.split_off(idx + s as usize);
|
||||
buf.append(&mut buf_to_read);
|
||||
return Ok(result);
|
||||
}
|
||||
result.push(c);
|
||||
skip_count = s - 1;
|
||||
} else if let Err((msg, count)) = next_char_result {
|
||||
//println!("Error: {}", msg); // DEBUG
|
||||
for i in 0..count {
|
||||
saved.push(buf_to_read[idx + i as usize]);
|
||||
}
|
||||
buf_to_read = buf_to_read.split_off(idx);
|
||||
buf.append(&mut buf_to_read);
|
||||
return Err((msg, result));
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
|
||||
*saved = buf_to_read;
|
||||
Err((String::from("Newline not reached"), result))
|
||||
}
|
||||
|
||||
impl MPDHandler {
|
||||
pub fn new(
|
||||
host: Ipv4Addr,
|
||||
port: u16,
|
||||
password: String,
|
||||
log_level: LogLevel,
|
||||
) -> Result<Self, String> {
|
||||
let stream = TcpStream::connect_timeout(
|
||||
&SocketAddr::new(IpAddr::V4(host), port),
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.map_err(|_| String::from("Failed to get TCP connection (is MPD running?)"))?;
|
||||
|
||||
let password_is_empty = password.is_empty();
|
||||
|
||||
let s = MPDHandler {
|
||||
state: Arc::new(RwLock::new(MPDHandlerState {
|
||||
art_data: Vec::new(),
|
||||
art_data_size: 0,
|
||||
art_data_type: String::new(),
|
||||
current_song_filename: String::new(),
|
||||
current_song_title: String::new(),
|
||||
current_song_artist: String::new(),
|
||||
current_song_length: 0.0,
|
||||
current_song_position: 0.0,
|
||||
current_binary_size: 0,
|
||||
poll_state: PollState::None,
|
||||
stream,
|
||||
password,
|
||||
error_text: String::new(),
|
||||
can_authenticate: true,
|
||||
is_authenticated: password_is_empty,
|
||||
can_get_album_art: true,
|
||||
can_get_album_art_in_dir: true,
|
||||
can_get_status: true,
|
||||
is_init: true,
|
||||
did_check_overtime: false,
|
||||
force_get_status: false,
|
||||
force_get_current_song: false,
|
||||
song_title_get_time: Instant::now().checked_sub(Duration::from_secs(10)).unwrap(),
|
||||
song_pos_get_time: Instant::now().checked_sub(Duration::from_secs(10)).unwrap(),
|
||||
song_length_get_time: Instant::now().checked_sub(Duration::from_secs(10)).unwrap(),
|
||||
self_thread: None,
|
||||
dirty_flag: Arc::new(AtomicBool::new(true)),
|
||||
stop_flag: Arc::new(AtomicBool::new(false)),
|
||||
log_level,
|
||||
mpd_play_state: MPDPlayState::Stopped,
|
||||
current_song_album: String::new(),
|
||||
})),
|
||||
};
|
||||
|
||||
let s_clone = s.clone();
|
||||
let thread = Arc::new(Mutex::new(thread::spawn(|| s_clone.handler_loop())));
|
||||
|
||||
loop {
|
||||
if let Ok(mut write_handle) = s.state.try_write() {
|
||||
write_handle.self_thread = Some(thread);
|
||||
break;
|
||||
} else {
|
||||
thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
pub fn get_mpd_handler_shared_state(&self) -> Result<InfoFromShared, ()> {
|
||||
if let Ok(read_lock) = self.state.try_read() {
|
||||
return Ok(InfoFromShared {
|
||||
filename: read_lock.current_song_filename.clone(),
|
||||
title: read_lock.current_song_title.clone(),
|
||||
artist: read_lock.current_song_artist.clone(),
|
||||
album: read_lock.current_song_album.clone(),
|
||||
length: read_lock.current_song_length,
|
||||
pos: read_lock.current_song_position
|
||||
+ read_lock.song_pos_get_time.elapsed().as_secs_f64(),
|
||||
error_text: read_lock.error_text.clone(),
|
||||
mpd_play_state: read_lock.mpd_play_state,
|
||||
});
|
||||
}
|
||||
|
||||
Err(())
|
||||
}
|
||||
|
||||
pub fn get_dirty_flag(&self) -> Result<Arc<AtomicBool>, ()> {
|
||||
if let Ok(read_lock) = self.state.try_read() {
|
||||
return Ok(read_lock.dirty_flag.clone());
|
||||
}
|
||||
|
||||
Err(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn is_dirty(&self) -> Result<bool, ()> {
|
||||
if let Ok(write_lock) = self.state.try_write() {
|
||||
return Ok(write_lock.dirty_flag.swap(false, Ordering::Relaxed));
|
||||
}
|
||||
|
||||
Err(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn force_get_current_song(&self) {
|
||||
loop {
|
||||
if let Ok(mut write_lock) = self.state.try_write() {
|
||||
write_lock.force_get_current_song = true;
|
||||
break;
|
||||
} else {
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_authenticated(&self) -> Result<bool, ()> {
|
||||
let read_handle = self.state.try_read().map_err(|_| ())?;
|
||||
Ok(read_handle.is_authenticated)
|
||||
}
|
||||
|
||||
pub fn failed_to_authenticate(&self) -> Result<bool, ()> {
|
||||
let read_handle = self.state.try_read().map_err(|_| ())?;
|
||||
Ok(!read_handle.can_authenticate)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn has_image_data(&self) -> Result<bool, ()> {
|
||||
let read_handle = self.state.try_read().map_err(|_| ())?;
|
||||
Ok(read_handle.is_art_data_ready())
|
||||
}
|
||||
|
||||
pub fn get_state_read_guard(&self) -> Result<RwLockReadGuard<'_, MPDHandlerState>, ()> {
|
||||
self.state.try_read().map_err(|_| ())
|
||||
}
|
||||
|
||||
pub fn stop_thread(&self) -> Result<(), ()> {
|
||||
let read_handle = self.state.try_read().map_err(|_| ())?;
|
||||
read_handle.stop_flag.store(true, Ordering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn force_try_other_album_art(&self) -> Result<(), ()> {
|
||||
let mut write_handle = self.state.try_write().map_err(|_| ())?;
|
||||
write_handle.art_data.clear();
|
||||
write_handle.art_data_size = 0;
|
||||
write_handle.can_get_album_art = false;
|
||||
write_handle.can_get_album_art_in_dir = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handler_loop(self) -> Result<(), String> {
|
||||
let log_level = self
|
||||
.state
|
||||
.read()
|
||||
.expect("Failed to get log_level")
|
||||
.log_level;
|
||||
let mut buf: [u8; BUF_SIZE] = [0; BUF_SIZE];
|
||||
let mut saved: Vec<u8> = Vec::new();
|
||||
let mut saved_str: String = String::new();
|
||||
|
||||
loop {
|
||||
if let Ok(write_handle) = self.state.try_write() {
|
||||
write_handle
|
||||
.stream
|
||||
.set_nonblocking(true)
|
||||
.map_err(|_| String::from("Failed to set non-blocking on TCP stream"))?;
|
||||
break;
|
||||
} else {
|
||||
thread::sleep(POLL_DURATION);
|
||||
}
|
||||
}
|
||||
|
||||
'main: loop {
|
||||
if !self.is_reading_picture()
|
||||
&& self.is_authenticated().unwrap_or(true)
|
||||
&& !self.failed_to_authenticate().unwrap_or(false)
|
||||
{
|
||||
thread::sleep(SLEEP_DURATION);
|
||||
if let Ok(write_handle) = self.state.try_write() {
|
||||
if write_handle.self_thread.is_none() {
|
||||
// main thread failed to store handle to this thread
|
||||
log(
|
||||
"MPDHandle thread stopping due to failed handle storage",
|
||||
LogState::Error,
|
||||
write_handle.log_level,
|
||||
);
|
||||
break 'main;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err_string) = self.handler_read_block(&mut buf, &mut saved, &mut saved_str) {
|
||||
log(
|
||||
format!("read_block error: {err_string}"),
|
||||
LogState::Warning,
|
||||
log_level,
|
||||
);
|
||||
} else if let Err(err_string) = self.handler_write_block() {
|
||||
log(
|
||||
format!("write_block error: {err_string}"),
|
||||
LogState::Warning,
|
||||
log_level,
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(read_handle) = self.state.try_read() {
|
||||
if read_handle.stop_flag.load(Ordering::Relaxed) || !read_handle.can_authenticate {
|
||||
break 'main;
|
||||
}
|
||||
}
|
||||
|
||||
io::stdout().flush().unwrap();
|
||||
}
|
||||
|
||||
log(
|
||||
"MPDHandler thread entering exit loop",
|
||||
LogState::Debug,
|
||||
log_level,
|
||||
);
|
||||
'exit: loop {
|
||||
if let Ok(mut write_handle) = self.state.try_write() {
|
||||
write_handle.self_thread = None;
|
||||
break 'exit;
|
||||
}
|
||||
thread::sleep(SLEEP_DURATION);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handler_read_block(
|
||||
&self,
|
||||
buf: &mut [u8; BUF_SIZE],
|
||||
saved: &mut Vec<u8>,
|
||||
saved_str: &mut String,
|
||||
) -> Result<(), String> {
|
||||
let mut write_handle = self
|
||||
.state
|
||||
.try_write()
|
||||
.map_err(|_| String::from("Failed to get MPDHandler write lock (read_block)"))?;
|
||||
let mut read_amount: usize = 0;
|
||||
let read_result = write_handle.stream.read(buf);
|
||||
if let Err(io_err) = read_result {
|
||||
if io_err.kind() != io::ErrorKind::WouldBlock {
|
||||
return Err(format!("TCP stream error: {io_err}"));
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
} else if let Ok(read_amount_result) = read_result {
|
||||
if read_amount_result == 0 {
|
||||
return Err(String::from("Got zero bytes from TCP stream"));
|
||||
}
|
||||
read_amount = read_amount_result;
|
||||
}
|
||||
let mut buf_vec: Vec<u8> = Vec::from(&buf[0..read_amount]);
|
||||
|
||||
let mut got_mpd_state: MPDPlayState = MPDPlayState::Playing;
|
||||
|
||||
'handle_buf: loop {
|
||||
if write_handle.current_binary_size > 0 {
|
||||
if write_handle.current_binary_size <= buf_vec.len() {
|
||||
let count = write_handle.current_binary_size;
|
||||
write_handle.art_data.extend_from_slice(&buf_vec[0..count]);
|
||||
buf_vec = buf_vec.split_off(count + 1);
|
||||
write_handle.current_binary_size = 0;
|
||||
write_handle.poll_state = PollState::None;
|
||||
log(
|
||||
format!(
|
||||
"Album art recv progress: {}/{}",
|
||||
write_handle.art_data.len(),
|
||||
write_handle.art_data_size
|
||||
),
|
||||
LogState::Debug,
|
||||
write_handle.log_level,
|
||||
);
|
||||
if write_handle.art_data.len() == write_handle.art_data_size {
|
||||
write_handle.dirty_flag.store(true, Ordering::Relaxed);
|
||||
}
|
||||
} else {
|
||||
write_handle.art_data.extend_from_slice(&buf_vec);
|
||||
write_handle.current_binary_size -= buf_vec.len();
|
||||
log(
|
||||
format!(
|
||||
"Album art recv progress: {}/{}",
|
||||
write_handle.art_data.len(),
|
||||
write_handle.art_data_size
|
||||
),
|
||||
LogState::Debug,
|
||||
write_handle.log_level,
|
||||
);
|
||||
if write_handle.art_data.len() == write_handle.art_data_size {
|
||||
write_handle.dirty_flag.store(true, Ordering::Relaxed);
|
||||
}
|
||||
break 'handle_buf;
|
||||
}
|
||||
}
|
||||
let read_line_result = read_line(&mut buf_vec, saved, write_handle.is_init);
|
||||
if let Ok(mut line) = read_line_result {
|
||||
line = saved_str.clone() + &line;
|
||||
*saved_str = String::new();
|
||||
if write_handle.is_init {
|
||||
if line.starts_with("OK MPD ") {
|
||||
write_handle.is_init = false;
|
||||
log(
|
||||
"Got initial \"OK\" from MPD",
|
||||
LogState::Debug,
|
||||
write_handle.log_level,
|
||||
);
|
||||
write_handle.poll_state = PollState::None;
|
||||
break 'handle_buf;
|
||||
} else {
|
||||
return Err(String::from("Did not get expected init message from MPD"));
|
||||
}
|
||||
} // write_handle.is_init
|
||||
|
||||
if line.starts_with("OK") {
|
||||
log(
|
||||
format!("Got OK when poll state is {:?}", write_handle.poll_state),
|
||||
LogState::Debug,
|
||||
write_handle.log_level,
|
||||
);
|
||||
match write_handle.poll_state {
|
||||
PollState::Password => write_handle.is_authenticated = true,
|
||||
PollState::ReadPicture => {
|
||||
if write_handle.art_data.is_empty() {
|
||||
write_handle.can_get_album_art = false;
|
||||
write_handle.dirty_flag.store(true, Ordering::Relaxed);
|
||||
log(
|
||||
"No embedded album art",
|
||||
LogState::Warning,
|
||||
write_handle.log_level,
|
||||
);
|
||||
}
|
||||
}
|
||||
PollState::ReadPictureInDir => {
|
||||
if write_handle.art_data.is_empty() {
|
||||
write_handle.can_get_album_art_in_dir = false;
|
||||
write_handle.dirty_flag.store(true, Ordering::Relaxed);
|
||||
log(
|
||||
"No album art in dir",
|
||||
LogState::Warning,
|
||||
write_handle.log_level,
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
write_handle.poll_state = PollState::None;
|
||||
break 'handle_buf;
|
||||
} else if line.starts_with("ACK") {
|
||||
log(&line, LogState::Warning, write_handle.log_level);
|
||||
match write_handle.poll_state {
|
||||
PollState::Password => {
|
||||
write_handle.can_authenticate = false;
|
||||
write_handle.dirty_flag.store(true, Ordering::Relaxed);
|
||||
write_handle.error_text = "Failed to authenticate to MPD".into();
|
||||
write_handle.stop_flag.store(true, Ordering::Relaxed);
|
||||
}
|
||||
PollState::CurrentSong | PollState::Status => {
|
||||
write_handle.can_get_status = false;
|
||||
write_handle.dirty_flag.store(true, Ordering::Relaxed);
|
||||
write_handle.error_text = "Failed to get MPD status".into();
|
||||
if line.contains("don't have permission") {
|
||||
write_handle.can_authenticate = false;
|
||||
write_handle.error_text.push_str(" (not authenticated?)");
|
||||
}
|
||||
}
|
||||
PollState::ReadPicture => {
|
||||
write_handle.can_get_album_art = false;
|
||||
write_handle.dirty_flag.store(true, Ordering::Relaxed);
|
||||
log(
|
||||
"Failed to get readpicture",
|
||||
LogState::Warning,
|
||||
write_handle.log_level,
|
||||
);
|
||||
// Not setting error_text here since
|
||||
// ReadPictureInDir is tried next
|
||||
}
|
||||
PollState::ReadPictureInDir => {
|
||||
write_handle.can_get_album_art_in_dir = false;
|
||||
write_handle.dirty_flag.store(true, Ordering::Relaxed);
|
||||
log(
|
||||
"Failed to get albumart",
|
||||
LogState::Warning,
|
||||
write_handle.log_level,
|
||||
);
|
||||
write_handle.error_text = "Failed to get album art from MPD".into();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
write_handle.poll_state = PollState::None;
|
||||
} else if line.starts_with("state: ") {
|
||||
let remaining = line.split_off(7);
|
||||
let remaining = remaining.trim();
|
||||
if remaining == "stop" {
|
||||
write_handle.current_song_filename.clear();
|
||||
write_handle.art_data.clear();
|
||||
write_handle.art_data_size = 0;
|
||||
write_handle.art_data_type.clear();
|
||||
write_handle.can_get_album_art = true;
|
||||
write_handle.can_get_album_art_in_dir = true;
|
||||
write_handle.current_song_title.clear();
|
||||
write_handle.current_song_artist.clear();
|
||||
write_handle.current_song_album.clear();
|
||||
write_handle.current_song_length = 0.0;
|
||||
write_handle.current_song_position = 0.0;
|
||||
write_handle.did_check_overtime = false;
|
||||
write_handle.force_get_status = true;
|
||||
}
|
||||
if remaining == "stop" || remaining == "pause" {
|
||||
got_mpd_state = if remaining == "stop" {
|
||||
MPDPlayState::Stopped
|
||||
} else {
|
||||
MPDPlayState::Paused
|
||||
};
|
||||
write_handle.error_text.clear();
|
||||
write!(&mut write_handle.error_text, "MPD has {got_mpd_state:?}").ok();
|
||||
log(
|
||||
format!("MPD is {got_mpd_state:?}"),
|
||||
LogState::Warning,
|
||||
write_handle.log_level,
|
||||
);
|
||||
break 'handle_buf;
|
||||
}
|
||||
} else if line.starts_with("file: ") {
|
||||
let song_file = line.split_off(6);
|
||||
if song_file != write_handle.current_song_filename {
|
||||
write_handle.current_song_filename = song_file;
|
||||
write_handle.art_data.clear();
|
||||
write_handle.art_data_size = 0;
|
||||
write_handle.art_data_type.clear();
|
||||
write_handle.can_get_album_art = true;
|
||||
write_handle.can_get_album_art_in_dir = true;
|
||||
write_handle.current_song_title.clear();
|
||||
write_handle.current_song_artist.clear();
|
||||
write_handle.current_song_album.clear();
|
||||
write_handle.current_song_length = 0.0;
|
||||
write_handle.current_song_position = 0.0;
|
||||
write_handle.did_check_overtime = false;
|
||||
write_handle.force_get_status = true;
|
||||
write_handle.error_text.clear();
|
||||
}
|
||||
write_handle.dirty_flag.store(true, Ordering::Relaxed);
|
||||
write_handle.song_title_get_time = Instant::now();
|
||||
} else if line.starts_with("elapsed: ") {
|
||||
let parse_pos_result = f64::from_str(&line.split_off(9));
|
||||
if let Ok(value) = parse_pos_result {
|
||||
write_handle.current_song_position = value;
|
||||
write_handle.dirty_flag.store(true, Ordering::Relaxed);
|
||||
write_handle.song_pos_get_time = Instant::now();
|
||||
} else {
|
||||
log(
|
||||
"Failed to parse current song position",
|
||||
LogState::Warning,
|
||||
write_handle.log_level,
|
||||
);
|
||||
}
|
||||
} else if line.starts_with("duration: ") {
|
||||
let parse_pos_result = f64::from_str(&line.split_off(10));
|
||||
if let Ok(value) = parse_pos_result {
|
||||
write_handle.current_song_length = value;
|
||||
write_handle.dirty_flag.store(true, Ordering::Relaxed);
|
||||
write_handle.song_length_get_time = Instant::now();
|
||||
} else {
|
||||
log(
|
||||
"Failed to parse current song duration",
|
||||
LogState::Warning,
|
||||
write_handle.log_level,
|
||||
);
|
||||
}
|
||||
} else if line.starts_with("size: ") {
|
||||
let parse_artsize_result = usize::from_str(&line.split_off(6));
|
||||
if let Ok(value) = parse_artsize_result {
|
||||
write_handle.art_data_size = value;
|
||||
write_handle.dirty_flag.store(true, Ordering::Relaxed);
|
||||
} else {
|
||||
log(
|
||||
"Failed to parse album art byte size",
|
||||
LogState::Warning,
|
||||
write_handle.log_level,
|
||||
);
|
||||
}
|
||||
} else if line.starts_with("binary: ") {
|
||||
let parse_artbinarysize_result = usize::from_str(&line.split_off(8));
|
||||
if let Ok(value) = parse_artbinarysize_result {
|
||||
write_handle.current_binary_size = value;
|
||||
} else {
|
||||
log(
|
||||
"Failed to parse album art chunk byte size",
|
||||
LogState::Warning,
|
||||
write_handle.log_level,
|
||||
);
|
||||
}
|
||||
} else if line.starts_with("Title: ") {
|
||||
write_handle.current_song_title = line.split_off(7);
|
||||
} else if line.starts_with("Artist: ") {
|
||||
write_handle.current_song_artist = line.split_off(8);
|
||||
} else if line.starts_with("Album: ") {
|
||||
write_handle.current_song_album = line.split_off(7);
|
||||
} else if line.starts_with("type: ") {
|
||||
write_handle.art_data_type = line.split_off(6);
|
||||
} else {
|
||||
log(
|
||||
format!("Got unrecognized/ignored line: {line}"),
|
||||
LogState::Warning,
|
||||
write_handle.log_level,
|
||||
);
|
||||
}
|
||||
} else if let Err((msg, read_line_in_progress)) = read_line_result {
|
||||
log(
|
||||
format!(
|
||||
"read_line: {}, saved size == {}, in_progress size == {}",
|
||||
msg,
|
||||
saved.len(),
|
||||
read_line_in_progress.len()
|
||||
),
|
||||
LogState::Warning,
|
||||
write_handle.log_level,
|
||||
);
|
||||
*saved_str = read_line_in_progress;
|
||||
break 'handle_buf;
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
} // 'handle_buf: loop
|
||||
|
||||
if got_mpd_state != write_handle.mpd_play_state {
|
||||
write_handle.dirty_flag.store(true, Ordering::Relaxed);
|
||||
if got_mpd_state == MPDPlayState::Playing {
|
||||
write_handle.error_text.clear();
|
||||
}
|
||||
}
|
||||
write_handle.mpd_play_state = got_mpd_state;
|
||||
if got_mpd_state != MPDPlayState::Playing {
|
||||
write_handle.poll_state = PollState::None;
|
||||
write_handle.song_pos_get_time = Instant::now();
|
||||
write_handle.current_song_length = 30.0;
|
||||
write_handle.current_song_position = 0.0;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handler_write_block(&self) -> Result<(), String> {
|
||||
let mut write_handle = self
|
||||
.state
|
||||
.try_write()
|
||||
.map_err(|_| String::from("Failed to get MPDHandler write lock (write_block)"))?;
|
||||
if write_handle.poll_state == PollState::None {
|
||||
if !write_handle.did_check_overtime
|
||||
&& write_handle.current_song_position
|
||||
+ write_handle.song_pos_get_time.elapsed().as_secs_f64()
|
||||
- 0.2
|
||||
> write_handle.current_song_length
|
||||
{
|
||||
write_handle.did_check_overtime = true;
|
||||
write_handle.force_get_current_song = true;
|
||||
}
|
||||
|
||||
if !write_handle.is_authenticated
|
||||
&& !write_handle.password.is_empty()
|
||||
&& write_handle.can_authenticate
|
||||
{
|
||||
let p = write_handle.password.clone();
|
||||
let write_result = write_handle
|
||||
.stream
|
||||
.write(format!("password {p}\n").as_bytes());
|
||||
if write_result.is_ok() {
|
||||
write_handle.poll_state = PollState::Password;
|
||||
} else if let Err(e) = write_result {
|
||||
log(
|
||||
format!("Failed to send password for authentication: {e}"),
|
||||
LogState::Error,
|
||||
write_handle.log_level,
|
||||
);
|
||||
}
|
||||
} else if write_handle.can_get_status
|
||||
&& (write_handle.song_title_get_time.elapsed() > POLL_DURATION
|
||||
|| write_handle.force_get_current_song)
|
||||
&& write_handle.mpd_play_state == MPDPlayState::Playing
|
||||
{
|
||||
write_handle.force_get_current_song = false;
|
||||
let write_result = write_handle.stream.write(b"currentsong\n");
|
||||
if write_result.is_ok() {
|
||||
write_handle.poll_state = PollState::CurrentSong;
|
||||
} else if let Err(e) = write_result {
|
||||
log(
|
||||
format!("Failed to request song info over stream: {e}"),
|
||||
LogState::Error,
|
||||
write_handle.log_level,
|
||||
);
|
||||
}
|
||||
} else if write_handle.can_get_status
|
||||
&& (write_handle.song_length_get_time.elapsed() > POLL_DURATION
|
||||
|| write_handle.song_pos_get_time.elapsed() > POLL_DURATION
|
||||
|| write_handle.force_get_status)
|
||||
{
|
||||
write_handle.force_get_status = false;
|
||||
let write_result = write_handle.stream.write(b"status\n");
|
||||
if write_result.is_ok() {
|
||||
write_handle.poll_state = PollState::Status;
|
||||
} else if let Err(e) = write_result {
|
||||
log(
|
||||
format!("Failed to request status over stream: {e}"),
|
||||
LogState::Error,
|
||||
write_handle.log_level,
|
||||
);
|
||||
}
|
||||
} else if (write_handle.art_data.is_empty()
|
||||
|| write_handle.art_data.len() != write_handle.art_data_size)
|
||||
&& !write_handle.current_song_filename.is_empty()
|
||||
{
|
||||
let title = write_handle.current_song_filename.clone();
|
||||
let art_data_length = write_handle.art_data.len();
|
||||
if write_handle.can_get_album_art {
|
||||
let write_result = write_handle
|
||||
.stream
|
||||
.write(format!("readpicture \"{title}\" {art_data_length}\n").as_bytes());
|
||||
if write_result.is_ok() {
|
||||
write_handle.poll_state = PollState::ReadPicture;
|
||||
} else if let Err(e) = write_result {
|
||||
log(
|
||||
format!("Failed to request album art: {e}"),
|
||||
LogState::Error,
|
||||
write_handle.log_level,
|
||||
);
|
||||
}
|
||||
} else if write_handle.can_get_album_art_in_dir {
|
||||
let write_result = write_handle
|
||||
.stream
|
||||
.write(format!("albumart \"{title}\" {art_data_length}\n").as_bytes());
|
||||
if write_result.is_ok() {
|
||||
write_handle.poll_state = PollState::ReadPictureInDir;
|
||||
} else if let Err(e) = write_result {
|
||||
log(
|
||||
format!("Failed to request album art in dir: {e}"),
|
||||
LogState::Error,
|
||||
write_handle.log_level,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_reading_picture(&self) -> bool {
|
||||
loop {
|
||||
if let Ok(read_handle) = self.state.try_read() {
|
||||
return read_handle.poll_state == PollState::ReadPicture
|
||||
|| read_handle.poll_state == PollState::ReadPictureInDir;
|
||||
} else {
|
||||
thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MPDHandlerState {
|
||||
pub fn get_art_type(&self) -> String {
|
||||
self.art_data_type.clone()
|
||||
}
|
||||
|
||||
pub fn is_art_data_ready(&self) -> bool {
|
||||
log(
|
||||
format!(
|
||||
"is_art_data_ready(): art_data_size == {}, art_data.len() == {}",
|
||||
self.art_data_size,
|
||||
self.art_data.len()
|
||||
),
|
||||
LogState::Debug,
|
||||
self.log_level,
|
||||
);
|
||||
self.art_data_size != 0 && self.art_data.len() == self.art_data_size
|
||||
}
|
||||
|
||||
pub fn get_art_data(&self) -> &[u8] {
|
||||
&self.art_data
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
mod fontconfig;
|
||||
mod freetype;
|
||||
|
||||
pub use self::freetype::font_has_char;
|
||||
pub use fontconfig::{get_matching_font_from_char, get_matching_font_from_str};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_ascii_verify() {
|
||||
let fetched_path =
|
||||
get_matching_font_from_char('a').expect("Should be able to find match for 'a'");
|
||||
if !font_has_char('a', &fetched_path).expect("Should be able to check font for 'a'") {
|
||||
panic!("fetched font does not have 'a'");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,350 +0,0 @@
|
|||
use std::{path::PathBuf, str::FromStr};
|
||||
|
||||
mod ffi {
|
||||
use std::{ffi::CStr, os::raw::c_int};
|
||||
|
||||
mod bindgen {
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(dead_code)]
|
||||
#![allow(deref_nullptr)]
|
||||
#![allow(clippy::redundant_static_lifetimes)]
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/unicode_support_bindings.rs"));
|
||||
}
|
||||
|
||||
pub struct FcConfigWr {
|
||||
config: *mut bindgen::FcConfig,
|
||||
}
|
||||
|
||||
impl Drop for FcConfigWr {
|
||||
fn drop(&mut self) {
|
||||
if !self.config.is_null() {
|
||||
unsafe {
|
||||
bindgen::FcConfigDestroy(self.config);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FcConfigWr {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let config = unsafe { bindgen::FcInitLoadConfigAndFonts() };
|
||||
if config.is_null() {
|
||||
Err(String::from("Failed to create FcConfig"))
|
||||
} else {
|
||||
Ok(Self { config })
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get(&mut self) -> *mut bindgen::FcConfig {
|
||||
self.config
|
||||
}
|
||||
|
||||
pub fn apply_pattern_to_config(&mut self, pattern: &mut FcPatternWr) -> bool {
|
||||
unsafe {
|
||||
bindgen::FcConfigSubstitute(
|
||||
self.config,
|
||||
pattern.get(),
|
||||
bindgen::_FcMatchKind_FcMatchPattern,
|
||||
) == bindgen::FcTrue as bindgen::FcBool
|
||||
}
|
||||
}
|
||||
|
||||
pub fn font_match(&mut self, pattern: &mut FcPatternWr) -> Result<FcPatternWr, String> {
|
||||
unsafe {
|
||||
let mut result: bindgen::FcResult = 0 as bindgen::FcResult;
|
||||
let result_pattern = bindgen::FcFontMatch(
|
||||
self.config,
|
||||
pattern.get(),
|
||||
&mut result as *mut bindgen::FcResult,
|
||||
);
|
||||
if result != bindgen::_FcResult_FcResultMatch {
|
||||
if !result_pattern.is_null() {
|
||||
bindgen::FcPatternDestroy(result_pattern);
|
||||
return Err(String::from("Failed to FcFontMatch (FcResult is not FcResultMatch; result_pattern is not null)"));
|
||||
} else {
|
||||
return Err(format!(
|
||||
"Failed to FcFontMatch (FcResult is not FcResultMatch; {result:?})"
|
||||
));
|
||||
}
|
||||
} else if result_pattern.is_null() {
|
||||
return Err(String::from(
|
||||
"Failed to FcFontMatch (result_pattern is null)",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(FcPatternWr {
|
||||
pattern: result_pattern,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FcCharSetWr {
|
||||
charset: *mut bindgen::FcCharSet,
|
||||
}
|
||||
|
||||
impl Drop for FcCharSetWr {
|
||||
fn drop(&mut self) {
|
||||
if !self.charset.is_null() {
|
||||
unsafe {
|
||||
bindgen::FcCharSetDestroy(self.charset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FcCharSetWr {
|
||||
#[allow(dead_code)]
|
||||
pub fn new_with_str(s: &str) -> Result<Self, String> {
|
||||
let charset;
|
||||
unsafe {
|
||||
let charset_ptr = bindgen::FcCharSetCreate();
|
||||
if charset_ptr.is_null() {
|
||||
return Err(String::from("Failed to create FcCharSet with str"));
|
||||
}
|
||||
charset = FcCharSetWr {
|
||||
charset: charset_ptr,
|
||||
};
|
||||
for c in s.chars() {
|
||||
if bindgen::FcCharSetAddChar(charset.charset, c as u32)
|
||||
== bindgen::FcFalse as bindgen::FcBool
|
||||
{
|
||||
return Err(String::from("Failed to add chars from str into FcCharSet"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(charset)
|
||||
}
|
||||
|
||||
pub fn new_with_char(c: char) -> Result<Self, String> {
|
||||
let charset;
|
||||
unsafe {
|
||||
let charset_ptr = bindgen::FcCharSetCreate();
|
||||
if charset_ptr.is_null() {
|
||||
return Err(String::from("Failed to create FcCharSet with char"));
|
||||
}
|
||||
charset = FcCharSetWr {
|
||||
charset: charset_ptr,
|
||||
};
|
||||
if bindgen::FcCharSetAddChar(charset.charset, c as u32)
|
||||
== bindgen::FcFalse as bindgen::FcBool
|
||||
{
|
||||
return Err(String::from("Failed to add char to FcCharSet"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(charset)
|
||||
}
|
||||
|
||||
pub fn get(&mut self) -> *mut bindgen::FcCharSet {
|
||||
self.charset
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FcPatternWr {
|
||||
pattern: *mut bindgen::FcPattern,
|
||||
}
|
||||
|
||||
impl Drop for FcPatternWr {
|
||||
fn drop(&mut self) {
|
||||
if !self.pattern.is_null() {
|
||||
unsafe {
|
||||
bindgen::FcPatternDestroy(self.pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FcPatternWr {
|
||||
pub fn new_with_charset(c: &mut FcCharSetWr) -> Result<Self, String> {
|
||||
let pattern;
|
||||
unsafe {
|
||||
let pattern_ptr = bindgen::FcPatternCreate();
|
||||
if pattern_ptr.is_null() {
|
||||
return Err(String::from("Failed to FcPatternCreate"));
|
||||
}
|
||||
pattern = Self {
|
||||
pattern: pattern_ptr,
|
||||
};
|
||||
bindgen::FcDefaultSubstitute(pattern.pattern);
|
||||
|
||||
let value = bindgen::FcValue {
|
||||
type_: bindgen::_FcType_FcTypeCharSet,
|
||||
u: bindgen::_FcValue__bindgen_ty_1 { c: c.get() },
|
||||
};
|
||||
|
||||
if bindgen::FcPatternAdd(
|
||||
pattern.pattern,
|
||||
bindgen::FC_CHARSET as *const _ as *const i8,
|
||||
value,
|
||||
bindgen::FcTrue as bindgen::FcBool,
|
||||
) == bindgen::FcFalse as bindgen::FcBool
|
||||
{
|
||||
return Err(String::from("Failed to add FcCharSet to new Pattern"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(pattern)
|
||||
}
|
||||
|
||||
pub fn get(&mut self) -> *mut bindgen::FcPattern {
|
||||
self.pattern
|
||||
}
|
||||
|
||||
pub fn get_count(&self) -> c_int {
|
||||
unsafe { bindgen::FcPatternObjectCount(self.pattern) }
|
||||
}
|
||||
|
||||
pub fn filter_to_filenames(&self) -> Result<Self, String> {
|
||||
let pattern;
|
||||
unsafe {
|
||||
let mut file_object_set_filter = FcObjectSetWr::new_file_object_set()?;
|
||||
let pattern_ptr =
|
||||
bindgen::FcPatternFilter(self.pattern, file_object_set_filter.get());
|
||||
if pattern_ptr.is_null() {
|
||||
return Err(String::from("Failed to FcPatternFilter"));
|
||||
}
|
||||
pattern = Self {
|
||||
pattern: pattern_ptr,
|
||||
};
|
||||
}
|
||||
|
||||
Ok(pattern)
|
||||
}
|
||||
|
||||
pub fn get_filename_contents(&self) -> Result<Vec<String>, String> {
|
||||
let mut vec: Vec<String> = Vec::new();
|
||||
let count = self.get_count();
|
||||
unsafe {
|
||||
let mut value = bindgen::FcValue {
|
||||
type_: 0,
|
||||
u: bindgen::_FcValue__bindgen_ty_1 { i: 0 },
|
||||
};
|
||||
|
||||
for i in 0..count {
|
||||
if bindgen::FcPatternGet(
|
||||
self.pattern,
|
||||
bindgen::FC_FILE as *const _ as *const i8,
|
||||
i,
|
||||
&mut value as *mut bindgen::FcValue,
|
||||
) == bindgen::_FcResult_FcResultMatch
|
||||
&& value.type_ == bindgen::_FcType_FcTypeString
|
||||
{
|
||||
let cs = CStr::from_ptr(value.u.s as *const i8);
|
||||
vec.push(
|
||||
cs.to_str()
|
||||
.map_err(|_| String::from("Failed to convert CStr to String"))?
|
||||
.to_owned(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(vec)
|
||||
}
|
||||
}
|
||||
|
||||
struct FcObjectSetWr {
|
||||
object_set: *mut bindgen::FcObjectSet,
|
||||
}
|
||||
|
||||
impl Drop for FcObjectSetWr {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
if !self.object_set.is_null() {
|
||||
bindgen::FcObjectSetDestroy(self.object_set);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FcObjectSetWr {
|
||||
pub fn new_file_object_set() -> Result<Self, String> {
|
||||
let object_set;
|
||||
unsafe {
|
||||
let object_set_ptr = bindgen::FcObjectSetCreate();
|
||||
if object_set_ptr.is_null() {
|
||||
return Err(String::from("Failed to FcObjectSetCreate"));
|
||||
}
|
||||
|
||||
object_set = Self {
|
||||
object_set: object_set_ptr,
|
||||
};
|
||||
|
||||
if bindgen::FcObjectSetAdd(
|
||||
object_set.object_set,
|
||||
bindgen::FC_FILE as *const _ as *const i8,
|
||||
) == bindgen::FcFalse as bindgen::FcBool
|
||||
{
|
||||
return Err(String::from(
|
||||
"Failed to add \"FC_FILE\" with FcObjectSetAdd",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(object_set)
|
||||
}
|
||||
|
||||
pub fn get(&mut self) -> *mut bindgen::FcObjectSet {
|
||||
self.object_set
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_matching_font_from_str(s: &str) -> Result<PathBuf, String> {
|
||||
let mut config = ffi::FcConfigWr::new()?;
|
||||
let mut charset = ffi::FcCharSetWr::new_with_str(s)?;
|
||||
let mut search_pattern = ffi::FcPatternWr::new_with_charset(&mut charset)?;
|
||||
if !config.apply_pattern_to_config(&mut search_pattern) {
|
||||
return Err(String::from("Failed to apply_pattern_to_config"));
|
||||
}
|
||||
let result_pattern = config.font_match(&mut search_pattern)?;
|
||||
let filtered_pattern = result_pattern.filter_to_filenames()?;
|
||||
let result_vec = filtered_pattern.get_filename_contents()?;
|
||||
|
||||
if result_vec.is_empty() {
|
||||
Err(String::from(
|
||||
"Empty result_vec for get_matching_font_from_str",
|
||||
))
|
||||
} else {
|
||||
PathBuf::from_str(&result_vec[0]).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_matching_font_from_char(c: char) -> Result<PathBuf, String> {
|
||||
let mut config = ffi::FcConfigWr::new()?;
|
||||
let mut charset = ffi::FcCharSetWr::new_with_char(c)?;
|
||||
let mut search_pattern = ffi::FcPatternWr::new_with_charset(&mut charset)?;
|
||||
if !config.apply_pattern_to_config(&mut search_pattern) {
|
||||
return Err(String::from("Failed to apply_pattern_to_config"));
|
||||
}
|
||||
let result_pattern = config.font_match(&mut search_pattern)?;
|
||||
let filtered_pattern = result_pattern.filter_to_filenames()?;
|
||||
let result_vec = filtered_pattern.get_filename_contents()?;
|
||||
|
||||
if result_vec.is_empty() {
|
||||
Err(String::from(
|
||||
"Empty result_vec for get_matching_font_from_char",
|
||||
))
|
||||
} else {
|
||||
PathBuf::from_str(&result_vec[0]).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_ascii_fetching() {
|
||||
let fetched_path =
|
||||
get_matching_font_from_char('a').expect("Should be able to find match for 'a'");
|
||||
println!("{:?}", fetched_path);
|
||||
}
|
||||
}
|
|
@ -1,160 +0,0 @@
|
|||
use std::path::Path;
|
||||
|
||||
mod ffi {
|
||||
use freetype::freetype::{
|
||||
FT_Done_Face, FT_Done_FreeType, FT_Face, FT_FaceRec_, FT_Get_Char_Index, FT_Init_FreeType,
|
||||
FT_Library, FT_ModuleRec_, FT_Open_Args, FT_Open_Face, FT_Parameter_, FT_StreamRec_,
|
||||
FT_OPEN_PATHNAME,
|
||||
};
|
||||
use std::ffi::CString;
|
||||
use std::path::Path;
|
||||
|
||||
pub struct FTLibrary {
|
||||
library: FT_Library,
|
||||
faces: Vec<FT_Face>,
|
||||
}
|
||||
|
||||
impl Drop for FTLibrary {
|
||||
fn drop(&mut self) {
|
||||
for face in &self.faces {
|
||||
unsafe {
|
||||
FT_Done_Face(*face);
|
||||
}
|
||||
}
|
||||
if !self.library.is_null() {
|
||||
unsafe {
|
||||
FT_Done_FreeType(self.library);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FTLibrary {
|
||||
pub fn new() -> Option<FTLibrary> {
|
||||
unsafe {
|
||||
let mut library_ptr: FT_Library = 0 as FT_Library;
|
||||
if FT_Init_FreeType(&mut library_ptr) == 0 {
|
||||
Some(FTLibrary {
|
||||
library: library_ptr,
|
||||
faces: Vec::new(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self) -> FT_Library {
|
||||
self.library
|
||||
}
|
||||
|
||||
pub fn init_faces(&mut self, args: &mut FTOpenArgs) -> Result<(), String> {
|
||||
unsafe {
|
||||
let mut face: FT_Face = 0 as FT_Face;
|
||||
// first get number of faces
|
||||
let mut result = FT_Open_Face(
|
||||
self.get(),
|
||||
args.get_ptr(),
|
||||
-1,
|
||||
&mut face as *mut *mut FT_FaceRec_,
|
||||
);
|
||||
if result != 0 {
|
||||
FT_Done_Face(face);
|
||||
return Err(String::from("Failed to get number of faces"));
|
||||
}
|
||||
let count = (*face).num_faces;
|
||||
|
||||
for i in 0..count {
|
||||
result = FT_Open_Face(
|
||||
self.get(),
|
||||
args.get_ptr(),
|
||||
i,
|
||||
&mut face as *mut *mut FT_FaceRec_,
|
||||
);
|
||||
if result != 0 {
|
||||
FT_Done_Face(face);
|
||||
return Err(String::from("Failed to fetch face"));
|
||||
}
|
||||
self.faces.push(face);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn drop_faces(&mut self) {
|
||||
for face in &self.faces {
|
||||
unsafe {
|
||||
FT_Done_Face(*face);
|
||||
}
|
||||
}
|
||||
self.faces.clear();
|
||||
}
|
||||
|
||||
pub fn has_char(&self, c: char) -> bool {
|
||||
let char_value: u64 = c as u64;
|
||||
|
||||
for face in &self.faces {
|
||||
unsafe {
|
||||
let result = FT_Get_Char_Index(*face, char_value);
|
||||
if result != 0 {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FTOpenArgs {
|
||||
args: FT_Open_Args,
|
||||
// "args" has a pointer to the CString in "pathname", so it must be kept
|
||||
#[allow(dead_code)]
|
||||
pathname: Option<CString>,
|
||||
}
|
||||
|
||||
impl FTOpenArgs {
|
||||
pub fn new_with_path(path: &Path) -> Self {
|
||||
unsafe {
|
||||
let cstring: CString = CString::from_vec_unchecked(
|
||||
path.as_os_str().to_str().unwrap().as_bytes().to_vec(),
|
||||
);
|
||||
let args = FT_Open_Args {
|
||||
flags: FT_OPEN_PATHNAME,
|
||||
memory_base: std::ptr::null::<u8>(),
|
||||
memory_size: 0,
|
||||
pathname: cstring.as_ptr() as *mut i8,
|
||||
stream: std::ptr::null_mut::<FT_StreamRec_>(),
|
||||
driver: std::ptr::null_mut::<FT_ModuleRec_>(),
|
||||
num_params: 0,
|
||||
params: std::ptr::null_mut::<FT_Parameter_>(),
|
||||
};
|
||||
|
||||
FTOpenArgs {
|
||||
args,
|
||||
pathname: Some(cstring),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_args(&self) -> FT_Open_Args {
|
||||
self.args
|
||||
}
|
||||
|
||||
pub fn get_ptr(&mut self) -> *mut FT_Open_Args {
|
||||
&mut self.args as *mut FT_Open_Args
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn font_has_char(c: char, font_path: &Path) -> Result<bool, String> {
|
||||
let mut library =
|
||||
ffi::FTLibrary::new().ok_or_else(|| String::from("Failed to get FTLibrary"))?;
|
||||
let mut args = ffi::FTOpenArgs::new_with_path(font_path);
|
||||
library.init_faces(&mut args)?;
|
||||
|
||||
Ok(library.has_char(c))
|
||||
}
|
Loading…
Reference in a new issue