#![doc = include_str!("../README.md")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
mod error;
use aes::cipher::{BlockDecryptMut, KeyInit, block_padding::Pkcs7};
use base64::{Engine, prelude::BASE64_STANDARD};
use ecb::Decryptor;
use error::{NcmError, Result};
use id3::{
Tag, TagLike as _,
frame::{Picture, PictureType},
};
use serde::{Deserialize, Serialize, ser::SerializeTuple as _};
use serde_json::Value;
use std::{
fs::File,
io::{Read, Seek as _, Write as _},
path::{Path, PathBuf},
};
const CORE_KEY: &[u8; 16] = b"hzHRAmso5kInbaxW";
const META_KEY: &[u8; 16] = br#"#14ljk_!\]&0U<'("#;
const KEY_MASK: u8 = 0x64;
const META_MASK: u8 = 0x63;
#[derive(Debug)]
pub struct NcmFile {
file: File,
path: PathBuf,
key: Key,
meta: Meta,
}
impl NcmFile {
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref().to_owned();
let mut file = File::open(&path)?;
Self::verify_header(&mut file)?;
let key = Key::new(Self::get_key(&mut file)?);
let mut meta = Self::get_meta(&mut file)?;
file.seek_relative(5)?; meta.cover = Self::get_cover(&mut file)?;
Ok(Self {
file,
path,
key,
meta,
})
}
#[cfg(feature = "cover_download")]
pub fn with_cover(mut self) -> Result<Self> {
self.fetch_cover()?;
Ok(self)
}
#[cfg(feature = "cover_download")]
pub fn fetch_cover(&mut self) -> Result<()> {
if self.meta.cover.is_empty() {
self.meta.cover = ureq::get(&self.meta.album_pic)
.call()?
.body_mut()
.read_to_vec()?;
}
Ok(())
}
fn verify_header(file: &mut File) -> Result<()> {
let mut buf = [0; 10];
file.read_exact(&mut buf)?;
if &buf[..8] != b"CTENFDAM" {
return Err(NcmError::Invalid("Invalid file header".to_string()));
}
Ok(())
}
fn get_key(file: &mut File) -> Result<Vec<u8>> {
let mut buf = [0; 4];
file.read_exact(&mut buf)?;
let length = u32::from_le_bytes(buf) as usize;
let mut buf = vec![0; length];
file.read_exact(&mut buf)?;
buf.iter_mut().for_each(|byte| *byte ^= KEY_MASK);
let aes = Decryptor::<aes::Aes128>::new_from_slice(CORE_KEY).unwrap();
let buf = aes
.decrypt_padded_mut::<Pkcs7>(&mut buf)
.map_err(|_| NcmError::Invalid("Failed to decrypt key".to_string()))?;
if &buf[..17] != b"neteasecloudmusic" {
return Err(NcmError::Invalid("Invalid key header".to_string()));
}
let key_data = &buf[17..];
let mut key_box: [u8; 256] = core::array::from_fn(|i| i as u8);
let mut last_byte = 0;
let mut key_offset = 0;
for i in 0..256 {
let c = key_box[i]
.wrapping_add(last_byte)
.wrapping_add(key_data[key_offset]);
key_offset += 1;
if key_offset >= key_data.len() {
key_offset = 0;
}
key_box.swap(i, c as usize);
last_byte = c;
}
Ok(key_box.to_vec())
}
fn get_meta(file: &mut File) -> Result<Meta> {
let mut buf = [0; 4];
file.read_exact(&mut buf)?;
let length = u32::from_le_bytes(buf) as usize;
let mut buf = vec![0; length];
file.read_exact(&mut buf)?;
buf.iter_mut().for_each(|byte| *byte ^= META_MASK);
if &buf[..22] != b"163 key(Don't modify):" {
return Err(NcmError::Invalid("Invalid metadata header".to_string()));
}
let mut buf = BASE64_STANDARD
.decode(&buf[22..])
.map_err(|_| NcmError::Invalid("Failed to decode base64 metadata".to_string()))?;
let aes = Decryptor::<aes::Aes128>::new_from_slice(META_KEY).unwrap();
let buf = aes
.decrypt_padded_mut::<Pkcs7>(&mut buf)
.map_err(|_| NcmError::Invalid("Failed to decrypt metadata".to_string()))?;
if &buf[..6] != b"music:" {
return Err(NcmError::Invalid("Invalid meta marker".to_string()));
}
serde_json::from_slice(&buf[6..])
.map_err(|e| NcmError::Invalid(format!("Failed to parse metadata: {e}")))
}
fn get_cover(file: &mut File) -> Result<Vec<u8>> {
let mut buf = [0; 4];
file.read_exact(&mut buf)?;
let cover_frame_length = u32::from_le_bytes(buf);
file.read_exact(&mut buf)?;
let length = u32::from_le_bytes(buf);
let mut buf = vec![0; length as usize];
file.read_exact(&mut buf)?;
file.seek_relative((cover_frame_length - length) as i64)?;
Ok(buf)
}
pub fn save(self) -> Result<()> {
let path = self.path.with_extension(&self.meta.format);
self.save_to(path)
}
pub fn save_to(self, path: impl AsRef<Path>) -> Result<()> {
let tag = Tag::from(&self.meta);
self.save_without_meta_to(&path)?;
tag.write_to_path(path, id3::Version::Id3v24)?;
Ok(())
}
pub fn save_without_meta(self) -> Result<()> {
let path = self.path.with_extension(&self.meta.format);
self.save_without_meta_to(path)
}
pub fn save_without_meta_to(mut self, path: impl AsRef<Path>) -> Result<()> {
let mut file = std::fs::File::create(&path)?;
std::io::copy(&mut self, &mut file)?;
file.flush()?;
Ok(())
}
pub fn meta(&self) -> &Meta {
&self.meta
}
}
impl Read for NcmFile {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let size = self.file.read(buf)?;
for (i, key) in (0..size).zip(&mut self.key) {
buf[i] ^= key;
}
Ok(size)
}
}
#[derive(Debug)]
struct Key {
key: Vec<u8>,
i: u8,
}
impl Key {
fn new(key: Vec<u8>) -> Self {
Self { key, i: 0 }
}
}
impl Iterator for Key {
type Item = u8;
fn next(&mut self) -> Option<Self::Item> {
self.i = self.i.wrapping_add(1);
Some(
self.key[self.key[self.i as usize]
.wrapping_add(self.key[self.key[self.i as usize].wrapping_add(self.i) as usize])
as usize],
)
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[allow(missing_docs)]
pub struct Meta {
#[serde(default)]
pub album: String,
#[serde(rename = "albumId", deserialize_with = "deserialize_to_string")]
pub album_id: String,
#[serde(rename = "albumPic")]
pub album_pic: String,
#[serde(rename = "albumPicDocId", deserialize_with = "deserialize_to_string")]
pub album_pic_doc_id: String,
#[serde(default)]
pub alias: Vec<String>,
#[serde(default)]
pub artist: Vec<Artist>,
pub bitrate: usize,
pub duration: usize,
pub fee: Option<usize>,
pub flag: Option<usize>,
pub format: String,
#[serde(rename = "mp3DocId")]
pub mp3_doc_id: Option<String>,
pub gain: Option<f64>,
#[serde(rename = "musicId", deserialize_with = "deserialize_to_string")]
pub music_id: String,
#[serde(rename = "musicName")]
pub music_name: String,
#[serde(default, rename = "mvId", deserialize_with = "deserialize_to_string")]
pub mv_id: String,
#[serde(default, rename = "transNames")]
pub trans_names: Vec<String>,
#[serde(skip)]
pub cover: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct Artist {
name: String,
id: String,
}
impl Serialize for Artist {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut seq = serializer.serialize_tuple(2)?;
seq.serialize_element(&self.name)?;
seq.serialize_element(&self.id)?;
seq.end()
}
}
impl<'de> Deserialize<'de> for Artist {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let v = Value::deserialize(deserializer)?;
match v.as_array() {
Some(v) => {
if let [Value::String(name), Value::String(id)] = v.as_slice() {
Ok(Artist {
name: name.clone(),
id: id.clone(),
})
} else if let [Value::String(name), Value::Number(id)] = v.as_slice() {
Ok(Artist {
name: name.clone(),
id: id.to_string(),
})
} else {
Err(serde::de::Error::custom("Invalid value"))
}
}
None => Err(serde::de::Error::custom("Invalid value")),
}
}
}
fn deserialize_to_string<'de, D>(deserializer: D) -> core::result::Result<String, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = Value::deserialize(deserializer)?;
match s {
Value::String(s) => Ok(s),
Value::Number(n) => Ok(n.to_string()),
_ => Err(serde::de::Error::custom("Invalid value")),
}
}
impl From<&Meta> for Tag {
fn from(meta: &Meta) -> Self {
let mut tag = Tag::new();
tag.set_album(meta.album.clone());
tag.add_frame(Picture {
mime_type: "image/jpeg".to_string(),
picture_type: PictureType::CoverFront,
description: "Cover".to_string(),
data: meta.cover.clone(),
});
tag.set_artist(meta.artist.iter().map(|artist| &artist.name).fold(
String::new(),
|acc, x| {
if acc.is_empty() {
x.to_string()
} else {
acc + ", " + x
}
},
));
tag.set_duration(meta.duration as u32);
tag.set_title(meta.music_name.clone());
tag
}
}