Initial commit

This commit is contained in:
Leon Grünewald 2024-12-09 23:19:51 +01:00
commit 84b17515a6
3 changed files with 2055 additions and 0 deletions

1839
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

12
Cargo.toml Normal file
View file

@ -0,0 +1,12 @@
[package]
name = "sticker-usage-analyzer"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
rocket = "0.5"
maud = { version = "0.26", features = ["rocket"] }
chrono = "0.4"
rayon = "1.10"

204
src/main.rs Normal file
View file

@ -0,0 +1,204 @@
use maud::{html, Markup, DOCTYPE};
use rocket::{Rocket,Build,launch,get,routes,fs::{FileServer}};
use std::collections::HashMap;
use chrono::prelude::*;
use rayon::prelude::*;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
enum MediaType {
Sticker,
Animation,
VideoFile,
AudioFile,
VoiceMessage,
VideoMessage
}
#[derive(Debug, Clone)]
struct ScoredChatMessage {
pub chat_message: ChatMessage,
pub last_used: i64,
pub times: u32
}
impl From<ChatMessage> for ScoredChatMessage {
fn from(item: ChatMessage) -> ScoredChatMessage {
ScoredChatMessage {
chat_message: item.clone(),
last_used: item.date_unixtime.parse().expect("Could not parse date_unixtime"),
times: 1
}
}
}
impl From<ScoredChatMessage> for Markup {
fn from(item: ScoredChatMessage) -> Markup {
if let Some(mime_type) = &item.chat_message.mime_type {
let message_file = item.chat_message.file.expect("No sticker file");
let message_date = DateTime::from_timestamp(item.last_used, 0).expect("Could not parse items");
match mime_type.as_str() {
"image/webp" => {
return html! {
.sticker {
img loading="lazy" src=(message_file);
p { "Times used: " (item.times) br; (message_date.format("%Y-%m-%d %H:%M:%S")) }
}
};
}
"video/webm" => {
return html! {
.sticker.animated {
video loading="lazy" autoplay loop controls src=(message_file) {}
p { "Times used: " (item.times) br; (message_date.format("%Y-%m-%d %H:%M:%S")) }
}
}
}
"application/x-tgsticker" => {
return html! {
.sticker.animated {
tgs-player loading="lazy" autoplay loop controls src=(message_file) {}
p { "Times used: " (item.times) br; (message_date.format("%Y-%m-%d %H:%M:%S")) }
}
}
}
_ => {
return html! {
p { "Unknown type " (mime_type) }
};
}
}
} else {
return html! {
p { "No type" }
};
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct ChatMessage {
pub media_type: Option<MediaType>,
pub mime_type: Option<String>,
pub file: Option<String>,
pub date_unixtime: String
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct Chat {
name: String,
messages: Vec<ChatMessage>
}
#[launch]
fn rocket() -> Rocket<Build> {
rocket::build()
.mount("/", routes![index])
.mount("/stickers", FileServer::from("./stickers"))
.mount("/video_files", FileServer::from("./video_files"))
}
#[get("/")]
fn index() -> Markup {
println!("Parsing JSON... (this can take a moment)");
let tg_export_result: Chat = serde_json::from_reader(
std::fs::OpenOptions::new().read(true).open("./result.json").expect("Could not open ./result.json")
).expect("Could not parse result.json");
println!("Done!");
let messages: Vec<ChatMessage> = tg_export_result.messages.into_par_iter().filter(|m| {
return if let Some(media_type) = &m.media_type {
*media_type == MediaType::Sticker
} else {
false
}
}).collect();
let mut messages: Vec<ScoredChatMessage> = messages.into_iter().fold(HashMap::new(), |mut acc: HashMap<String, ScoredChatMessage>, message| {
let file = message.file.as_ref().expect("No file").to_owned();
if acc.contains_key(&file) {
if let Some(scored_chat_message) = acc.get_mut(&file) {
let message_date = message.date_unixtime.parse().expect("Could not parse date_unixtime");
if scored_chat_message.last_used < message_date {
scored_chat_message.last_used = message_date;
}
scored_chat_message.times = scored_chat_message.times + 1;
}
} else {
acc.insert(file, ScoredChatMessage::from(message));
}
acc
})
.into_values()
.collect();
messages.sort_by(|a, b| b.times.cmp(&a.times));
return html! {
(DOCTYPE)
head {
script src="https://unpkg.com/@lottiefiles/lottie-player@0.4.0/dist/tgs-player.js" {}
}
body {
style {
r#"* { box-sizing: border-box; }
h1 {text-align: center;}
body {
width: 100vw;
margin: 0;
padding: 0;
}
.container {
max-width: 1024px;
margin: 0 auto;
}
.container,
.stickers,
.sticker {
display: flex;
position: relative;
flex-flow: row wrap;
}
.sticker {
flex: 0 0 25%;
align-items: flex-end;
padding: 1rem;
border: 1px solid #00000055;
}
.sticker p {
flex: 1;
text-align: center;
}
img, video, tgs-player {
width: 100%;
height: auto;
margin: 0 auto;
aspect-ratio: 1 / 1;
object-fit: contain;
}"#
}
.container {
h1 { "Stickers with " (tg_export_result.name) }
.stickers {
@for message in messages {
(<ScoredChatMessage as Into<Markup>>::into(message))
}
}
}
}
};
}