feat: unp4k update

This commit is contained in:
xkeyC 2025-12-10 21:04:47 +08:00
parent c172b623d7
commit 23e909e330
7 changed files with 114 additions and 111 deletions

View File

@ -8,7 +8,7 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'package:freezed_annotation/freezed_annotation.dart' hide protected;
part 'unp4k_api.freezed.dart';
// These functions are ignored because they are not marked as `pub`: `dos_datetime_to_millis`, `ensure_files_loaded`
// These functions are ignored because they are not marked as `pub`: `dos_datetime_to_millis`, `ensure_files_loaded`, `p4k_get_entry`
/// P4K
Future<void> p4KOpen({required String p4KPath}) =>

View File

@ -399,10 +399,10 @@ class Unp4kCModel extends _$Unp4kCModel {
dPrint("extractFile .... $filePath -> $fullOutputPath");
// 使 Rust API
await unp4k_api.p4KExtractToDisk(filePath: filePath, outputPath: fullOutputPath);
await unp4k_api.p4KExtractToDisk(filePath: filePath, outputPath: outputPath);
if (mode == "extract_open") {
const textExt = [".txt", ".xml", ".json", ".lua", ".cfg", ".ini"];
const textExt = [".txt", ".xml", ".json", ".lua", ".cfg", ".ini", ".mtl"];
const imgExt = [".png"];
String openType = "unknown";
for (var element in textExt) {
@ -475,9 +475,7 @@ class Unp4kCModel extends _$Unp4kCModel {
current++;
onProgress?.call(current, total, entryPath);
final fullOutputPath = "$outputDir\\$entryPath";
await unp4k_api.p4KExtractToDisk(filePath: entryPath, outputPath: fullOutputPath);
await unp4k_api.p4KExtractToDisk(filePath: entryPath, outputPath: outputDir);
}
state = state.copyWith(endMessage: S.current.tools_unp4k_extract_completed(current));
@ -493,8 +491,7 @@ class Unp4kCModel extends _$Unp4kCModel {
return (false, 0, S.current.tools_unp4k_extract_cancelled);
}
final fullOutputPath = "$outputDir\\$filePath";
await unp4k_api.p4KExtractToDisk(filePath: filePath, outputPath: fullOutputPath);
await unp4k_api.p4KExtractToDisk(filePath: filePath, outputPath: outputDir);
state = state.copyWith(endMessage: S.current.tools_unp4k_extract_completed(1));
return (true, 1, null);
@ -609,9 +606,7 @@ class Unp4kCModel extends _$Unp4kCModel {
current++;
onProgress?.call(current, total, extractPath);
final fullOutputPath = "$outputDir\\$extractPath";
await unp4k_api.p4KExtractToDisk(filePath: extractPath, outputPath: fullOutputPath);
await unp4k_api.p4KExtractToDisk(filePath: extractPath, outputPath: outputDir);
}
state = state.copyWith(endMessage: S.current.tools_unp4k_extract_completed(current));

View File

@ -50,7 +50,7 @@ final class ToolsLogAnalyzeProvider
}
}
String _$toolsLogAnalyzeHash() => r'f5079c7d35daf25b07f83bacb224484171e9c93f';
String _$toolsLogAnalyzeHash() => r'4c1aea03394e5c5641b2eb40a31d37892bb978bf';
final class ToolsLogAnalyzeFamily extends $Family
with

42
rust/Cargo.lock generated
View File

@ -3404,15 +3404,6 @@ dependencies = [
"syn 2.0.111",
]
[[package]]
name = "matchers"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata",
]
[[package]]
name = "matches"
version = "0.1.10"
@ -3716,15 +3707,6 @@ dependencies = [
"zbus",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "num"
version = "0.2.1"
@ -4941,8 +4923,6 @@ dependencies = [
"tao",
"tokenizers",
"tokio",
"tracing",
"tracing-subscriber",
"unp4k_rs",
"url",
"uuid",
@ -6074,33 +6054,15 @@ dependencies = [
"tracing-subscriber",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex-automata",
"sharded-slab",
"smallvec 1.15.1",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
@ -6186,7 +6148,7 @@ checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3"
[[package]]
name = "unp4k_rs"
version = "0.1.0"
source = "git+https://github.com/StarCitizenToolBox/unp4k_rs?tag=V0.0.2#02867472dda1c18e81b0f635b8653fa86bd145cb"
source = "git+https://github.com/StarCitizenToolBox/unp4k_rs?rev=b55d64934bde37bb1079c2c3e2996c8286532914#b55d64934bde37bb1079c2c3e2996c8286532914"
dependencies = [
"aes",
"anyhow",
@ -6199,7 +6161,7 @@ dependencies = [
"globset",
"indicatif",
"quick-xml 0.38.4",
"rayon",
"sha2",
"thiserror 2.0.17",
"zip",
"zstd",

View File

@ -31,13 +31,11 @@ tokenizers = { version = "0.22.2", default-features = false, features = ["onig"]
ndarray = "0.17.1"
serde_json = "1.0.145"
serde = { version = "1.0.228", features = ["derive"] }
unp4k_rs = { git = "https://github.com/StarCitizenToolBox/unp4k_rs", tag = "V0.0.2" }
unp4k_rs = { git = "https://github.com/StarCitizenToolBox/unp4k_rs", rev = "b55d64934bde37bb1079c2c3e2996c8286532914" }
uuid = { version = "1.19.0", features = ["v4"] }
parking_lot = "0.12.5"
crossbeam-channel = "0.5.15"
librqbit = { git = "https://github.com/StarCitizenToolBox/rqbit", rev = "f8c0b0927904e1d8b0e28e708bd69fd8069d413a" }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
bytes = "1.10"
# WebView

View File

@ -7,7 +7,9 @@ use anyhow::{bail, Context, Result};
use bytes::Bytes;
use flutter_rust_bridge::frb;
use librqbit::{
AddTorrent, AddTorrentOptions, AddTorrentResponse, ManagedTorrent, Session, SessionOptions, SessionPersistenceConfig, TorrentStats, TorrentStatsState, WebSeedConfig, api::TorrentIdOrHash, dht::PersistentDhtConfig, limits::LimitsConfig
api::TorrentIdOrHash, dht::PersistentDhtConfig, limits::LimitsConfig, AddTorrent,
AddTorrentOptions, AddTorrentResponse, ManagedTorrent, Session, SessionOptions,
SessionPersistenceConfig, TorrentStats, TorrentStatsState, WebSeedConfig,
};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
@ -72,7 +74,7 @@ pub struct DownloadGlobalStat {
}
/// Initialize the download manager session with persistence enabled
///
///
/// Parameters:
/// - working_dir: The directory to store session data (persistence, DHT, etc.)
/// - default_download_dir: The default directory to store downloads
@ -90,7 +92,7 @@ pub async fn downloader_init(
}
let _lock = SESSION_INIT_LOCK.lock().await;
// Double check after acquiring lock
if SESSION.read().is_some() {
return Ok(());
@ -99,15 +101,15 @@ pub async fn downloader_init(
// Working directory for persistence and session data
let working_folder = PathBuf::from(&working_dir);
std::fs::create_dir_all(&working_folder)?;
// Default download folder
let output_folder = PathBuf::from(&default_download_dir);
std::fs::create_dir_all(&output_folder)?;
// Create persistence folder for session state in working directory
let persistence_folder = working_folder.join("rqbit-session");
std::fs::create_dir_all(&persistence_folder)?;
// DHT persistence file in working directory
let dht_persistence_file = working_folder.join("dht.json");
@ -131,7 +133,7 @@ pub async fn downloader_init(
upload_bps: upload_limit_bps.and_then(NonZeroU32::new),
download_bps: download_limit_bps.and_then(NonZeroU32::new),
},
webseed_config: Some(WebSeedConfig{
webseed_config: Some(WebSeedConfig {
max_concurrent_per_source: 32,
max_total_concurrent: 64,
request_timeout_secs: 30,
@ -162,21 +164,21 @@ pub fn downloader_is_initialized() -> bool {
/// Check if there are pending tasks to restore from session file (without starting the downloader)
/// This reads the session.json file directly to check if there are any torrents saved.
///
///
/// Parameters:
/// - working_dir: The directory where session data is stored (same as passed to downloader_init)
///
///
/// Returns: true if there are tasks to restore, false otherwise
#[frb(sync)]
pub fn downloader_has_pending_session_tasks(working_dir: String) -> bool {
let session_file = PathBuf::from(&working_dir)
.join("rqbit-session")
.join("session.json");
if !session_file.exists() {
return false;
}
// Try to read and parse the session file
match std::fs::read_to_string(&session_file) {
Ok(content) => {
@ -200,7 +202,8 @@ pub fn downloader_has_pending_session_tasks(working_dir: String) -> bool {
/// Helper function to get session
fn get_session() -> Result<Arc<Session>> {
SESSION.read()
SESSION
.read()
.clone()
.context("Downloader not initialized. Call downloader_init first.")
}
@ -321,7 +324,10 @@ pub async fn downloader_add_url(
.context("Failed to download torrent file")?;
if !response.status().is_success() {
bail!("Failed to download torrent file: HTTP {}", response.status());
bail!(
"Failed to download torrent file: HTTP {}",
response.status()
);
}
let bytes = response
@ -345,7 +351,10 @@ pub async fn downloader_pause(task_id: usize) -> Result<()> {
};
if let Some(handle) = handle {
session.pause(&handle).await.context("Failed to pause torrent")?;
session
.pause(&handle)
.await
.context("Failed to pause torrent")?;
Ok(())
} else {
bail!("Task not found: {}", task_id)
@ -362,7 +371,10 @@ pub async fn downloader_resume(task_id: usize) -> Result<()> {
};
if let Some(handle) = handle {
session.unpause(&handle).await.context("Failed to resume torrent")?;
session
.unpause(&handle)
.await
.context("Failed to resume torrent")?;
Ok(())
} else {
bail!("Task not found: {}", task_id)
@ -427,7 +439,9 @@ pub async fn downloader_get_task_info(task_id: usize) -> Result<DownloadTaskInfo
let (download_speed, upload_speed, num_peers) = if let Some(live) = &stats.live {
let down = (live.download_speed.mbps * 1024.0 * 1024.0) as u64;
let up = (live.upload_speed.mbps * 1024.0 * 1024.0) as u64;
let peers = (live.snapshot.peer_stats.queued + live.snapshot.peer_stats.connecting + live.snapshot.peer_stats.live) as usize;
let peers = (live.snapshot.peer_stats.queued
+ live.snapshot.peer_stats.connecting
+ live.snapshot.peer_stats.live) as usize;
(down, up, peers)
} else {
(0, 0, 0)
@ -455,7 +469,7 @@ fn get_task_status(stats: &TorrentStats) -> DownloadTaskStatus {
if stats.error.is_some() {
return DownloadTaskStatus::Error;
}
if stats.finished {
return DownloadTaskStatus::Finished;
}
@ -508,7 +522,9 @@ pub async fn downloader_get_all_tasks() -> Result<Vec<DownloadTaskInfo>> {
let (download_speed, upload_speed, num_peers) = if let Some(live) = &stats.live {
let down = (live.download_speed.mbps * 1024.0 * 1024.0) as u64;
let up = (live.upload_speed.mbps * 1024.0 * 1024.0) as u64;
let peers = (live.snapshot.peer_stats.queued + live.snapshot.peer_stats.connecting + live.snapshot.peer_stats.live) as usize;
let peers = (live.snapshot.peer_stats.queued
+ live.snapshot.peer_stats.connecting
+ live.snapshot.peer_stats.live) as usize;
(down, up, peers)
} else {
(0, 0, 0)
@ -536,7 +552,7 @@ pub async fn downloader_get_all_tasks() -> Result<Vec<DownloadTaskInfo>> {
// Merge cached completed tasks with IDs based on cache index (10000 + index)
let mut result = tasks.into_inner();
let completed_tasks_cache = COMPLETED_TASKS_CACHE.read();
for (cache_index, task) in completed_tasks_cache.iter().enumerate() {
let mut task_with_id = task.clone();
// Assign ID based on cache index: 10000, 10001, 10002, etc.
@ -552,11 +568,11 @@ pub async fn downloader_get_global_stats() -> Result<DownloadGlobalStat> {
let tasks = downloader_get_all_tasks().await?;
let mut stat = DownloadGlobalStat::default();
for task in &tasks {
stat.download_speed += task.download_speed;
stat.upload_speed += task.upload_speed;
match task.status {
DownloadTaskStatus::Live => stat.num_active += 1,
DownloadTaskStatus::Paused | DownloadTaskStatus::Checking => stat.num_waiting += 1,
@ -568,20 +584,23 @@ pub async fn downloader_get_global_stats() -> Result<DownloadGlobalStat> {
}
/// Check if a task with given name exists
///
///
/// Parameters:
/// - name: Task name to search for
/// - downloading_only: If true, only search in active/waiting tasks. If false, include completed tasks (default: true)
pub async fn downloader_is_name_in_task(name: String, downloading_only: Option<bool>) -> bool {
let downloading_only = downloading_only.unwrap_or(true);
if let Ok(tasks) = downloader_get_all_tasks().await {
for task in tasks {
// If downloading_only is true, skip finished and error tasks
if downloading_only && (task.status == DownloadTaskStatus::Finished || task.status == DownloadTaskStatus::Error) {
if downloading_only
&& (task.status == DownloadTaskStatus::Finished
|| task.status == DownloadTaskStatus::Error)
{
continue;
}
if task.name.contains(&name) {
return true;
}
@ -595,11 +614,11 @@ pub async fn downloader_pause_all() -> Result<()> {
let session = get_session()?;
let handles: Vec<_> = TORRENT_HANDLES.read().values().cloned().collect();
for handle in handles {
let _ = session.pause(&handle).await;
}
Ok(())
}
@ -608,11 +627,11 @@ pub async fn downloader_resume_all() -> Result<()> {
let session = get_session()?;
let handles: Vec<_> = TORRENT_HANDLES.read().values().cloned().collect();
for handle in handles {
let _ = session.unpause(&handle).await;
}
Ok(())
}
@ -632,11 +651,11 @@ pub async fn downloader_shutdown() -> Result<()> {
let mut guard = SESSION.write();
guard.take()
};
if let Some(session) = session_opt {
session.stop().await;
}
TORRENT_HANDLES.write().clear();
// Clear completed tasks cache on shutdown
COMPLETED_TASKS_CACHE.write().clear();
@ -657,7 +676,7 @@ pub fn downloader_clear_completed_tasks_cache() {
}
/// Update global speed limits
/// Note: rqbit Session doesn't support runtime limit changes,
/// Note: rqbit Session doesn't support runtime limit changes,
/// this function is a placeholder that returns an error.
/// Speed limits should be set during downloader_init.
pub async fn downloader_update_speed_limits(
@ -676,7 +695,7 @@ pub async fn downloader_remove_completed_tasks() -> Result<u32> {
let tasks = downloader_get_all_tasks().await?;
let mut removed_count = 0u32;
for task in tasks {
if task.status == DownloadTaskStatus::Finished {
// Only process active tasks (id < 10000)
@ -684,7 +703,11 @@ pub async fn downloader_remove_completed_tasks() -> Result<u32> {
let has_handle = TORRENT_HANDLES.read().contains_key(&task.id);
if has_handle {
// Use TorrentIdOrHash::Id for deletion
if session.delete(TorrentIdOrHash::Id(task.id), false).await.is_ok() {
if session
.delete(TorrentIdOrHash::Id(task.id), false)
.await
.is_ok()
{
// Cache the task - it will get ID based on cache length
COMPLETED_TASKS_CACHE.write().push(task.clone());
TORRENT_HANDLES.write().remove(&task.id);
@ -694,7 +717,7 @@ pub async fn downloader_remove_completed_tasks() -> Result<u32> {
}
}
}
Ok(removed_count)
}
@ -702,8 +725,9 @@ pub async fn downloader_remove_completed_tasks() -> Result<u32> {
pub async fn downloader_has_active_tasks() -> bool {
if let Ok(tasks) = downloader_get_all_tasks().await {
for task in tasks {
if task.status != DownloadTaskStatus::Finished
&& task.status != DownloadTaskStatus::Error {
if task.status != DownloadTaskStatus::Finished
&& task.status != DownloadTaskStatus::Error
{
return true;
}
}

View File

@ -3,7 +3,7 @@ use flutter_rust_bridge::frb;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use unp4k::{P4kEntry, P4kFile};
use unp4k::{CryXmlReader, P4kEntry, P4kFile};
/// P4K 文件项信息
#[frb(dart_metadata=("freezed"))]
@ -32,7 +32,11 @@ fn dos_datetime_to_millis(date: u16, time: u16) -> i64 {
let days_since_epoch = {
let mut days = 0i64;
for y in 1970..year {
days += if (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0) { 366 } else { 365 };
days += if (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0) {
366
} else {
365
};
}
let days_in_months = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
if month >= 1 && month <= 12 {
@ -45,7 +49,8 @@ fn dos_datetime_to_millis(date: u16, time: u16) -> i64 {
days
};
(days_since_epoch * 86400 + (hour as i64) * 3600 + (minute as i64) * 60 + (second as i64)) * 1000
(days_since_epoch * 86400 + (hour as i64) * 3600 + (minute as i64) * 60 + (second as i64))
* 1000
}
// 全局 P4K 读取器实例(用于保持状态)
@ -132,6 +137,32 @@ pub async fn p4k_get_all_files() -> Result<Vec<P4kFileItem>> {
pub async fn p4k_extract_to_memory(file_path: String) -> Result<Vec<u8>> {
// 确保文件列表已加载
tokio::task::spawn_blocking(|| ensure_files_loaded()).await??;
// 获取文件 entry 的克隆
let entry = p4k_get_entry(file_path).await?;
// 在后台线程执行阻塞的提取操作
let data = tokio::task::spawn_blocking(move || {
let mut reader = GLOBAL_P4K_READER.lock().unwrap();
if reader.is_none() {
return Err(anyhow!("P4K reader not initialized"));
}
let data = reader.as_mut().unwrap().extract_entry(&entry)?;
if (entry.name.ends_with(".xml") || entry.name.ends_with(".mtl"))
&& CryXmlReader::is_cryxml(&data)
{
let cry_xml_string = CryXmlReader::parse(&data)?;
return Ok(cry_xml_string.into_bytes());
}
Ok::<_, anyhow::Error>(data)
})
.await??;
Ok(data)
}
async fn p4k_get_entry(file_path: String) -> Result<P4kEntry> {
// 确保文件列表已加载
tokio::task::spawn_blocking(|| ensure_files_loaded()).await??;
// 规范化路径
let normalized_path = if file_path.starts_with("\\") {
@ -149,34 +180,27 @@ pub async fn p4k_extract_to_memory(file_path: String) -> Result<Vec<u8>> {
.clone()
};
// 在后台线程执行阻塞的提取操作
let data = tokio::task::spawn_blocking(move || {
let mut reader = GLOBAL_P4K_READER.lock().unwrap();
if reader.is_none() {
return Err(anyhow!("P4K reader not initialized"));
}
let data = reader.as_mut().unwrap().extract_entry(&entry)?;
Ok::<_, anyhow::Error>(data)
})
.await??;
Ok(data)
Ok(entry)
}
/// 提取文件到磁盘
pub async fn p4k_extract_to_disk(file_path: String, output_path: String) -> Result<()> {
let data = p4k_extract_to_memory(file_path).await?;
let entry = p4k_get_entry(file_path).await?;
// 在后台线程执行阻塞的文件写入操作
tokio::task::spawn_blocking(move || {
let output = PathBuf::from(&output_path);
// 创建父目录
if let Some(parent) = output.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(output, data)?;
let mut reader_guard = GLOBAL_P4K_READER.lock().unwrap();
let reader = reader_guard
.as_mut()
.ok_or_else(|| anyhow!("P4K reader not initialized"))?;
unp4k::p4k_utils::extract_single_file(reader, &entry, &output, true)?;
Ok::<_, anyhow::Error>(())
})
.await??;