Files
amane-config/src/art.rs
T

119 lines
3.1 KiB
Rust

use std::hash::{DefaultHasher, Hash, Hasher};
use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;
use std::{env, fs, thread};
use amane::{Media, Service};
use crate::island::{self, Island};
use crate::config::island::timing;
use crate::parse;
/*
* the shown player's cover as a local file Image can draw: file:// links are
* decoded, https:// ones (spotify) downloaded once. a copy also goes to one
* fixed path the Palette watches, so the background glow follows the cover
*/
#[derive(Default)]
pub struct Art {
url: String,
// empty when there is no cover
pub path: String,
}
impl Service for Art {
fn new() -> Self {
Self::default()
}
fn listen() {
loop {
let url = shown_art_url();
if url != Self::read().url {
let path = local_copy(&url);
if !path.is_empty() {
let _ = fs::copy(&path, palette_file());
}
*Self::write() = Self { url, path };
}
// the song playing, kept for the card shown while nothing plays
let (url, path) = {
let art = Self::read();
(art.url.clone(), art.path.clone())
};
crate::last::watch(&url, &path);
// 4 checks a second: after next/previous the new cover follows the title quickly
thread::sleep(Duration::from_millis(timing::cover_check_ms()));
}
}
}
fn shown_art_url() -> String {
let selected = Island::read().selected.clone();
let media = Media::read();
island::shown(&media, selected.as_deref()).map_or(String::new(), |player| player.art_url().to_string())
}
fn local_copy(url: &str) -> String {
if let Some(path) = parse::file_path(url) {
return path;
}
if !url.starts_with("https://") && !url.starts_with("http://") {
return String::new();
}
let mut hasher = DefaultHasher::new();
url.hash(&mut hasher);
let path = folder().join(format!("{:016x}", hasher.finish()));
if !path.exists() {
// downloaded beside, then moved, so Image never caches a half written file
let partial = path.with_extension("part");
// no shell: the url comes from another program and must never run as a command
let fetched = Command::new("curl")
.args(["-sfL", "--max-time", "10", "-o"])
.arg(&partial)
.arg(url)
.status()
.is_ok_and(|status| status.success());
if !fetched || fs::rename(&partial, &path).is_err() {
let _ = fs::remove_file(&partial);
return String::new();
}
}
path.display().to_string()
}
// tmpfs under XDG_RUNTIME_DIR, so downloaded covers and equalizer configs go away at logout
pub fn folder() -> PathBuf {
let base = env::var_os("XDG_RUNTIME_DIR").map_or_else(env::temp_dir, PathBuf::from);
let folder = base.join("magic-island");
let _ = fs::create_dir_all(&folder);
folder
}
pub fn palette_file() -> String {
folder().join("palette-cover").display().to_string()
}