likes
comments
collection
share

吃得饱系列-用 Rust 写个 CLI 解决 UWP Bilibili 下载的视频无法播放问题

作者站长头像
站长
· 阅读数 31

发现问题

众所周知,使用 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(())
}

然后就可以通过一般的视频播放器打开文件啦。