ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Rust与Tauri构建高性能跨平台媒体刮削器实战指南

基于Rust与Tauri构建高性能跨平台媒体刮削器实战指南 在构建个人媒体库时你是否也遇到过这样的困扰从网络下载的电影、剧集文件命名混乱海报、简介等元信息缺失手动整理耗时费力而现有的媒体服务器如Jellyfin、Plex自带的刮削器对中文支持不佳或者速度慢、匹配不准如果你是一名开发者可能还希望有一个能高度自定义、性能出色且易于二次开发的工具。本文将为你深入解析一个由 Rust、TypeScript、Tauri、React 和 Mantine 等技术栈构建的开源媒体刮削器项目。它不仅旨在解决上述痛点更提供了一个从技术选型、架构设计到具体实现的完整实战案例。无论你是想直接使用一个强大的刮削工具还是希望学习如何用现代技术栈构建高性能的跨平台桌面应用这篇文章都将为你提供详尽的指南。1. 项目背景与核心概念为什么需要自研刮削器1.1 什么是媒体刮削器媒体刮削器Media Scraper是一种自动化工具用于从互联网上的特定数据库如 The Movie Database, TheTVDB获取影视作品的元数据。这些元数据通常包括基本信息标题、原名、上映年份、简介。多媒体信息海报、背景图、剧集缩略图。分类信息类型、导演、演员、评分、片长。刮削器通过匹配本地文件的文件名如The.Matrix.1999.1080p.mkv向在线API发起查询并将获取的元数据以特定格式如NFO文件保存或直接写入媒体库数据库从而使杂乱的本地文件变得井然有序便于媒体服务器识别和展示。1.2 现有方案的不足与自研动机市面上已有如tinyMediaManager、FileBot等优秀工具以及Jellyfin、Emby内置的刮削器。但对于开发者或高阶用户它们可能存在以下局限性能与资源占用部分基于Java或Python的工具在扫描大量文件时启动慢、内存占用高。定制化程度低规则固定难以适配特殊的文件命名习惯或小众媒体类型。中文支持与源质量对中文元数据如豆瓣的支持参差不齐匹配准确率有待提升。技术栈陈旧不便基于其代码进行二次开发或集成到自己的系统中。因此一个采用现代高性能语言Rust编写核心逻辑结合高效前端框架React提供友好界面并能打包为跨平台桌面应用Tauri的刮削器成为了一个理想的选择。1.3 本项目的技术栈全景本项目是一个典型的前后端分离的跨平台桌面应用技术选型颇具匠心后端/核心 (Rust): 负责文件系统扫描、网络请求、数据解析、业务逻辑和本地数据持久化。Rust保证了极高的执行效率和内存安全。本地数据库 (Sled): 一个用Rust编写的嵌入式数据库类似B树结构的Key-Value存储非常适合桌面应用场景无需额外安装数据库服务。前端界面 (React TypeScript Mantine): 构建用户交互界面。TypeScript提供了良好的类型安全Mantine是一套功能丰富、现代的React组件库能快速搭建美观的UI。应用框架 (Tauri): 用于将Rust后端和Web前端打包成一个轻量级、安全的桌面应用程序。相比ElectronTauri利用系统自带的WebView打包体积更小性能更好。这个组合兼顾了性能、开发体验、安全性和最终用户体验。2. 环境准备与开发工具在开始编码或运行本项目前需要配置好相应的开发环境。以下是各环节所需的工具和版本建议。2.1 Rust 开发环境Rust是本项目的核心需要安装Rust工具链。# 推荐使用 rustup 安装和管理 Rust # 访问 https://rustup.rs/ 获取安装脚本 # 安装完成后验证安装 rustc --version # 预期输出类似rustc 1.77.0 (aedd173a2 2024-03-17) cargo --version # 预期输出类似cargo 1.77.0 (c4b8d843c 2024-03-26) # 安装 wasm32-unknown-unknown 目标如果涉及 WebAssembly rustup target add wasm32-unknown-unknown2.2 Node.js 与前端环境前端部分需要Node.js运行环境和包管理器。# 推荐使用 nvm 管理 Node.js 版本 # 安装 Node.js (LTS版本如 18.x, 20.x) node --version # 预期输出类似v20.11.0 npm --version # 预期输出类似10.2.4 # 也可以使用 yarn 或 pnpm yarn --version2.3 Tauri CLI 工具Tauri提供了命令行工具来创建、开发和构建项目。# 使用 Cargo 安装 Tauri CLI cargo install tauri-cli # 验证安装 tauri --version2.4 IDE 或代码编辑器推荐Visual Studio Code: 对Rust、TypeScript、Tauri都有非常好的插件支持。必备插件rust-analyzer(Rust语言支持),TypeScript and JavaScript(内置),Tauri(官方扩展)。IntelliJ IDEA (Rust插件)或CLion: 适合深度Rust开发。2.5 项目结构与初始化一个典型的Tauri项目结构如下your_scraper_project/ ├── src-tauri/ # Rust后端代码 │ ├── Cargo.toml # Rust项目配置和依赖 │ ├── src/ │ │ ├── main.rs # 后端入口点 │ │ ├── lib.rs # 核心库定义 │ │ └── ... # 其他模块scraper, db, utils等 │ └── build.rs # 构建脚本 ├── src/ # 前端React源代码 │ ├── main.tsx # 前端入口点 │ ├── App.tsx # 主应用组件 │ ├── components/ # React组件 │ └── styles/ ├── index.html # 前端主页面 ├── package.json # 前端项目配置和依赖 ├── tsconfig.json # TypeScript配置 ├── vite.config.ts # Vite构建配置Tauri默认使用Vite └── README.md你可以使用Tauri CLI快速创建一个基础模板# 按照提示选择前端框架React TypeScript和UI库 cargo tauri init3. 核心模块设计与 Rust 后端实现后端是刮削器的“大脑”我们将其拆分为几个核心模块。3.1 文件系统扫描模块这个模块负责递归扫描指定目录识别视频文件并提取可用于查询的“指纹”信息如文件名、目录名、文件大小等。// src-tauri/src/scanner.rs use std::path::{Path, PathBuf}; use walkdir::WalkDir; // 需要添加 walkdir 依赖 use regex::Regex; // 需要添加 regex 依赖 #[derive(Debug, Clone)] pub struct MediaFile { pub path: PathBuf, pub file_name: String, pub parent_dir: OptionString, pub size: u64, pub fingerprint: MediaFingerprint, } #[derive(Debug, Clone)] pub struct MediaFingerprint { pub clean_title: String, pub year: Optioni32, pub season: Optionu32, pub episode: Optionu32, pub quality: OptionString, } pub fn scan_directory(dir_path: Path) - ResultVecMediaFile, Boxdyn std::error::Error { let mut media_files Vec::new(); let video_extensions [mkv, mp4, avi, mov, wmv, flv, webm]; for entry in WalkDir::new(dir_path).into_iter().filter_map(|e| e.ok()) { let path entry.path(); if path.is_file() { if let Some(ext) path.extension().and_then(|e| e.to_str()) { if video_extensions.contains(ext.to_lowercase().as_str()) { if let Some(file) parse_media_file(path) { media_files.push(file); } } } } } Ok(media_files) } fn parse_media_file(path: Path) - OptionMediaFile { // 使用正则表达式从文件名中提取关键信息 // 例如The.Matrix.1999.1080p.BluRay.x264.mkv let re Regex::new(r(?i)(.?)[\.\s](\d{4})[\.\s].*?(1080p|720p|4k)?).unwrap(); let file_name path.file_name()?.to_string_lossy().to_string(); let caps re.captures(file_name)?; let clean_title caps.get(1)?.as_str().replace(., ).trim().to_string(); let year caps.get(2)?.as_str().parse::i32().ok(); let quality caps.get(3).map(|m| m.as_str().to_string()); let fingerprint MediaFingerprint { clean_title, year, season: None, // 剧集需要更复杂的解析 episode: None, quality, }; let metadata std::fs::metadata(path).ok()?; Some(MediaFile { path: path.to_path_buf(), file_name, parent_dir: path.parent().and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string())), size: metadata.len(), fingerprint, }) }3.2 网络请求与 API 客户端模块该模块负责与TMDB等在线元数据提供商进行通信。我们需要处理HTTP请求、API密钥管理和JSON解析。// src-tauri/src/api/tmdb_client.rs use reqwest::Client; // 需要添加 reqwest 依赖 use serde::{Deserialize, Serialize}; // 需要添加 serde 依赖 use std::collections::HashMap; const TMDB_BASE_URL: str https://api.themoviedb.org/3; const TMDB_IMAGE_BASE_URL: str https://image.tmdb.org/t/p; #[derive(Debug, Deserialize)] pub struct TmdbMovieSearchResult { pub results: VecTmdbMovie, } #[derive(Debug, Deserialize, Clone)] pub struct TmdbMovie { pub id: i32, pub title: String, pub original_title: String, pub overview: OptionString, pub release_date: OptionString, pub poster_path: OptionString, pub backdrop_path: OptionString, pub vote_average: f32, } pub struct TmdbClient { client: Client, api_key: String, language: String, } impl TmdbClient { pub fn new(api_key: String, language: String) - Self { Self { client: Client::new(), api_key, language, } } pub async fn search_movie( self, query: str, year: Optioni32, ) - ResultVecTmdbMovie, reqwest::Error { let mut params HashMap::new(); params.insert(api_key, self.api_key.as_str()); params.insert(query, query); params.insert(language, self.language); if let Some(y) year { params.insert(year, y.to_string()); } let url format!({}/search/movie, TMDB_BASE_URL); let response self.client.get(url).query(params).send().await?; let search_result: TmdbMovieSearchResult response.json().await?; Ok(search_result.results) } pub fn get_poster_url(self, path: OptionString, size: str) - OptionString { path.map(|p| format!({}/{}{}, TMDB_IMAGE_BASE_URL, size, p)) } }3.3 数据持久化模块 (Sled)使用Sled数据库来缓存API响应、存储用户配置和任务状态避免重复请求并提升离线体验。// src-tauri/src/db/mod.rs use sled::{Db, IVec}; use serde::{Deserialize, Serialize}; use bincode; // 需要添加 bincode 依赖 #[derive(Clone)] pub struct AppDatabase { db: Db, } impl AppDatabase { pub fn new(path: std::path::Path) - ResultSelf, sled::Error { let db sled::open(path)?; Ok(Self { db }) } // 存储序列化的结构体 pub fn insert_serializedK, V(self, key: K, value: V) - Result(), Boxdyn std::error::Error where K: AsRef[u8], V: Serialize, { let encoded: Vecu8 bincode::serialize(value)?; self.db.insert(key, encoded)?; Ok(()) } // 读取并反序列化结构体 pub fn get_deserializedK, V(self, key: K) - ResultOptionV, Boxdyn std::error::Error where K: AsRef[u8], V: forde Deserializede, { if let Some(ivec) self.db.get(key)? { let decoded: V bincode::deserialize(ivec)?; Ok(Some(decoded)) } else { Ok(None) } } // 示例缓存电影搜索结果 pub fn cache_movie_search( self, query: str, year: Optioni32, movies: [TmdbMovie], ) - Result(), Boxdyn std::error::Error { let cache_key format!(movie_search:{}:{}, query, year.unwrap_or(0)); self.insert_serialized(cache_key.as_bytes(), movies)?; Ok(()) } pub fn get_cached_movie_search( self, query: str, year: Optioni32, ) - ResultOptionVecTmdbMovie, Boxdyn std::error::Error { let cache_key format!(movie_search:{}:{}, query, year.unwrap_or(0)); self.get_deserialized(cache_key.as_bytes()) } }3.4 核心刮削逻辑与任务调度这是串联所有模块的“指挥官”。它需要管理扫描、匹配、下载元数据、保存结果等一系列可能耗时的任务并考虑并发和错误处理。// src-tauri/src/scraper_engine.rs use tokio::task; // 需要添加 tokio 依赖 use std::sync::Arc; use crate::scanner::{scan_directory, MediaFile}; use crate::api::tmdb_client::TmdbClient; use crate::db::AppDatabase; pub struct ScraperEngine { tmdb_client: ArcTmdbClient, db: ArcAppDatabase, } impl ScraperEngine { pub fn new(tmdb_client: TmdbClient, db: AppDatabase) - Self { Self { tmdb_client: Arc::new(tmdb_client), db: Arc::new(db), } } pub async fn scrape_directory(self, dir_path: str) - ResultScrapeReport, Boxdyn std::error::Error { let path std::path::Path::new(dir_path); let media_files scan_directory(path)?; let mut tasks Vec::new(); for media_file in media_files { let client self.tmdb_client.clone(); let db self.db.clone(); let task task::spawn(async move { Self::process_single_file(client, db, media_file).await }); tasks.push(task); } let mut report ScrapeReport::default(); for task in tasks { match task.await { Ok(Ok(file_report)) report.merge(file_report), Ok(Err(e)) { eprintln!(处理文件失败: {}, e); report.failed 1; } Err(join_err) { eprintln!(任务执行失败: {}, join_err); report.failed 1; } } } Ok(report) } async fn process_single_file( client: TmdbClient, db: AppDatabase, media_file: MediaFile, ) - ResultFileScrapeReport, Boxdyn std::error::Error { // 1. 检查缓存 if let Some(cached_movies) db.get_cached_movie_search(media_file.fingerprint.clean_title, media_file.fingerprint.year)? { // 使用缓存逻辑... } // 2. 调用API let search_results client.search_movie(media_file.fingerprint.clean_title, media_file.fingerprint.year).await?; // 3. 缓存结果 db.cache_movie_search(media_file.fingerprint.clean_title, media_file.fingerprint.year, search_results)?; // 4. 选择最佳匹配这里简化处理选择第一个 let chosen_movie search_results.first().ok_or(未找到匹配的电影)?; // 5. 生成NFO文件、下载图片等此处省略具体实现 // save_nfo(media_file.path, chosen_movie); // download_image(chosen_movie.poster_path, media_file.path); Ok(FileScrapeReport { file_name: media_file.file_name, matched_title: chosen_movie.title.clone(), success: true, }) } } #[derive(Default)] pub struct ScrapeReport { pub total: usize, pub succeeded: usize, pub failed: usize, pub details: VecFileScrapeReport, } pub struct FileScrapeReport { pub file_name: String, pub matched_title: String, pub success: bool, }4. 前端界面与 Tauri 集成实战前端负责提供直观的操作界面并通过Tauri的“指令”系统与Rust后端通信。4.1 使用 Mantine 构建基础UI首先安装Mantine及相关依赖。# 在项目根目录前端部分 npm install mantine/core mantine/hooks mantine/form emotion/react # 或使用 yarn/pnpm创建一个基本的布局和扫描页面组件。// src/App.tsx import { useState } from react; import { AppShell, Navbar, Header, Button, Group, Text, Stack, Progress, Table } from mantine/core; import { IconFolder, IconSearch, IconDatabase } from tabler/icons-react; import { invoke } from tauri-apps/api/tauri; import { open } from tauri-apps/api/dialog; function App() { const [directory, setDirectory] useStatestring(); const [scanning, setScanning] useState(false); const [progress, setProgress] useState(0); const [report, setReport] useStateScrapeReport | null(null); const selectDirectory async () { const selected await open({ directory: true, multiple: false, }); if (selected !Array.isArray(selected)) { setDirectory(selected); } }; const startScraping async () { if (!directory) return; setScanning(true); setReport(null); try { // 调用 Rust 后端的 scrape_directory 指令 const result: ScrapeReport await invoke(scrape_directory, { dirPath: directory }); setReport(result); } catch (error) { console.error(刮削失败:, error); } finally { setScanning(false); } }; return ( AppShell paddingmd navbar{ Navbar width{{ base: 300 }} pxs Navbar.Section grow Button leftIcon{IconFolder /} variantsubtle fullWidth justifystart文件管理/Button Button leftIcon{IconSearch /} variantsubtle fullWidth justifystart刮削任务/Button Button leftIcon{IconDatabase /} variantsubtle fullWidth justifystart元数据管理/Button /Navbar.Section /Navbar } header{ Header height{60} pxs Text sizexl weight{700}我的媒体刮削器/Text /Header } Stack spacingmd Group Text媒体库目录/Text Text colorblue{directory || 未选择}/Text Button onClick{selectDirectory} leftIcon{IconFolder /}选择目录/Button Button onClick{startScraping} loading{scanning} leftIcon{IconSearch /} disabled{!directory} 开始刮削 /Button /Group {scanning Progress value{progress} animate /} {report ( div Text sizelg刮削报告/Text Text总计{report.total}成功{report.succeeded}失败{report.failed}/Text Table thead tr th文件名/th th匹配结果/th th状态/th /tr /thead tbody {report.details.map((detail, idx) ( tr key{idx} td{detail.fileName}/td td{detail.matchedTitle}/td td{detail.success ? ✅ 成功 : ❌ 失败}/td /tr ))} /tbody /Table /div )} /Stack /AppShell ); } export default App;4.2 定义 Tauri 指令并暴露给前端在Rust后端我们需要创建与前端invoke调用对应的指令。// src-tauri/src/main.rs #![cfg_attr(not(debug_assertions), windows_subsystem windows)] mod scanner; mod api; mod db; mod scraper_engine; use scraper_engine::{ScraperEngine, ScrapeReport}; use db::AppDatabase; use api::tmdb_client::TmdbClient; use std::sync::Mutex; struct AppState { engine: MutexOptionScraperEngine, } // 初始化引擎的指令 #[tauri::command] fn init_engine(state: tauri::StateAppState, api_key: String, lang: String, db_path: String) - Result(), String { let tmdb_client TmdbClient::new(api_key, lang); let db AppDatabase::new(std::path::Path::new(db_path)).map_err(|e| e.to_string())?; let engine ScraperEngine::new(tmdb_client, db); *state.engine.lock().unwrap() Some(engine); Ok(()) } // 执行刮削的指令 #[tauri::command] async fn scrape_directory(dir_path: String, state: tauri::State_, AppState) - ResultScrapeReport, String { let engine_guard state.engine.lock().unwrap(); let engine engine_guard.as_ref().ok_or(引擎未初始化请先调用 init_engine.to_string())?; engine.scrape_directory(dir_path).await.map_err(|e| e.to_string()) } fn main() { tauri::Builder::default() .manage(AppState { engine: Mutex::new(None), }) .invoke_handler(tauri::generate_handler![init_engine, scrape_directory]) .run(tauri::generate_context!()) .expect(运行 Tauri 应用时出错); }同时需要在Cargo.toml中声明依赖和特性。# src-tauri/Cargo.toml [dependencies] tauri { version 1.0, features [api-all] } serde { version 1.0, features [derive] } reqwest { version 0.11, features [json] } tokio { version 1.0, features [full] } walkdir 2.3 regex 1.9 sled 0.34 bincode 1.3 [build-dependencies] tauri-build { version 1.0 }4.3 前端调用指令与状态管理在前端我们需要一个更健壮的状态来管理应用配置如API密钥和引擎初始化。// src/hooks/useScraperEngine.ts import { invoke } from tauri-apps/api/tauri; import { useState, useCallback } from react; interface ScrapeReport { total: number; succeeded: number; failed: number; details: Array{ fileName: string; matchedTitle: string; success: boolean; }; } export function useScraperEngine() { const [initialized, setInitialized] useState(false); const [scraping, setScraping] useState(false); const initEngine useCallback(async (apiKey: string, language: string zh-CN) { try { // 假设数据库路径固定或由用户选择 const dbPath await invokestring(get_app_data_dir); // 需要实现此指令来获取应用数据目录 await invoke(init_engine, { apiKey, lang: language, dbPath }); setInitialized(true); return true; } catch (error) { console.error(初始化引擎失败:, error); return false; } }, []); const scrapeDir useCallback(async (dirPath: string): PromiseScrapeReport | null { if (!initialized) { alert(请先配置API密钥并初始化引擎); return null; } setScraping(true); try { const report: ScrapeReport await invoke(scrape_directory, { dirPath }); return report; } catch (error) { console.error(刮削过程出错:, error); return null; } finally { setScraping(false); } }, [initialized]); return { initialized, scraping, initEngine, scrapeDir }; }5. 项目构建、调试与打包5.1 开发环境运行在项目根目录下使用以下命令启动开发环境# 启动开发服务器前端热重载 Tauri 窗口 cargo tauri dev这个命令会同时启动Vite开发服务器前端和Tauri应用窗口并支持热重载方便调试。5.2 生产环境构建开发完成后可以构建发布版本。# 构建应用根据系统生成安装包 cargo tauri build构建完成后安装包会位于src-tauri/target/release/bundle/目录下可能是.deb、.msi、.dmg或.app文件。5.3 配置 Tauri 应用信息在src-tauri/tauri.conf.json中配置应用元数据、权限和构建选项。{ package: { productName: 我的媒体刮削器, version: 0.1.0 }, tauri: { allowlist: { all: false, dialog: { open: true, save: false }, path: { all: true }, fs: { readFile: true, writeFile: true, readDir: true } }, bundle: { identifier: com.example.mediascraper, icon: [icons/32x32.png, icons/128x128.png, icons/128x1282x.png, icons/icon.icns, icons/icon.ico] }, security: { csp: null } } }6. 常见问题与排查思路在开发和使用过程中你可能会遇到以下问题问题现象可能原因排查与解决思路cargo tauri dev启动失败提示前端依赖问题Node.js 版本不兼容或node_modules损坏。1. 确认Node.js版本建议LTS。2. 删除node_modules和package-lock.json重新运行npm install。3. 检查vite.config.ts配置是否正确。Rust 编译错误找不到tauri等相关crateCargo.toml依赖未正确添加或网络问题。1. 检查Cargo.toml拼写和版本号。2. 运行cargo update更新依赖。3. 更换Cargo镜像源如rsproxy.cn。前端可以打开但调用Rust指令时报command not found指令未在main.rs中注册或函数签名不匹配。1. 确认#[tauri::command]宏已添加。2. 确认指令名在generate_handler!中注册。3. 检查前端invoke调用的指令名是否一致。扫描文件时程序卡死或无响应文件系统IO阻塞了主线程或递归扫描了符号链接循环。1. 确保文件扫描等耗时操作在tokio::spawn或独立线程中运行。2. 使用WalkDir的max_depth限制深度用follow_links(false)避免跟随符号链接。TMDB API 请求返回401 UnauthorizedAPI密钥无效、过期或未在请求中正确传递。1. 在TMDB官网检查API密钥状态。2. 在前端配置界面确认密钥已正确保存并传递给后端。3. 检查Rust客户端构造请求时api_key参数是否正确拼接。匹配准确率低文件名解析规则过于简单或TMDB中无对应条目。1. 优化parse_media_file中的正则表达式支持更多命名格式。2. 实现更智能的匹配算法如模糊匹配、比较年份。3. 考虑集成多个数据源如豆瓣API作为备选。打包后的应用体积过大包含了不必要的调试信息或资源。1. 确保使用cargo tauri build --release进行发布构建。2. 检查tauri.conf.json中的bundle设置移除未使用的资源。3. Tauri本身比Electron应用小很多如果仍过大检查前端依赖是否引入了过大的库。7. 最佳实践与进阶优化方向一个基础可用的刮削器已经搭建完成但要将其打造成“最好用”的工具还需要考虑以下工程实践和优化点。7.1 配置管理安全存储API密钥不要将API密钥硬编码在代码中。Tauri提供了tauri-plugin-store用于安全存储配置或者使用系统密钥链。支持多数据源配置允许用户配置TMDB、豆瓣等不同来源的API密钥和优先级。7.2 性能优化并发控制使用tokio的信号量或线程池限制并发网络请求和文件IO的数量避免被API限流或系统资源耗尽。增量扫描与缓存记录文件的最后修改时间只扫描新文件或发生变化的文件。充分利用Sled缓存API响应。前端虚拟列表当扫描结果很多时使用如tanstack/react-virtual实现虚拟滚动避免渲染成千上万行DOM导致界面卡顿。7.3 用户体验任务队列与进度反馈将刮削任务放入队列提供详细的进度条、当前处理文件名和预估剩余时间。手动匹配与修正当自动匹配失败或不准时提供界面让用户从搜索结果列表中选择正确的条目并记住用户选择。规则自定义提供界面让用户自定义文件名解析的正则表达式规则适应个人独特的文件命名习惯。7.4 代码质量与可维护性错误处理使用Rust的Result和?操作符进行链式错误处理在前端清晰展示错误信息而非直接崩溃。日志记录集成log和tracingcrate输出不同级别的日志到文件便于排查线上问题。单元测试与集成测试为核心的文件解析、API客户端逻辑编写单元测试。为Tauri指令编写集成测试。7.5 扩展性设计插件化架构将“扫描器”、“匹配器”、“元数据获取器”、“元数据写入器”设计为插件接口方便未来支持新的媒体类型如音乐、图书或新的元数据来源。支持剧集TV Shows剧集的刮削逻辑比电影复杂需要解析季和集的信息并匹配TVDB等剧集数据库。这需要扩展MediaFingerprint和对应的API客户端。导出与同步支持将刮削好的NFO和图片导出为Kodi、Jellyfin等媒体服务器兼容的格式或直接与这些服务器的数据库进行同步。通过这个项目你不仅得到了一个功能强大的个人工具更完成了一次全栈技术深度实践。从Rust高性能后端的编写到React TypeScript前端界面的构建再到通过Tauri将它们无缝融合为桌面应用每一步都涉及现代开发中的关键决策和实现细节。你可以在此基础上继续深化比如用Rust重写更复杂的文件名解析器或用React Flow设计一个可视化的刮削规则编辑器使其真正成为符合你所有期待的“最好用的开源刮削器”。
返回列表