119 lines
3.1 KiB
Rust
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()
|
|
}
|