ebook 0.1.2

A CLI tool for reading, writing, and operating on various ebook formats
Documentation
use crate::{Metadata, Result, EbookError};
use quick_xml::events::{Event, BytesEnd, BytesStart, BytesText};
use quick_xml::{Reader, Writer};
use std::io::Cursor;

#[derive(Debug, Clone, Default)]
pub struct ComicInfo {
    pub title: Option<String>,
    pub series: Option<String>,
    pub number: Option<String>,
    pub volume: Option<String>,
    pub summary: Option<String>,
    pub publisher: Option<String>,
    pub writer: Option<String>,
    pub penciller: Option<String>,
    pub inker: Option<String>,
    pub colorist: Option<String>,
    pub letterer: Option<String>,
    pub cover_artist: Option<String>,
    pub editor: Option<String>,
    pub year: Option<String>,
    pub month: Option<String>,
    pub day: Option<String>,
    pub language_iso: Option<String>,
    pub page_count: Option<u32>,
    pub genre: Option<String>,
    pub tags: Vec<String>,
    pub web: Option<String>,
}

impl ComicInfo {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn from_metadata(metadata: &Metadata) -> Self {
        let mut comic_info = Self::new();
        
        comic_info.title = metadata.title.clone();
        comic_info.publisher = metadata.publisher.clone();
        comic_info.summary = metadata.description.clone();
        comic_info.language_iso = metadata.language.clone();
        
        if let Some(author) = &metadata.author {
            comic_info.writer = Some(author.clone());
        }
        
        if let Some(tags) = &metadata.tags {
            comic_info.tags = tags.clone();
        }
        
        comic_info
    }

    pub fn to_metadata(&self) -> Metadata {
        let mut metadata = Metadata::new();
        
        metadata.title = self.title.clone();
        metadata.publisher = self.publisher.clone();
        metadata.description = self.summary.clone();
        metadata.language = self.language_iso.clone();
        metadata.author = self.writer.clone();
        metadata.format = Some("CBZ".to_string());
        
        if !self.tags.is_empty() {
            metadata.tags = Some(self.tags.clone());
        }
        
        metadata
    }

    pub fn parse_xml(xml_content: &str) -> Result<Self> {
        let mut reader = Reader::from_str(xml_content);
        reader.config_mut().trim_text(true);
        
        let mut comic_info = Self::new();
        let mut buf = Vec::new();
        let mut current_tag = String::new();
        
        loop {
            match reader.read_event_into(&mut buf) {
                Ok(Event::Start(e)) => {
                    current_tag = String::from_utf8_lossy(e.name().as_ref()).to_string();
                }
                Ok(Event::Text(e)) => {
                    let text = e.unescape().unwrap_or_default().to_string();
                    match current_tag.as_str() {
                        "Title" => comic_info.title = Some(text),
                        "Series" => comic_info.series = Some(text),
                        "Number" => comic_info.number = Some(text),
                        "Volume" => comic_info.volume = Some(text),
                        "Summary" => comic_info.summary = Some(text),
                        "Publisher" => comic_info.publisher = Some(text),
                        "Writer" => comic_info.writer = Some(text),
                        "Penciller" => comic_info.penciller = Some(text),
                        "Inker" => comic_info.inker = Some(text),
                        "Colorist" => comic_info.colorist = Some(text),
                        "Letterer" => comic_info.letterer = Some(text),
                        "CoverArtist" => comic_info.cover_artist = Some(text),
                        "Editor" => comic_info.editor = Some(text),
                        "Year" => comic_info.year = Some(text),
                        "Month" => comic_info.month = Some(text),
                        "Day" => comic_info.day = Some(text),
                        "LanguageISO" => comic_info.language_iso = Some(text),
                        "PageCount" => {
                            if let Ok(count) = text.parse::<u32>() {
                                comic_info.page_count = Some(count);
                            }
                        }
                        "Genre" => comic_info.genre = Some(text),
                        "Tags" => {
                            for tag in text.split(',') {
                                let trimmed = tag.trim();
                                if !trimmed.is_empty() {
                                    comic_info.tags.push(trimmed.to_string());
                                }
                            }
                        }
                        "Web" => comic_info.web = Some(text),
                        _ => {}
                    }
                }
                Ok(Event::Eof) => break,
                Err(e) => return Err(EbookError::Xml(e.to_string())),
                _ => {}
            }
            buf.clear();
        }
        
        Ok(comic_info)
    }

