简介

Vielpork是一个高性能的多线程HTTP下载器,由Rust编写,具有可自定义的报告器和资源解析策略。它提供:

  • 🚀 多线程下载以获得最大速度
  • 📊 多种内置报告器适配大部分场景
  • 📦 丰富的路径策略选项与模板命名支持
  • 🔧 为不同下载场景提供可定制的资源解析策略
  • ⏯️ 支持全局与单个任务的暂停/恢复功能
stateDiagram-v2
    [*] --> GlobalInit
    GlobalInit --> GlobalRunning: start_all()
    GlobalRunning --> GlobalSuspended: pause_all()
    GlobalSuspended --> GlobalRunning: resume_all()
    GlobalRunning --> GlobalStopped: cancel_all()
    GlobalStopped --> [*]
    
    state TaskStates {
        [*] --> TaskPending
        TaskPending --> TaskDownloading: start_task()
        TaskDownloading --> TaskPaused: pause_task()
        TaskPaused --> TaskDownloading: resume_task()
        TaskDownloading --> TaskCanceled: cancel_task()
        TaskDownloading --> TaskCompleted: finish()
        TaskPaused --> TaskCanceled: cancel_task()
        TaskCanceled --> [*]
        TaskCompleted --> [*]
    }
    
    GlobalSuspended --> TaskPaused : propagate
    GlobalStopped --> TaskCanceled : propagate

相关项目

  • osynic_downloader: 基于vielpork的osu!谱面下载器,包含工具库和TUI应用

核心特性

  • 多线程架构:利用Rust的异步运行时进行并发的分块下载
  • 可扩展的报告系统
    • 内置报告器:TUI进度条,CLI广播mpsc通道
    • 通过trait实现自定义报告器
  • 智能解析
    • 通过Resolver trait进行自定义解析逻辑
  • 恢复与韧性
    • 继续上次中断的下载
  • 进度跟踪
    • 实时速度计算
    • ETA估算
    • 详细的传输统计

内置选项

报告器

  • TuiReporter:基于indicatif库的终端进度条
  • CliReporterBoardcastMpsc:一个广播进度更新到多个通道并用单个通道完成的报告器(使用示例:在Tonic gRPC服务器流中,rx类型只能是mpsc,因此我们需要将进度广播到mpsc通道,然后通过服务器将其发送到客户端)

解析器

  • UrlResolver:一个从URL下载资源的解析器,只是reqwest的简单包装

自定义组件

您可以在vielpork::base::traits中查看所有trait并实现自己的组件。

自定义报告器

  • 这里有2个需要使用async_trait实现的trait:
    • ProgressReporter:允许报告器处理进度更新的trait
    • ResultReporter:允许报告器处理操作或任务的结果的trait

自定义解析器

  • 这里只有1个需要使用async_trait实现的trait:
    • ResourceResolver:允许解析器从特定来源下载资源的trait

后记(或者说最开始的序章)

最开始找到了viel这个词,后面想了下rufen、ekstase、reichen

但是正在我还在犹豫不决的时候,好朋友来寝室送了我一纸杯的熏猪肉丝

所以我就直接取名叫做vielpork了,这个名字的意思是很多猪肉丝

但如果是功能描述的话,这个下载器主打的是多报道通道下载,所以也是很多报道

report的vielpork很接近,也还不错

对于连续吃了一个星期免费粥的我来说,这个名字已经很好了

哦对了,水煮肉片也可以算是VielPork了

快速开始

主要分为如下几个部分

  • 初始例程
  • 流程控制
  • 使用实例:OsynicDownloader

在阅读完初始例程后,您可以尝试运行示例代码,下载您需要的资源。

然后,您可以尝试自定义Reporter和Resolver。

  • 自定义Reporter
  • 自定义Resolver

初始例程

安装

在你的Cargo.toml中添加:

[dependencies]
vielpork = "0.1.0"

快速开始


use vielpork::downloader::Downloader;
use vielpork::reporters::tui::TuiReporter;
use vielpork::resolvers::url::UrlResolver;
use vielpork::base::structs::DownloadOptions;
use vielpork::base::enums::DownloadResource;
use vielpork::error::Result;

use std::sync::Arc;
use tokio::sync::Mutex;

