aboutsummaryrefslogtreecommitdiff
path: root/import
diff options
context:
space:
mode:
Diffstat (limited to 'import')
-rw-r--r--import/Cargo.toml1
-rw-r--r--import/fallback_generator/Cargo.toml10
-rw-r--r--import/fallback_generator/src/lib.rs103
-rw-r--r--import/src/lib.rs21
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(),
},
});