吃得饱系列-用 Rust 写个 CLI 解决 UWP Bilibili 下载的视频无法播放问题
发现问题
众所周知,使用 wINDOWS 微软商店下载的 Bilibili 客户端是有视频下载功能的,不过它有个问题,下载后的视频无法直接使用一般的打开。
稍微研究(使用搜索引擎)一下发觉,原来是该客户端下载后的文件开头补了三个 0xFF
。
解决方案
最简单的方法就是直接使用一些支持 hex 编辑的 editor 手动把它们删掉再保存,不过如果有多个文件(很明显这是常态),那一个一个手动处理太机械了,于是直接写个命令行工具解决。实现原理也很简单,就是直接跳过读取开头三个 0xFF
再写入到新文件。
开始编码
创建项目
创建 Rust 项目先,直接执行 cargo new xxx
就把项目生成好啦。然后添加几个第三方库,首先自然是 clap,然后是错误处理,这种小工具就不手写自定义错误处理啦,使用 anyhow,还可以加个进度条指示器indictif,因为要批处理文件,文件夹内可能有非视频文件,所以加个 regex 包用正则过滤一下文件。
其中 clap 用得是它的派生宏版本。
cargo add clap -F derive
cargo add anyhow
cargo add indicatif
cargo add regex
可以 cat 查看一下 Cargo.toml 内容
$ cat ./Cargo.toml
---------------------------
[package]
name = "xxx"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.70"
clap = { version = "4.2.1", features = ["derive"] }
indicatif = "0.17.3"
regex = "1.7.3"
正式编码
先定义好传参,其实就是接收输入的目录,所以用派生宏包一下结构体 Args
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
struct Args {
#[clap(value_parser)]
input: String,
}
然后在 main
函数写上下面的代码,主要是获取待处理的目录,当它不是一个文件夹时直接 panic
fn main() {
let args: Args = Args::parse();
let p = Path::new(&args.input);
if !p.is_dir() {
panic!("The input is not directory!");
}
read_file(p);
}
我们还得把 read_file
函数补上,这一步就是基本逻辑就是使用 std::fs::read_dir
读取文件夹内的子路径,判断一下读取到的是否是文件或文件夹,如果是文件夹就递归继续读取,直到读到文件为止,读取到文件就用 process_file
处理它,出错了不管直接 unwrap
死就死吧,小项目是这样的捏。
fn read_file(dir: &Path) {
for sub_path in read_dir(dir).expect("Failed to read file path.").flatten() {
let p = sub_path.path();
if p.is_file() {
process_file(&p).unwrap();
} else {
read_file(&p);
}
}
}
个么很自然地就是实现 process_file
函数,先取到文件名(其实是取得完整的文件路径),然后通过正则过滤一下副档名是 mp4
的文件,读取文件大小给进度条结构用,判断下原文件开头三个字节是 0xFF
下继续进行下一步操作,主要是为了防止处理错文件。然后创建个空文件,循环读取原文件内容写入到新文件,后面再把原文件删除,把新文件修改一下名字,至此处理完成。
const BUFFER_SIZE: usize = 1024;
fn process_file(p: &Path) -> Result<(), anyhow::Error> {
let filename = p
.file_name()
.expect("Failed to get filename.")
.to_str()
.expect("Failed to unwrap path str.");
let mut buffer = [0u8; BUFFER_SIZE];
let file_regex = Regex::new(r"\.mp4$").expect("Failed to new file regex.");
if file_regex.is_match(filename) {
let mut original_file = File::open(p)?;
let file_size = original_file.metadata()?.len();
let pb = ProgressBar::new(file_size);
let mut count = original_file.read(&mut buffer)?;
if buffer[0] == 0xFF && buffer[1] == 0xFF && buffer[2] == 0xFF {
let new_filename = String::from("new_") + filename;
let new_filename = p.with_file_name(new_filename);
let new_file_path = Path::new(&new_filename);
let mut new_file = File::create(new_file_path)?;
let mut is_first = true;
while count != 0 {
if is_first {
new_file.write_all(&buffer[3..count])?;
pb.inc((count - 3) as u64);
is_first = false;
} else {
new_file.write_all(&buffer[..count])?;
pb.inc(count as u64);
}
count = original_file.read(&mut buffer)?;
}
remove_file(p)?;
rename(new_file_path, p)?;
pb.finish_with_message("done");
}
}
Ok(())
}
然后就可以通过一般的视频播放器打开文件啦。
转载自:https://juejin.cn/post/7220344703589105701