#[tokio::main]
async fn main() -> Result<()> {
    let options: DownloadOptions = DownloadOptions::default()
        .with_save_path("fetch".to_string())
        .with_concurrency(3);

    let downloader = Downloader::new(options, Box::new(UrlResolver::new()), Box::new(TuiReporter::new()));

    let resources = vec![
        DownloadResource::Url("https://example.com".to_string()),
        DownloadResource::Url("https://example.com".to_string()),
        DownloadResource::Url("https://example.com".to_string()),
        DownloadResource::Url("https://example.com".to_string()),
        DownloadResource::Url("https://example.com".to_string()),
        DownloadResource::Url("https://example.com".to_string()),
        DownloadResource::Url("https://example.com".to_string()),
        DownloadResource::Url("https://example.com".to_string()),
        DownloadResource::Url("https://example.com".to_string()),
    ];

    downloader.start(resources).await?;

    loop {
        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
        // Because of the async nature of the downloader, we need to keep the main thread alive
    }

    Ok(())
}

流程控制

vielpork提供了完整的流程控制方法,以便于您在编写异步代码时更加方便地控制流程。

  • 全局控制

    • downloader.lock().await.start(resources).await?;
    • downloader.lock().await.pause().await?;
    • downloader.lock().await.resume().await?;
    • downloader.lock().await.cancel().await?;
  • 任务控制

    • downloader.lock().await.pause_task(resource).await?;
    • downloader.lock().await.resume_task(resource).await?;
    • downloader.lock().await.cancel_task(resource).await?;

直接在代码中使用即可,vielpork会自动处理任务的状态转换。

状态转移图

stateDiagram-v2
    [*] --> GlobalInit
    GlobalInit --> GlobalRunning: start_all()
    GlobalRunning --> GlobalSuspended: pause_all()
    GlobalSuspended --> GlobalRunning: resume_all()
    GlobalRunning --> GlobalStopped: cancel_all()
    GlobalStopped --> [*]
    
    state TaskStates {
        [*] --> TaskPending
        TaskPending --> TaskDownloading: start_task()
        TaskDownloading --> TaskPaused: pause_task()
        TaskPaused --> TaskDownloading: resume_task()
        TaskDownloading --> TaskCanceled: cancel_task()
        TaskDownloading --> TaskCompleted: finish()
        TaskPaused --> TaskCanceled: cancel_task()
        TaskCanceled --> [*]
        TaskCompleted --> [*]
    }
    
    GlobalSuspended --> TaskPaused : propagate
    GlobalStopped --> TaskCanceled : propagate

合理的状态转换

vielpork的状态转换是有限状态机的形式,每个任务都有一个状态,每个任务的状态转换都是有限的。这样可以保证任务的状态转换是合理的,不会出现不合理的状态转换。

合理全局状态转换


#![allow(unused)]
fn main() {
let valid = match (*current, new_state) {
    (DownloaderState::Idle, DownloaderState::Idle) => true,
    (DownloaderState::Idle, DownloaderState::Running) => true,
    (DownloaderState::Running, DownloaderState::Suspended) => true,
    (DownloaderState::Suspended, DownloaderState::Running) => true,
    (DownloaderState::Stopped, DownloaderState::Idle) => true,
    (_, DownloaderState::Stopped) => true,
    _ => false,
};
}

合理任务状态转换


#![allow(unused)]
fn main() {
let valid = match (*current, new_state) {
    (TaskState::Paused, TaskState::Downloading) => true,
    (TaskState::Paused, TaskState::Paused) => true,
    (TaskState::Paused, TaskState::Pending) => true,
    (TaskState::Pending, TaskState::Paused) => true,
    (TaskState::Pending, TaskState::Downloading) => true,
    (TaskState::Downloading, TaskState::Paused) => true,
    (TaskState::Downloading, TaskState::Completed) => true,
    (TaskState::Failed, _) => true,
    (_, TaskState::Failed) => true,
    (_, TaskState::Canceled) => true,
    _ => false,
};
}

使用实例:OsynicDownloader

osynic_downloader 是一款高效的osu!谱面下载工具,基于vielpork开发,支持两种输入格式和并行下载,专为音游玩家和多设备谱面同步打造。

