bookmark 0.1.3

Import, search, and open bookmarks from all browsers
Documentation
use anyhow::Result;
use chrono::{DateTime, Utc};
use std::path::Path;

use super::{Bookmark, HistoryEntry, UrlEntry};

pub fn extract_bookmarks(profile_path: &Path) -> Result<Option<Vec<Bookmark>>> {
    let bookmarks_path = profile_path.join("Bookmarks");
    if !bookmarks_path.exists() {
        return Ok(None);
    }

    let content = std::fs::read_to_string(bookmarks_path)?;
    let json: serde_json::Value = serde_json::from_str(&content)?;

    Ok(Some(parse_chrome_bookmarks(&json)?))
}

pub fn extract_history(profile_path: &Path) -> Result<Option<HistoryEntry>> {
    let history_path = profile_path.join("History");
    if !history_path.exists() {
        return Ok(None);
    }

    let conn = rusqlite::Connection::open(&history_path)?;

    let mut stmt = conn.prepare(
        "SELECT url, title, visit_count, last_visit_time 
         FROM urls 
         ORDER BY last_visit_time DESC 
         LIMIT 10000",
    )?;

    let rows = stmt.query_map([], |row| {
        Ok(UrlEntry {
            url: row.get(0)?,
            title: row.get(1)?,
            visit_count: row.get(2)?,
            last_visit: row.get::<_, Option<i64>>(3)?.map(|ts| {
                DateTime::from_timestamp((ts - 11644473600000000) / 1000000, 0)
                    .unwrap_or_else(Utc::now)
            }),
        })
    })?;

    let mut urls = Vec::new();
    for row in rows {
        urls.push(row?);
    }

    Ok(Some(HistoryEntry { urls }))
}

fn parse_chrome_bookmarks(json: &serde_json::Value) -> Result<Vec<Bookmark>> {
    let mut bookmarks = Vec::new();

    if let Some(roots) = json.get("roots").and_then(|r| r.as_object()) {
        for (folder_name, folder_data) in roots {
            bookmarks.extend(parse_bookmark_folder(
                folder_data,
                Some(folder_name.clone()),
            )?);
        }
    }

    Ok(bookmarks)
}

fn parse_bookmark_folder(
    folder: &serde_json::Value,
    folder_name: Option<String>,
) -> Result<Vec<Bookmark>> {
    let mut bookmarks = Vec::new();

    if let Some(children) = folder.get("children").and_then(|c| c.as_array()) {
        for child in children {
            if let Some(obj) = child.as_object() {
                if obj.get("type").and_then(|t| t.as_str()) == Some("url") {
                    let bookmark = Bookmark {
                        id: obj
                            .get("id")
                            .and_then(|i| i.as_str())
                            .unwrap_or("")
                            .to_string(),
                        title: obj
                            .get("name")
                            .and_then(|n| n.as_str())
                            .unwrap_or("")
                            .to_string(),
                        url: obj
                            .get("url")
                            .and_then(|u| u.as_str())
                            .map(|s| s.to_string()),
                        folder: folder_name.clone(),
                        date_added: obj
                            .get("date_added")
                            .and_then(|d| d.as_str())
                            .and_then(|s| s.parse::<i64>().ok())
                            .map(|ts| {
                                DateTime::from_timestamp((ts - 11644473600000000) / 1000000, 0)
                                    .unwrap_or_else(Utc::now)
                            }),
                        children: None,
                    };
                    bookmarks.push(bookmark);
                } else if obj.get("type").and_then(|t| t.as_str()) == Some("folder") {
                    let subfolder_name = obj
                        .get("name")
                        .and_then(|n| n.as_str())
                        .unwrap_or("")
                        .to_string();
                    let full_folder_name = match folder_name {
                        Some(ref parent) => format!("{}/{}", parent, subfolder_name),
                        None => subfolder_name,
                    };
                    bookmarks.extend(parse_bookmark_folder(child, Some(full_folder_name))?);
                }
            }
        }
    }

    Ok(bookmarks)
}