    pub fn to_xml(&self) -> Result<String> {
        let mut writer = Writer::new(Cursor::new(Vec::new()));
        
        writer.write_event(Event::Decl(quick_xml::events::BytesDecl::new("1.0", Some("utf-8"), None)))?;
        
        let mut comic_info_elem = BytesStart::new("ComicInfo");
        comic_info_elem.push_attribute(("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"));
        comic_info_elem.push_attribute(("xmlns:xsd", "http://www.w3.org/2001/XMLSchema"));
        writer.write_event(Event::Start(comic_info_elem))?;
        
        self.write_element(&mut writer, "Title", &self.title)?;
        self.write_element(&mut writer, "Series", &self.series)?;
        self.write_element(&mut writer, "Number", &self.number)?;
        self.write_element(&mut writer, "Volume", &self.volume)?;
        self.write_element(&mut writer, "Summary", &self.summary)?;
        self.write_element(&mut writer, "Publisher", &self.publisher)?;
        self.write_element(&mut writer, "Writer", &self.writer)?;
        self.write_element(&mut writer, "Penciller", &self.penciller)?;
        self.write_element(&mut writer, "Inker", &self.inker)?;
        self.write_element(&mut writer, "Colorist", &self.colorist)?;
        self.write_element(&mut writer, "Letterer", &self.letterer)?;
        self.write_element(&mut writer, "CoverArtist", &self.cover_artist)?;
        self.write_element(&mut writer, "Editor", &self.editor)?;
        self.write_element(&mut writer, "Year", &self.year)?;
        self.write_element(&mut writer, "Month", &self.month)?;
        self.write_element(&mut writer, "Day", &self.day)?;
        self.write_element(&mut writer, "LanguageISO", &self.language_iso)?;
        
        if let Some(page_count) = self.page_count {
            self.write_element(&mut writer, "PageCount", &Some(page_count.to_string()))?;
        }
        
        self.write_element(&mut writer, "Genre", &self.genre)?;
        
        if !self.tags.is_empty() {
            let tags_str = self.tags.join(", ");
            self.write_element(&mut writer, "Tags", &Some(tags_str))?;
        }
        
        self.write_element(&mut writer, "Web", &self.web)?;
        
        writer.write_event(Event::End(BytesEnd::new("ComicInfo")))?;
        
        let result = writer.into_inner().into_inner();
        String::from_utf8(result)
            .map_err(|e| EbookError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))
    }
    
    fn write_element<W: std::io::Write>(
        &self,
        writer: &mut Writer<W>,
        tag: &str,
        value: &Option<String>,
    ) -> Result<()> {
        if let Some(val) = value {
            writer.write_event(Event::Start(BytesStart::new(tag)))?;
            writer.write_event(Event::Text(BytesText::new(val)))?;
            writer.write_event(Event::End(BytesEnd::new(tag)))?;
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_comic_info_parse_and_generate() {
        let xml = r#"<?xml version="1.0"?>
<ComicInfo>
    <Title>Test Comic</Title>
    <Series>Test Series</Series>
    <Number>1</Number>
    <Writer>Test Writer</Writer>
    <Publisher>Test Publisher</Publisher>
    <Summary>A test comic book</Summary>
    <LanguageISO>en</LanguageISO>
    <PageCount>24</PageCount>
    <Tags>Action, Adventure</Tags>
</ComicInfo>"#;

        let comic_info = ComicInfo::parse_xml(xml).unwrap();
        assert_eq!(comic_info.title, Some("Test Comic".to_string()));
        assert_eq!(comic_info.series, Some("Test Series".to_string()));
        assert_eq!(comic_info.writer, Some("Test Writer".to_string()));
        assert_eq!(comic_info.page_count, Some(24));
        assert_eq!(comic_info.tags.len(), 2);
    }

    #[test]
    fn test_comic_info_to_xml() {
        let mut comic_info = ComicInfo::new();
        comic_info.title = Some("Test Comic".to_string());
        comic_info.writer = Some("Test Writer".to_string());
        comic_info.page_count = Some(24);
        
        let xml = comic_info.to_xml().unwrap();
        assert!(xml.contains("<Title>Test Comic</Title>"));
        assert!(xml.contains("<Writer>Test Writer</Writer>"));
        assert!(xml.contains("<PageCount>24</PageCount>"));
    }

    #[test]
    fn test_metadata_conversion() {
        let mut metadata = Metadata::new();
        metadata.title = Some("Comic Title".to_string());
        metadata.author = Some("Comic Author".to_string());
        metadata.publisher = Some("Comic Publisher".to_string());
        
        let comic_info = ComicInfo::from_metadata(&metadata);
        assert_eq!(comic_info.title, Some("Comic Title".to_string()));
        assert_eq!(comic_info.writer, Some("Comic Author".to_string()));
        
        let converted_metadata = comic_info.to_metadata();
        assert_eq!(converted_metadata.title, Some("Comic Title".to_string()));
        assert_eq!(converted_metadata.author, Some("Comic Author".to_string()));
    }
}