osynic_downloader.gif

推荐搭配osynic_serializer使用,实现osu!谱面的快速序列化。

osynic_serializer.gif

✨ 特性

  • 双模式输入:支持osu!谱面集ID列表和Osynic序列化生成的JSON格式
  • 多下载源:目前支持OsuDirect、OsuApiV2、SayoApi和ChimuApi共四种下载源
  • 并发支持:多线程并发下载加速(默认4线程)(请注意各osu!镜像站API的并发限制!文明使用!)
  • 智能管理:自动创建目录结构,自定义保存路径
  • 可视化进度:实时TUI进度显示(支持终端256色)
  • 错误恢复:无畏中断,自动断点续传

📦 安装

预编译版本

cargo install osynic_downloader

源码编译

git clone https://github.com/osynicite/osynic_downloader
cd osynic_downloader
cargo build --release

🚀 快速开始

基本使用

# 原生模式(ID列表)
osynic-dl --beatmapsets json/sets.json -o ./osu_maps -c 8

# Osynic模式(歌曲元数据)
osynic-dl --osynic-songs json/songs.json --output ./music

输入JSON示例

sets.json(Beatmapsets模式):

{
    "beatmapset_ids": ["114514", "1919810", "1538879"]
}

songs.json(Songs模式)(Osynic):

[
  {
    "song_id": 1985060,
    "artist_name": "ヒトリエ",
    "mapper_name": "flake",
    "song_name": "日常と地球の額縁 (wowaka x 初音ミク Edit)",
    "no_video": false
  },
    {
    "song_id": 1997071,
    "artist_name": "ナブナ",
    "mapper_name": "Ryuusei Aika",
    "song_name": "始発とカフカ",
    "no_video": false
  }
]

⚙️ 参数详解

参数简写默认值说明
--beatmapsets-b-原生模式JSON文件路径
--osynic-songs-n-Osynic模式JSON文件路径
--source-sSayoApiosu!谱面下载源
--username-u-osu!账号(仅OsuDirect/OsuApiV2需要)
--password-p-osu!密码(仅OsuDirect/OsuApiV2需要)
--output-obeatmapsets下载目录(自动创建)
--concurrency-c4下载并发数(1-16)
--help-h-显示帮助信息

支持的osu!下载源

  1. OsuDirect:osu!官方谱面下载源(需osu账号密码,做URL传参)
  2. OsuApiV2: osu!lazer的谱面下载源(需osu账号密码,做Basic认证)
  3. SayoApi(默认):Sayobot谱面下载源(无需登录)
  4. ChimuApi:Chimu.moe谱面下载源(无需登录)

📌 注意事项

  1. 视频下载适配(no_video)暂未实现,相关选项会被忽略
  2. 下载文件命名遵循{{filename}}命名规则
  3. 使用Ctrl+C中断下载进程后,可重新运行恢复下载
  4. 建议使用稳定的网络连接以获得最佳体验

🤝 贡献指南

欢迎通过Issue提交建议或Pull Request参与开发!请确保:

  • 遵循Rust官方编码规范
  • 新增功能需附带测试用例
  • 提交前运行cargo fmtcargo clippy

📜 开源协议

本项目基于 MIT License 开源,请尊重原作者的著作权。使用osu!相关资源时请遵守osu!社区准则

自定义Reporter

vielpork中,Reporter是一个用于向外部报告下载任务进度和结果的接口。Reporter由两个trait构成,他们定义了一系列的方法,用于向外部报告下载任务的进度和结果。Reporter的实现可以是任何类型,只要它实现了这两个Reporter trait即可。

vielport::base::traits中定义的这两个trait是ProgressReporterResultReporterProgressReporter定义了三个方法,用于报告下载任务的开始、进度更新和结束。ResultReporter定义了一个方法,用于报告下载任务的结果。


#![allow(unused)]
fn main() {
#[async_trait]
pub trait ProgressReporter
{
    async fn start_task(&self, task_id: u32, total: u64) -> Result<()>;
    async fn update_progress(&self, task_id: u32, progress: &DownloadProgress) -> Result<()>;
    async fn finish_task(&self, task_id: u32, result: DownloadResult) -> Result<()>;
}

#[async_trait]
pub trait ResultReporter
{
    async fn operation_result(&self, operation: OperationType, code: u32, message: String) -> Result<()>;
}

pub trait CombinedReporter: ProgressReporter + ResultReporter + Send + Sync {}
impl<T: ProgressReporter + ResultReporter + Send + Sync> CombinedReporter for T {}

}

