trnovel 0.10.4

Terminal reader for novel
Documentation
use super::toc_rule::{TocRuleSet, detect};
use super::{Novel, NovelChapters, VolumeMarker};
use crate::cache::LocalNovelCache;
use crate::errors::Result;
use crate::history::HistoryItem;
use anyhow::anyhow;
use std::ops::{Deref, DerefMut};
use std::{
    io::SeekFrom,
    path::{Path, PathBuf},
    sync::Arc,
};
use tokio::fs::File;
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncSeekExt, BufReader};
use tokio::sync::Mutex;

#[derive(Debug, Clone)]
pub struct LocalNovel {
    pub file: Arc<Mutex<File>>,
    pub novel_chapters: NovelChapters<(String, usize)>,
    pub encoding: &'static encoding_rs::Encoding,
    pub path: PathBuf,
}

impl Deref for LocalNovel {
    type Target = NovelChapters<(String, usize)>;
    fn deref(&self) -> &Self::Target {
        &self.novel_chapters
    }
}

impl DerefMut for LocalNovel {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.novel_chapters
    }
}

impl LocalNovel {
    async fn from_cache(value: LocalNovelCache) -> Result<Self> {
        let file = File::open(&value.path).await?;
        Ok(Self {
            file: Arc::new(Mutex::new(file)),
            novel_chapters: NovelChapters {
                chapters: Some(value.chapters),
                current_chapter: value.current_chapter,
                line_percent: value.line_percent,
                volumes: value.volumes,
            },
            encoding: value.encoding,
            path: value.path,
        })
    }

    pub async fn from_path<T: AsRef<Path>>(path: T) -> Result<Self> {
        let path = path.as_ref().to_path_buf().canonicalize()?;
        match LocalNovelCache::try_from(path.as_path()) {
            Ok(cache) => Self::from_cache(cache).await,
            Err(_) => Self::new(path).await,
        }
    }

    pub async fn new<T>(path: T) -> Result<Self>
    where
        T: AsRef<Path>,
    {
        let path = path.as_ref().to_path_buf().canonicalize()?;

        let file = File::open(&path).await?;
        let encoding = Self::get_file_encoding(file).await?;

        let file = File::open(&path).await?;

        Ok(Self {
            file: Arc::new(Mutex::new(file)),
            novel_chapters: NovelChapters::new(),
            encoding,
            path,
        })
    }

    async fn get_file_encoding(mut file: File) -> std::io::Result<&'static encoding_rs::Encoding> {
        let mut buffer = vec![];

        file.read_to_end(&mut buffer).await?;
        if let (_, encoding, false) = encoding_rs::UTF_8.decode(&buffer) {
            return Ok(encoding);
        }

        if let (_, encoding, false) = encoding_rs::GBK.decode(&buffer) {
            return Ok(encoding);
        }

        Err(std::io::Error::new(
            std::io::ErrorKind::Unsupported,
            "Unsupported encoding",
        ))
    }
}

impl Novel for LocalNovel {
    type Chapter = (String, usize);
    type Args = PathBuf;

    async fn init(args: Self::Args) -> Result<Self> {
        Self::from_path(args).await
    }

    async fn request_toc(&self) -> Result<(Vec<Self::Chapter>, Vec<VolumeMarker>)> {
        let path = self.path.clone();
        let encoding = self.encoding;
        // 加载规则集(内置默认 + ~/.novel/toc_rules.json)。
        let rules = TocRuleSet::load();

        let file = File::open(path).await?;
        let mut buf_reader = BufReader::new(file);

        // 逐行收集 (解码后整行, 行起始字节偏移),偏移基于原始文件字节,供 get_content 按字节区间读取。
        let mut lines: Vec<(String, usize)> = Vec::new();
        let mut offset = 0usize;
        let mut line = vec![];

        while let Ok(chunk_size) = buf_reader.read_until(b'\n', &mut line).await {
            if chunk_size == 0 {
                break;
            }
            let (decoded, _, _) = encoding.decode(&line);
            lines.push((decoded.into_owned(), offset));
            line.clear();
            offset += chunk_size;
        }

        Ok(detect(lines, &rules))
    }

    async fn get_content(&self) -> Result<String> {
        let start = if self.current_chapter == 0 {
            0
        } else {
            let (_, start) = self.get_chapters_result()?[self.current_chapter];
            start.to_owned()
        };
        let is_last = self.get_chapters_result()?.is_empty()
            || self.current_chapter + 1 >= self.get_chapters_result()?.len();

        let end = self.chapters.as_ref().and_then(|chapters| {
            chapters
                .get(self.current_chapter + 1)
                .map(|chapter| chapter.1)
        });

        let file = self.file.clone();
        let encoding = self.encoding;
        let mut file = file.lock().await;

        let end = if is_last {
            file.metadata().await?.len() as usize
        } else {
            end.ok_or(anyhow!("找不到下一章"))?
        };

        let mut buffer = vec![0; end - start];
        file.seek(SeekFrom::Start(start as u64)).await?;
        file.read_exact(&mut buffer).await?;

        let (str, _, has_error) = encoding.decode(&buffer);
        if has_error {
            return Err(anyhow::anyhow!("解码错误").into());
        }
        Ok(str.to_string())
    }

    fn get_current_chapter_name(&self) -> Result<String> {
        self.get_current_chapter().map(|chapter| chapter.0)
    }

    fn get_chapters_names(&self) -> Result<Vec<(String, usize)>> {
        Ok(self
            .get_chapters_result()?
            .iter()
            .enumerate()
            .map(|(index, item)| (item.0.clone(), index))
            .collect())
    }

    fn to_history_item(&self) -> Result<HistoryItem> {
        let local_novel_cache = LocalNovelCache::try_from(self)?;
        local_novel_cache.save()?;
        Ok(local_novel_cache.into())
    }

    fn get_id(&self) -> String {
        self.path.to_string_lossy().to_string()
    }
}