feat: Fully implemented instagram service
This commit is contained in:
parent
386e8abe5b
commit
7c23064afe
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
||||
/target
|
||||
project.zip
|
||||
|
39
Cargo.lock
generated
39
Cargo.lock
generated
@ -28,6 +28,15 @@ dependencies = [
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.18"
|
||||
@ -571,6 +580,7 @@ dependencies = [
|
||||
"clap",
|
||||
"dialoguer",
|
||||
"encoding",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tabled",
|
||||
@ -732,6 +742,35 @@ dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.24"
|
||||
|
@ -14,3 +14,4 @@ async-trait = "0.1.48"
|
||||
encoding = "0.2"
|
||||
zip = "0.6"
|
||||
tokio-util = "0.7"
|
||||
regex = "1.10"
|
||||
|
@ -5,7 +5,7 @@ use dialoguer::Select;
|
||||
use crate::{services::{instagram::Instagram, service::Service}, types::{participant::Participant, project::Project, user::User}};
|
||||
|
||||
pub async fn add(path: String) {
|
||||
let mut raw_project = Project::load(path).await.unwrap_or_else(|e| {
|
||||
let mut raw_project = Project::load(path.clone()).await.unwrap_or_else(|e| {
|
||||
eprintln!("Error while loading a project: {}", e);
|
||||
exit(1);
|
||||
});
|
||||
@ -59,5 +59,10 @@ pub async fn add(path: String) {
|
||||
}
|
||||
|
||||
println!("Starting to merge messages...");
|
||||
final_service.merge_messages(&mut raw_project, &user_map)
|
||||
final_service.merge_messages(&mut raw_project, &user_map).await;
|
||||
|
||||
raw_project.project.sort_messages();
|
||||
|
||||
println!("Saving all messages to a project file.");
|
||||
let _ = raw_project.save(&path).await;
|
||||
}
|
||||
|
@ -3,13 +3,13 @@ use std::process::exit;
|
||||
use dialoguer::{Input, Select};
|
||||
use tabled::Table;
|
||||
|
||||
use crate::types::{project::Project, user::User};
|
||||
use crate::types::{project::{Project, ProjectRaw}, user::User};
|
||||
|
||||
pub async fn users(path: String) {
|
||||
let project = Project::load(path.clone()).await.unwrap_or_else(|e| {
|
||||
eprintln!("Error while loading a project: {}", e);
|
||||
exit(1);
|
||||
}).project;
|
||||
});
|
||||
|
||||
menu(project, path).await;
|
||||
}
|
||||
@ -24,12 +24,13 @@ pub fn print_users(users: &Vec<User>) {
|
||||
println!("{}", users);
|
||||
}
|
||||
|
||||
pub async fn menu(mut project: Project, path: String) {
|
||||
pub async fn menu(mut project_raw: ProjectRaw, path: String) {
|
||||
let project = &mut project_raw.project;
|
||||
println!("User management:");
|
||||
print_users(&project.users);
|
||||
println!("\n");
|
||||
|
||||
let options = vec!["Create a new user", "Remove a user", "Save and exit"];
|
||||
let options = vec!["Create a new user", "Remove a user", "Save and exit", "Exit without saving"];
|
||||
|
||||
let option = Select::new()
|
||||
.with_prompt("Choose a option")
|
||||
@ -52,12 +53,12 @@ pub async fn menu(mut project: Project, path: String) {
|
||||
|
||||
project.users.push(user);
|
||||
|
||||
Box::pin(menu(project, path)).await;
|
||||
Box::pin(menu(project_raw, path)).await;
|
||||
},
|
||||
1 => {
|
||||
if project.users.is_empty() {
|
||||
println!("You do not have any users created! No users to be removed!");
|
||||
Box::pin(menu(project, path)).await;
|
||||
Box::pin(menu(project_raw, path)).await;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -74,15 +75,18 @@ pub async fn menu(mut project: Project, path: String) {
|
||||
|
||||
project.users.remove(option_remove);
|
||||
|
||||
Box::pin(menu(project, path)).await;
|
||||
Box::pin(menu(project_raw, path)).await;
|
||||
},
|
||||
2 => {
|
||||
// Save and exit
|
||||
let _ = project.save(path).await;
|
||||
let _ = project_raw.save(&path).await;
|
||||
},
|
||||
3 => {
|
||||
// Exit without saving
|
||||
}
|
||||
_ => {
|
||||
println!("Error! Unknown option!");
|
||||
Box::pin(menu(project, path)).await;
|
||||
Box::pin(menu(project_raw, path)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,8 @@ async fn main() {
|
||||
timestamps: HashSet::new(),
|
||||
media_index: 0,
|
||||
};
|
||||
let _ = new_project.save(path).await;
|
||||
let _ = new_project.save_new(path).await;
|
||||
println!("New project was successfully created!");
|
||||
}
|
||||
|
||||
if let Some(path) = cli.users {
|
||||
|
@ -1,5 +1,6 @@
|
||||
use std::{collections::HashMap, fs, io, path::PathBuf, process::exit};
|
||||
|
||||
use regex::Regex;
|
||||
use async_trait::async_trait;
|
||||
use dialoguer::{Input, Select};
|
||||
use encoding::{all::ISO_8859_1, EncoderTrap, Encoding};
|
||||
@ -24,7 +25,6 @@ pub struct InstagramMessage {
|
||||
}
|
||||
|
||||
impl InstagramMessage {
|
||||
// TODO: Take into consideration the user_map
|
||||
pub fn conv(&self, media_index: &mut usize, project_path: PathBuf, user_map: &HashMap<Participant, User>) -> Message {
|
||||
let text_content = if let Some(content) = &self.content {
|
||||
Some(decode_str(content))
|
||||
@ -61,14 +61,18 @@ impl InstagramPhoto {
|
||||
*media_index += 1;
|
||||
let img_path = (*media_index - 1).to_string();
|
||||
|
||||
original_path.pop();
|
||||
for _ in 0..4 {
|
||||
if !original_path.pop() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
original_path.push(self.uri.clone());
|
||||
|
||||
Photo {
|
||||
text_content: text_content.clone(),
|
||||
img_path,
|
||||
original_img_path: original_path.display().to_string()
|
||||
original_img_path: Some(original_path.display().to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -142,12 +146,15 @@ impl Service for Instagram {
|
||||
let data = self.data.as_mut().unwrap();
|
||||
let path = PathBuf::from(&data.project_path);
|
||||
|
||||
let pattern = r".*/message_.+\.json$";
|
||||
let re = Regex::new(pattern).unwrap();
|
||||
|
||||
let paths: Vec<PathBuf> = fs::read_dir(&path).unwrap_or_else(|_| {
|
||||
eprintln!("Folder not found!");
|
||||
exit(1);
|
||||
})
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| entry.path().is_file())
|
||||
.filter(|entry| entry.path().is_file() && re.is_match(&entry.path().display().to_string()))
|
||||
.map(|entry| { entry.path() }).collect();
|
||||
|
||||
for (index, path) in paths.iter().enumerate() {
|
||||
@ -193,9 +200,10 @@ impl Service for Instagram {
|
||||
}).collect()
|
||||
}
|
||||
|
||||
fn merge_messages(&self, raw_project: &mut ProjectRaw, user_map: &HashMap<Participant, User>) {
|
||||
async fn merge_messages(&self, raw_project: &mut ProjectRaw, user_map: &HashMap<Participant, User>) {
|
||||
let data = self.data.as_ref().unwrap();
|
||||
let path = PathBuf::from(data.project_path.clone());
|
||||
let mut media_files: Vec<(String, String)> = vec![];
|
||||
|
||||
for message in &data.messages {
|
||||
let mut media_index_clone = raw_project.project.media_index.clone();
|
||||
@ -205,13 +213,19 @@ impl Service for Instagram {
|
||||
|
||||
if moved {
|
||||
// Move the file to the media folder
|
||||
|
||||
for photo in msg.photos.clone() {
|
||||
let original_path = photo.original_img_path.unwrap().clone();
|
||||
let path = format!("media/{}", photo.img_path);
|
||||
let photovalues = (path, original_path);
|
||||
media_files.push(photovalues);
|
||||
}
|
||||
|
||||
// Update the highest media index of the project if the message was merged
|
||||
raw_project.project.media_index = media_index_clone;
|
||||
}
|
||||
|
||||
println!("{:?}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
let _ = raw_project.save_media_files(media_files).await;
|
||||
}
|
||||
}
|
||||
|
@ -11,5 +11,5 @@ pub trait Service {
|
||||
async fn load(&mut self) -> io::Result<()>;
|
||||
|
||||
fn get_participants(&self) -> Vec<Participant>;
|
||||
fn merge_messages(&self, raw_project: &mut ProjectRaw, user_map: &HashMap<Participant, User>);
|
||||
async fn merge_messages(&self, raw_project: &mut ProjectRaw, user_map: &HashMap<Participant, User>);
|
||||
}
|
||||
|
@ -13,6 +13,6 @@ pub struct Photo {
|
||||
pub text_content: Option<String>,
|
||||
pub img_path: String,
|
||||
|
||||
#[serde(skip_serializing)]
|
||||
pub original_img_path: String,
|
||||
#[serde(skip)]
|
||||
pub original_img_path: Option<String>,
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ impl Project {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn save(&self, file_path: String) -> io::Result<()> {
|
||||
pub async fn save_new(&self, file_path: String) -> io::Result<()> {
|
||||
let contents = serde_json::to_string(&self)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
|
||||
@ -89,4 +89,107 @@ impl Project {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn sort_messages(&mut self) {
|
||||
self.messages.sort_by_key(|msg| msg.timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl ProjectRaw {
|
||||
/// Save multiple files to the zip archive.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `files` - A list of tuples where each tuple contains:
|
||||
/// - `target_path`: The path inside the zip archive (e.g., `media/1`).
|
||||
/// - `source_path`: The path to the file on the filesystem.
|
||||
pub async fn save_media_files(
|
||||
&mut self,
|
||||
files: Vec<(String, String)>,
|
||||
) -> io::Result<()> {
|
||||
let mut new_buffer = Cursor::new(Vec::new());
|
||||
{
|
||||
let mut zip_writer = ZipWriter::new(&mut new_buffer);
|
||||
let options = FileOptions::default().unix_permissions(0o644);
|
||||
|
||||
let replace_files: HashSet<_> = files.iter().map(|(target, _)| target.clone()).collect();
|
||||
|
||||
for i in 0..self.zip.len() {
|
||||
let mut file = self.zip.by_index(i)?;
|
||||
let name = file.name().to_string();
|
||||
|
||||
if replace_files.contains(&name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
zip_writer.start_file(name, options)?;
|
||||
let mut file_content = Vec::new();
|
||||
file.read_to_end(&mut file_content)?;
|
||||
zip_writer.write_all(&file_content)?;
|
||||
}
|
||||
|
||||
for (target_path, source_path) in files {
|
||||
let mut file = File::open(source_path).await?;
|
||||
let mut file_content = Vec::new();
|
||||
file.read_to_end(&mut file_content).await?;
|
||||
|
||||
zip_writer.start_file(target_path, options)?;
|
||||
zip_writer.write_all(&file_content)?;
|
||||
}
|
||||
|
||||
zip_writer.finish()?;
|
||||
}
|
||||
|
||||
self.zip = ZipArchive::new(new_buffer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Save the `Project` to `project.json` and flush the zip archive to the specified file.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `output_path` - The path to save the zip archive to.
|
||||
pub async fn save(&mut self, output_path: &str) -> io::Result<()> {
|
||||
let project_json = serde_json::to_string(&self.project)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
|
||||
let mut new_buffer = Cursor::new(Vec::new());
|
||||
{
|
||||
let mut zip_writer = ZipWriter::new(&mut new_buffer);
|
||||
let options = FileOptions::default().unix_permissions(0o644);
|
||||
|
||||
let mut project_json_written = false;
|
||||
|
||||
for i in 0..self.zip.len() {
|
||||
let mut file = self.zip.by_index(i)?;
|
||||
let name = file.name();
|
||||
|
||||
if name == "project.json" {
|
||||
// Replace the existing `project.json`.
|
||||
zip_writer.start_file("project.json", options)?;
|
||||
zip_writer.write_all(project_json.as_bytes())?;
|
||||
project_json_written = true;
|
||||
} else {
|
||||
// Copy other files as-is.
|
||||
zip_writer.start_file(name, options)?;
|
||||
let mut file_content = Vec::new();
|
||||
file.read_to_end(&mut file_content)?;
|
||||
zip_writer.write_all(&file_content)?;
|
||||
}
|
||||
}
|
||||
|
||||
if !project_json_written {
|
||||
zip_writer.start_file("project.json", options)?;
|
||||
zip_writer.write_all(project_json.as_bytes())?;
|
||||
}
|
||||
|
||||
zip_writer.finish()?;
|
||||
}
|
||||
|
||||
let mut output_file = File::create(output_path).await?;
|
||||
output_file.write_all(&new_buffer.into_inner()).await?;
|
||||
output_file.flush().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user