内置使用实例一:TuiReporter

vielpork提供了一个内置的TUI报告器TuiReporter,用于在终端中显示下载任务的进度。TuiReporter实现了ProgressReporterResultReporter两个trait,用于向外部报告下载任务的进度和结果。

TuiReporter使用indicatif库来显示下载任务的进度。在下载任务开始时,TuiReporter会创建一个新的进度条,并在下载任务结束时更新进度条的状态。TuiReporter还会在下载任务结束时向外部报告下载任务的结果。


#![allow(unused)]

fn main() {
use crate::error::Result;
use crate::base::structs::DownloadProgress;
use crate::base::enums::{DownloadResult, OperationType};
use crate::base::traits::{ProgressReporter, ResultReporter};
use async_trait::async_trait;


#[cfg(feature = "tui")]
use indicatif::{ProgressBar, ProgressStyle,ProgressDrawTarget,MultiProgress};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;

const MAX_CONCURRENT_BARS: usize = 10 ;

// TUI 实现
#[cfg(feature = "tui")]
#[derive(Debug)]
pub struct TuiReporter {
    mp: MultiProgress,
    bars: Arc<Mutex<HashMap<u32, ProgressBar>>>,
}

impl TuiReporter {
    pub fn new() -> Self {
        let mp = Self::setup_global_progress();
        Self {
            mp,
            bars: Arc::new(Mutex::new(HashMap::new())),
        }
    }
    fn setup_global_progress() -> MultiProgress {
        let mp = MultiProgress::new();
        mp.set_draw_target(ProgressDrawTarget::stdout());
        mp
    }

    // 私有方法用于获取或创建进度条
    async fn get_or_create_bar(&self, task_id: u32, total: u64) -> ProgressBar {
        let mut bars = self.bars.lock().await;
        
        if bars.len() >= MAX_CONCURRENT_BARS {
            bars.retain(|_, bar| !bar.is_finished());
        }

        bars.entry(task_id)
            .or_insert_with(|| {
                let bar = ProgressBar::new(total);
                // 关键修改:将进度条添加到 MultiProgress 系统
                let bar = self.mp.add(bar); // 这行是核心修改
                bar.set_style(ProgressStyle::with_template(&format!(
                    "{{spinner:.green}} [{{bar:.cyan/blue}}] {{bytes}}/{{total_bytes}} ({}) {{msg}}",
                    task_id
                ))
                .unwrap_or(ProgressStyle::default_bar())
                .progress_chars("#>-"));
                bar
            });
        
        bars.get(&task_id).unwrap_or(&ProgressBar::hidden()).clone()
    }
}


#[async_trait]
impl ProgressReporter for TuiReporter {
    async fn start_task(&self, task_id: u32, total: u64) -> Result<()> {
        let bar = self.get_or_create_bar(task_id, total).await;
        bar.set_message("Downloading...");
        Ok(())
    }

    async fn update_progress(&self, task_id: u32, progress: &DownloadProgress) -> Result<()> {
        let bar = self.get_or_create_bar(task_id, progress.total_bytes).await;
        bar.set_position(progress.bytes_downloaded);

        let speed = if progress.rate > 1_000_000.0 {
            format!("{:.2} MB/s", progress.rate / 1_000_000.0)
        } else if progress.rate > 1_000.0 {
            format!("{:.2} KB/s", progress.rate / 1_000.0)
        } else {
            format!("{:.0} B/s", progress.rate)
        };

        // 格式化剩余时间
        let eta = if progress.remaining_time.as_secs() > 60 {
            format!("{}m {}s", 
                progress.remaining_time.as_secs() / 60,
                progress.remaining_time.as_secs() % 60)
        } else {
            format!("{}s", progress.remaining_time.as_secs())
        };
        
        bar.set_message(format!("Speed: {} | ETA: {}", speed,eta));

        
        Ok(())
    }

    async fn finish_task(&self, task_id: u32,result: DownloadResult) -> Result<()> {
        let mut bars = self.bars.lock().await;
        if let Some(bar) = bars.remove(&task_id) {
            match result {
                DownloadResult::Success { path ,duration ,.. } => {
                    // 条的颜色变成绿色,还是#>-
                    bar.set_style(ProgressStyle::with_template(&format!(
                        "{{spinner:.green}} [{{bar:.green/blue}}] {{bytes}}/{{total_bytes}} ({}): {{msg}}",
                        task_id
                    ))?.progress_chars("#>-"));
                    let success_message = format!("✅ Done in {}s, saved to {}",duration.as_secs(),path.display());
                    bar.finish_with_message(success_message)
                },
                DownloadResult::Failed { error,.. } => {
                    // 条变成红色
                    bar.set_style(ProgressStyle::default_bar().template(&format!(
                        "{{spinner:.red}} [{{bar:.red/blue}}] {{bytes}}/{{total_bytes}} ({}): {{msg}}",
                        task_id
                    ))?.progress_chars("#>-"));
                    let error_message = format!("❌ Error: {}", error);
                    bar.abandon_with_message(error_message)
                },
                DownloadResult::Canceled => {
                    // 条变成黄色
                    bar.set_style(ProgressStyle::default_bar().template(&format!(
                        "{{spinner:.red}} [{{bar:.yellow/blue}}] {{bytes}}/{{total_bytes}} ({}): {{msg}}",
                        task_id
                    ))?.progress_chars("#>-"));
                    bar.abandon_with_message("⛔ Canceled")
                },
            }
        }
        Ok(())
    }
}

#[async_trait]
impl ResultReporter for TuiReporter {
    async fn operation_result(&self, operation: OperationType, code: u32, message: String) -> Result<()> {
        if code == 200 {
            println!("{}: {}", operation, message);
        } else {
            eprintln!("{}: {}", operation, message);
        }
        Ok(())
    }
}

}

