diff options
author | metamuffin <metamuffin@disroot.org> | 2025-04-26 22:23:13 +0200 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2025-04-26 22:23:13 +0200 |
commit | 1d0fbc5c471db162ccc2c164273955515e918116 (patch) | |
tree | ddb7082abae7eed07ce83d7c5dee0bc0e7128398 /import | |
parent | 546b5a6b5b5ae5f0604c8fafb1c21b9c12b85d24 (diff) | |
download | jellything-1d0fbc5c471db162ccc2c164273955515e918116.tar jellything-1d0fbc5c471db162ccc2c164273955515e918116.tar.bz2 jellything-1d0fbc5c471db162ccc2c164273955515e918116.tar.zst |
fancier fallback images
Diffstat (limited to 'import')
-rw-r--r-- | import/Cargo.toml | 1 | ||||
-rw-r--r-- | import/fallback_generator/Cargo.toml | 10 | ||||
-rw-r--r-- | import/fallback_generator/src/lib.rs | 103 | ||||
-rw-r--r-- | import/src/lib.rs | 21 |
4 files changed, 134 insertions, 1 deletions
diff --git a/import/Cargo.toml b/import/Cargo.toml index 37b5a77..ad9d529 100644 --- a/import/Cargo.toml +++ b/import/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" jellybase = { path = "../base" } jellyclient = { path = "../client" } jellyremuxer = { path = "../remuxer" } +jellyimport-fallback-generator = { path = "fallback_generator" } rayon = "1.10.0" crossbeam-channel = "0.5.14" diff --git a/import/fallback_generator/Cargo.toml b/import/fallback_generator/Cargo.toml new file mode 100644 index 0000000..13823ba --- /dev/null +++ b/import/fallback_generator/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "jellyimport-fallback-generator" +version = "0.1.0" +edition = "2024" + +[dependencies] +ab_glyph = "0.2.29" +imageproc = "0.25.0" +image = { version = "0.25.6", default-features = false, features = ["qoi"] } +anyhow = "1.0.98" diff --git a/import/fallback_generator/src/lib.rs b/import/fallback_generator/src/lib.rs new file mode 100644 index 0000000..efb6579 --- /dev/null +++ b/import/fallback_generator/src/lib.rs @@ -0,0 +1,103 @@ +#![feature(random, string_remove_matches)] +use ab_glyph::{FontRef, PxScale}; +use anyhow::Result; +use image::{DynamicImage, ImageBuffer, ImageEncoder, Rgba, codecs::qoi::QoiEncoder}; +use imageproc::drawing::{draw_text_mut, text_size}; +use std::{ + hash::{Hash, Hasher}, + io::Write, +}; + +pub fn generate_fallback(name: &str, output: &mut dyn Write) -> Result<()> { + let width = 1024; + let height = (width * 1000) / 707; + + let mut image = ImageBuffer::<Rgba<f32>, Vec<f32>>::new(width, height); + + let text = name; + let bg = random_accent(text, 0.4); + image.pixels_mut().for_each(|p| *p = bg); + + let font = FontRef::try_from_slice(include_bytes!( + "/usr/share/fonts/cantarell/Cantarell-VF.otf" + )) + .unwrap(); + let font_bold = FontRef::try_from_slice(include_bytes!( + "/usr/share/fonts/TTF/OpenSans-CondensedExtraBold.ttf" + )) + .unwrap(); + + let mut bgtext = text.to_string(); + bgtext.remove_matches(&[',', ' ']); + let bgtext = bgtext.repeat(3); + let scale = PxScale { x: 1000., y: 1000. }; + let (w, h) = text_size(scale, &font, &bgtext); + for i in -1..4 { + draw_text_mut( + &mut image, + random_accent(&text, 0.6), + width as i32 / 2 - w as i32 / 2 + i * h as i32, + i * h as i32 * 2 / 3, + scale, + &font_bold, + &bgtext, + ); + } + + image.enumerate_pixels_mut().for_each(|(_x, y, p)| { + let f = 1. - (y as f32 / height as f32) * 0.5; + p.0[0] *= f; + p.0[1] *= f; + p.0[2] *= f; + }); + + let scale = PxScale { x: 200., y: 200. }; + let (w, _h) = text_size(scale, &font, text); + draw_text_mut( + &mut image, + Rgba([1., 1., 1., 1.]), + width as i32 / 2 - w as i32 / 2, + height as i32 * 3 / 4, + scale, + &font, + text, + ); + + let image = DynamicImage::from(image).to_rgb8(); + + QoiEncoder::new(output).write_image( + image.as_raw(), + image.width(), + image.height(), + image::ExtendedColorType::Rgb8, + )?; + Ok(()) +} + +struct XorshiftHasher(u64); +impl Hasher for XorshiftHasher { + fn finish(&self) -> u64 { + self.0 + } + fn write(&mut self, bytes: &[u8]) { + for b in bytes { + self.0 = self + .0 + .wrapping_add((*b as u64).wrapping_shl(24) + (*b as u64).wrapping_shl(56)); + self.0 ^= self.0.wrapping_shl(13); + self.0 ^= self.0.wrapping_shr(7); + self.0 ^= self.0.wrapping_shl(17); + } + } +} + +fn random_accent(text: &str, y: f32) -> Rgba<f32> { + let mut hasher = XorshiftHasher(0); + text.hash(&mut hasher); + let h = hasher.finish(); + let mut u = (h >> 32) as u32 as f32 / u32::MAX as f32; + let mut v = (h) as u32 as f32 / u32::MAX as f32; + u *= 0.2; + v *= 0.2; + Rgba([y - u * 0.5 - v * 0.5, y + v, y + u, 1.]) +} diff --git a/import/src/lib.rs b/import/src/lib.rs index b93dbec..0d72da2 100644 --- a/import/src/lib.rs +++ b/import/src/lib.rs @@ -9,6 +9,7 @@ use anyhow::{anyhow, bail, Context, Result}; use infojson::YVideo; use jellybase::{ assetfed::AssetInner, + cache::cache_file, common::{Chapter, MediaInfo, Node, NodeID, NodeKind, Rating, SourceTrack, SourceTrackKind}, database::Database, CONF, SECRETS, @@ -17,6 +18,7 @@ use jellyclient::{ Appearance, LocalTrack, ObjectIds, PeopleGroup, Person, TmdbKind, TrackSource, TraktKind, Visibility, }; +use jellyimport_fallback_generator::generate_fallback; use jellyremuxer::metadata::checked_matroska_metadata; use log::info; use musicbrainz::MusicBrainz; @@ -702,6 +704,9 @@ fn apply_musicbrainz_recording( .insert("musicbrainz.artist".to_string(), a.artist.id.to_string()); } + // // TODO proper dedup + // node.people.clear(); + for rel in &rec.relations { use musicbrainz::reltypes::*; let a = match rel.type_id.as_str() { @@ -757,6 +762,20 @@ fn apply_musicbrainz_recording( jobs.push(note.to_string()); } jobs.extend(rel.attributes.clone()); + + let headshot = match image_1.or(image_2) { + Some(x) => x, + None => AssetInner::Cache(cache_file( + "person-headshot-fallback", + &artist.sort_name, + |mut file| { + generate_fallback(&artist.sort_name, &mut file)?; + Ok(()) + }, + )?) + .ser(), + }; + node.people.entry(group).or_default().push(Appearance { jobs, characters: vec![], @@ -766,7 +785,7 @@ fn apply_musicbrainz_recording( } else { rel.target_credit.clone() }, - headshot: image_1.or(image_2), + headshot: Some(headshot), ids: ObjectIds::default(), }, }); |