内置使用实例二:CliReporterBoardcastMpsc

vielpork提供了一个内置的CLI广播报告器CliReporterBoardcastMpsc,用于将下载任务的进度广播到多个mpsc通道。CliReporterBoardcastMpsc实现了ProgressReporterResultReporter两个trait,用于向外部报告下载任务的进度和结果。

CliReporterBoardcastMpsc使用tokio::sync::mpsc来广播下载任务的进度。在下载任务开始时,CliReporterBoardcastMpsc会向多个mpsc通道发送进度更新消息,并在下载任务结束时向外部报告下载任务的结果。

这个Reporter的实现比较简单,只需要在ProgressReporterResultReporter的方法中向多个mpsc通道发送消息即可。最开始是用来解决在Tonic gRPC服务器流中,rx类型只能是mpsc,因此我们需要将进度广播到mpsc通道,然后通过服务器将其发送到客户端。


#![allow(unused)]
fn main() {
use crate::error::Result;
use crate::base::traits::{ProgressReporter, ResultReporter};
use crate::base::structs::DownloadProgress;
use crate::base::enums::{ProgressEvent, DownloadResult, OperationType};
use async_trait::async_trait;

#[derive(Debug,Clone)]
pub struct CliReporterBoardcastMpsc{
    inner_tx: tokio::sync::broadcast::Sender<ProgressEvent>,
    buffer_size: usize,
}
impl CliReporterBoardcastMpsc {
    pub fn new(buffer_size: usize) -> Self {
        let (inner_tx, _) = tokio::sync::broadcast::channel(buffer_size);
        Self { inner_tx, buffer_size }
    }

    pub fn subscribe_mpsc(&self) -> tokio::sync::mpsc::Receiver<ProgressEvent> {
        let (tx, rx) = tokio::sync::mpsc::channel(self.buffer_size);
        let mut inner_rx = self.inner_tx.subscribe();
        
        tokio::spawn(async move {
            loop {
                match inner_rx.recv().await {
                    Ok(event) => {
                        if tx.send(event).await.is_err() {
                            break;
                        }
                    }
                    Err(_) => {
                        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
                    }
                }
            }
        });
        
        rx
    }

    // 发送事件的方法
    pub async fn send(&self, event: ProgressEvent) -> Result<usize> {
        self.inner_tx.send(event)?;
        Ok(self.inner_tx.receiver_count())
    }

    // 创建新订阅者
    pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<ProgressEvent> {
        self.inner_tx.subscribe()
    }
}

#[async_trait]
impl ProgressReporter for CliReporterBoardcastMpsc {
    async fn start_task(&self, task_id: u32, total: u64) ->Result<()> {
        self.send(ProgressEvent::Start { task_id, total }).await?;
        Ok(())
    }

    async fn update_progress(&self, task_id: u32, progress: &DownloadProgress)->Result<()> {
        self.send(ProgressEvent::Update { task_id, progress: progress.clone() }).await?;
        Ok(())
    }

    async fn finish_task(&self, task_id: u32,finish: DownloadResult) ->Result<()>{
        self.send(ProgressEvent::Finish { task_id ,finish}).await?;
        Ok(())
    }
}

#[async_trait]
impl ResultReporter for CliReporterBoardcastMpsc {
    async fn operation_result(&self, operation: OperationType, code: u32, message: String) ->Result<()> {
        self.send(ProgressEvent::OperationResult { operation, code, message }).await?;
        Ok(())
    }
}
}

自定义Resolver

vielpork中,Resolver是一个trait,用于解析资源的来源。vielpork提供了一个内置的UrlResolver,用于从URL下载资源。您可以通过实现Resolver trait来自定义解析逻辑。

vielport::base::traits中定义的这个trait是ResourceResolverResourceResolver定义了一个方法,用于解析资源的来源。


#![allow(unused)]
fn main() {
#[async_trait]
pub trait ResourceResolver: Send + Sync {
    async fn resolve(&self, resource: &DownloadResource) -> Result<ResolvedResource>;
}
}

Resolver何为?

Resolver用来将DownloadResource转换为ResolvedResourceDownloadResource是一个枚举类型,它包含了资源的不同类型。ResolvedResource是一个结构体,它包含了HTTP请求所需的详细信息。


#![allow(unused)]
fn main() {
// `vielpork::base::enums`中定义的`DownloadResource`枚举类型

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DownloadResource {
    Url(String),
    Id(String), 
    Params(Vec<String>),
    HashMap(HashMap<String, String>),
    Resolved(ResolvedResource),
}
}

#![allow(unused)]
fn main() {
// `vielpork::base::structs`中定义的`ResolvedResource`结构体
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolvedResource {
    pub id: u32,
    pub url: String,
    pub headers: Vec<(String, String)>,
    pub auth: Option<AuthMethod>,
}
}

其中,AuthMethod支持BasicBearer以及ApiKey三种认证方式。


#![allow(unused)]
fn main() {
// `vielpork::base::enums`中定义的`AuthMethod`枚举类型

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum AuthMethod {
    None,
    Basic { username: String, password: String },
    Bearer { token: String },
    ApiKey { key: String, header: String },
}
}

内置使用实例一:UrlResolver

UrlResolver是一个简单的解析器,用于从URL下载资源。

实际上,UrlResolver只是一个简单的reqwest包装器,它将URL转换为ResolvedResource


#![allow(unused)]

fn main() {
use crate::error::Result;
use crate::base::traits::ResourceResolver;
use crate::base::structs::ResolvedResource;
use crate::base::enums::DownloadResource;
use crate::base::algorithms::generate_task_id;
use async_trait::async_trait;

#[derive(Debug,Clone)]
pub struct UrlResolver {}

impl UrlResolver {
    pub fn new() -> Self {
        Self {}
    }
}

#[async_trait]
impl ResourceResolver for UrlResolver {
    async fn resolve(&self, resource: &DownloadResource) -> Result<ResolvedResource> {
        match resource {
            DownloadResource::Url(url) => {
                Ok(ResolvedResource{
                    id: generate_task_id(url),
                    url: url.clone(),
                    headers: vec![],
                    auth: None,
                })
            }
            DownloadResource::Resolved(resolved) => {
                Ok(resolved.clone())
            }
            _ => {
                Err("Unsupported resource type".into())
            }
        }
    }
}
}

外部使用实例一:OsuBeatmapResolver

这一部分可以详见osynic_downloader的源码。

osynic_downloader是一个基于vielpork的osu!谱面下载器,包含工具库和TUI应用。

它使用的Resolver是OsuBeatmapResolver,这个Resolver是一个从osu!官网、API以及各打镜像站下载谱面的资源解析器。

例如:

  • 对于Url类型的资源,直接返回Url;
  • 对于Id类型的资源,根据Id生成Url,使用默认的下载源;
  • 对于Params类型的资源,根据参数生成Url,使用参数指定的下载源;
  • 对于HashMap类型的资源,根据HashMap生成Url,使用参数指定的下载源。

#![allow(unused)]
fn main() {
use vielpork::base::traits::ResourceResolver;
use vielpork::base::structs::ResolvedResource;
use vielpork::base::enums::{DownloadResource,AuthMethod};
use vielpork::base::algorithms::generate_task_id;
use async_trait::async_trait;

use crate::sources::{DownloadSource, DownloadSourceType};
use crate::url::form_url;

#[derive(Debug,Clone)]
pub struct OsuBeatmapsetResolver {}

impl OsuBeatmapsetResolver {
    pub fn new() -> Self {
        Self {}
    }
}

#[async_trait]
impl ResourceResolver for OsuBeatmapsetResolver {
    async fn resolve(&self, resource: &DownloadResource) -> vielpork::error::Result<ResolvedResource> {
        match resource {
            DownloadResource::Url(url) => {
                Ok(ResolvedResource{
                    id: generate_task_id(url),
                    url: url.clone(),
                    headers: vec![],
                    auth: None,
                })
            }
            DownloadResource::Id(id) => {
                let beatmapset_id: u32;
                match id.parse::<u32>() {
                    Ok(id) => {
                        beatmapset_id = id;
                    }
                    Err(_) => {
                        return Err("Invalid beatmapset id".into());
                    }
                }
                let download_source = DownloadSource::from(DownloadSourceType::Default);
                let base_url = download_source.base_url.clone();
                let url = form_url(&base_url, &beatmapset_id, "", "").map_err(|e| e.to_string())?;
                Ok(ResolvedResource{
                    id: beatmapset_id,
                    url: url.clone(),
                    headers: vec![],
                    auth: None,
                })
            }
            DownloadResource::Params(params) => {
                let beatmapset_id: u32;
                let source: String;

                match params.get(0) {
                    Some(id) => {
                        match id.parse::<u32>() {
                            Ok(id) => {
                                beatmapset_id = id;
                            }
                            Err(_) => {
                                return Err("Invalid beatmapset id".into());
                            }
                        }
                    }
                    None => {
                        return Err("Missing beatmapset_id".into());
                    }
                }
                match params.get(1) {
                    Some(src) => {
                        source = src.clone();
                    }
                    None => {
                        return Err("Missing source".into());
                    }
                }
                let download_source = DownloadSource::from(DownloadSourceType::from(source));
                let base_url = download_source.base_url.clone();

                let username: String;
                let password: String;

                let url: String;
                if download_source.requires_osu_credentials {
                    match params.get(2) {
                        Some(name) => {
                            username = name.clone();
                        }
                        None => {
                            return Err("Missing username".into());
                        }
                    }
                    match params.get(3) {
                        Some(pass) => {
                            password = pass.clone();
                        }
                        None => {
                            return Err("Missing password".into());
                        }
                    }
                    if download_source.requires_basic_auth{
                        url = form_url(&base_url, &beatmapset_id, "","").map_err(|e| e.to_string())?;
                        Ok(
                            ResolvedResource{
                                id:beatmapset_id,
                                url: url.clone(),
                                headers: vec![],
                                auth: Some(AuthMethod::Basic { username, password }),
                            }
                        )
                    } else {
                        let hashed_password = format!("{:x}", md5::compute(password));
                        url = form_url(&base_url, &beatmapset_id, &username, &hashed_password).map_err(|e| e.to_string())?;
                        Ok(
                            ResolvedResource{
                                id:beatmapset_id,
                                url: url.clone(),
                                headers: vec![],
                                auth: None,
                            }
                        )
                    }
                } else {
                    url = form_url(&base_url, &beatmapset_id, "", "").map_err(|e| e.to_string())?;
                    Ok(
                        ResolvedResource{
                            id:beatmapset_id,
                            url: url.clone(),
                            headers: vec![],
                            auth: None,
                        }
                    )
                }

            }
            DownloadResource::HashMap(hashmap) => {
                let beatmapset_id: u32;
                let source: String;
                match hashmap.get("beatmapset_id") {
                    Some(id) => {
                        match id.parse::<u32>() {
                            Ok(id) => {
                                beatmapset_id = id;
                            }
                            Err(_) => {
                                return Err("Invalid beatmapset id".into());
                            }
                        }
                    }
                    None => {
                        return Err("Missing beatmapset_id".into());
                    }
                }
                match hashmap.get("source") {
                    Some(src) => {
                        source = src.clone();
                    }
                    None => {
                        return Err("Missing source".into());
                    }
                }
                let download_source = DownloadSource::from(DownloadSourceType::from(source));
                let base_url = download_source.base_url.clone();

                let username: String;
                let password: String;

                let url: String;
                if download_source.requires_osu_credentials {
                    match hashmap.get("username") {
                        Some(name) => {
                            username = name.clone();
                        }
                        None => {
                            return Err("Missing username".into());
                        }
                    }
                    match hashmap.get("password") {
                        Some(pass) => {
                            password = pass.clone();
                        }
                        None => {
                            return Err("Missing password".into());
                        }
                    }
                    if download_source.requires_basic_auth{
                        url = form_url(&base_url, &beatmapset_id, "","").map_err(|e| e.to_string())?;
                        Ok(
                            ResolvedResource{
                                id:beatmapset_id,
                                url: url.clone(),
                                headers: vec![],
                                auth: Some(AuthMethod::Basic { username, password }),
                            }
                        )
                    } else {
                        let hashed_password = format!("{:x}", md5::compute(password));
                        url = form_url(&base_url, &beatmapset_id, &username, &hashed_password).map_err(|e| e.to_string())?;
                        Ok(
                            ResolvedResource{
                                id:beatmapset_id,
                                url: url.clone(),
                                headers: vec![],
                                auth: None,
                            }
                        )
                    }
                } else {
                    url = form_url(&base_url, &beatmapset_id, "", "").map_err(|e| e.to_string())?;
                    Ok(
                        ResolvedResource{
                            id:beatmapset_id,
                            url: url.clone(),
                            headers: vec![],
                            auth: None,
                        }
                    )
                }                
            }
            DownloadResource::Resolved(resolved) => {
                Ok(resolved.clone())
            }
        }
    }
}

}

更改日志

0.1

  • osynic_core里面的download模块抽离出了通用化的HTTP下载器逻辑,并在支持了自定义Reporter和Resolver之后,将其独立为vielpork

路线图

暂无更多安排,敬请期待。

附录

状态图

stateDiagram-v2
    [*] --> GlobalInit
    GlobalInit --> GlobalRunning: start_all()
    GlobalRunning --> GlobalSuspended: pause_all()
    GlobalSuspended --> GlobalRunning: resume_all()
    GlobalRunning --> GlobalStopped: cancel_all()
    GlobalStopped --> [*]
    
    state TaskStates {
        [*] --> TaskPending
        TaskPending --> TaskDownloading: start_task()
        TaskDownloading --> TaskPaused: pause_task()
        TaskPaused --> TaskDownloading: resume_task()
        TaskDownloading --> TaskCanceled: cancel_task()
        TaskDownloading --> TaskCompleted: finish()
        TaskPaused --> TaskCanceled: cancel_task()
        TaskCanceled --> [*]
        TaskCompleted --> [*]
    }
    
    GlobalSuspended --> TaskPaused : propagate
    GlobalStopped --> TaskCanceled : propagate

问题反馈

这个库是差不多一个上午写完的,所以肯定还有很多地方需要改进,目前也只是满足了我自己的项目需求,不能保证完全符合所有人的需求。

所以,如果代码有任何问题,或者你有任何建议,欢迎提交PR或者Issue,我会尽快处理~

如果你想贡献代码,请遵循以下规则:

  • 遵循Rust官方编码规范
  • 新增功能需附带测试用例
  • 提交前运行cargo fmtcargo clippy

Ending

Reference