mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-30 21:29:36 +02:00
fix(metadata): read tags and covers from MP4 files named opus
This commit is contained in:
1 parent
347208b3d9
commit
097af1532f
3 files changed
+122
-20
No files matched your search
@@ -28,8 +28,32 @@ pub struct CoverArt {
|
||||
pub mime: String,
|
||||
}
|
||||
|
||||
fn read_container_format<'a>(
|
||||
reader: &mut (impl Read + Seek),
|
||||
format: &'a str,
|
||||
) -> Result<&'a str, String> {
|
||||
if format != "opus" {
|
||||
return Ok(format);
|
||||
}
|
||||
// Opus is also carried in MP4; the filename can still end in .opus.
|
||||
reader
|
||||
.seek(SeekFrom::Start(0))
|
||||
.map_err(|error| error.to_string())?;
|
||||
let mut header = [0; 8];
|
||||
let read = reader.read_exact(&mut header);
|
||||
reader
|
||||
.seek(SeekFrom::Start(0))
|
||||
.map_err(|error| error.to_string())?;
|
||||
match read {
|
||||
Ok(()) if &header[4..] == b"ftyp" => Ok("m4a"),
|
||||
Ok(()) => Ok(format),
|
||||
Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => Ok(format),
|
||||
Err(error) => Err(error.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the embedded bytes without decoding the image or reading audio into
|
||||
/// memory. Format selection/hints and output publication belong to the owner.
|
||||
/// memory. Path hints and output publication belong to the owner.
|
||||
pub fn extract_cover(
|
||||
reader: &mut (impl Read + Seek),
|
||||
format: &str,
|
||||
@@ -44,6 +68,7 @@ pub fn extract_cover(
|
||||
check,
|
||||
failure: None,
|
||||
};
|
||||
let format = read_container_format(&mut observed, format)?;
|
||||
let mut reader = BufReader::new(&mut observed);
|
||||
let result = match format {
|
||||
"flac" => containers::flac_cover(&mut reader),
|
||||
@@ -173,6 +198,7 @@ fn read_tags(
|
||||
reader: BufReader::new(CheckedReader { reader, check }),
|
||||
position: 0,
|
||||
};
|
||||
let format = read_container_format(&mut reader, format)?;
|
||||
let result = match format {
|
||||
"flac" => match cover {
|
||||
Some(cover) => containers::flac_with_cover(&mut reader, cover),
|
||||
@@ -239,3 +265,77 @@ fn truthy(value: &str) -> bool {
|
||||
"1" | "true" | "yes" | "explicit"
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Cursor;
|
||||
|
||||
fn atom(kind: &[u8; 4], body: &[u8]) -> Vec<u8> {
|
||||
[
|
||||
((body.len() + 8) as u32).to_be_bytes().as_slice(),
|
||||
kind,
|
||||
body,
|
||||
]
|
||||
.concat()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opus_in_mp4_reads_tags_cover_and_library_quality() {
|
||||
let mut ilst = Vec::new();
|
||||
let cover = b"\xff\xd8\xff\xd9";
|
||||
for (kind, value) in [
|
||||
(b"\xa9nam", b"Track Title".as_slice()),
|
||||
(b"\xa9ART", b"Artist Name".as_slice()),
|
||||
(b"covr", cover.as_slice()),
|
||||
] {
|
||||
ilst.extend(atom(kind, &atom(b"data", &[&[0; 8], value].concat())));
|
||||
}
|
||||
let metadata = atom(
|
||||
b"udta",
|
||||
&atom(
|
||||
b"meta",
|
||||
&[&[0; 4], atom(b"ilst", &ilst).as_slice()].concat(),
|
||||
),
|
||||
);
|
||||
let mut entry = [0; 28];
|
||||
entry[24..26].copy_from_slice(&48_000_u16.to_be_bytes());
|
||||
let moov = [metadata, atom(b"Opus", &entry)].concat();
|
||||
let data = [atom(b"ftyp", b"isom\0\0\0\0"), atom(b"moov", &moov)].concat();
|
||||
for suffix in ["opus", "m4a"] {
|
||||
let mut reader = Cursor::new(&data);
|
||||
reader.set_position(data.len() as u64);
|
||||
let path = format!("Track Title - Artist Name.{suffix}");
|
||||
let scan = read_library_metadata(&mut reader, &path, "", "", 0, &|| Ok(())).unwrap();
|
||||
assert_eq!(scan["trackName"], "Track Title");
|
||||
assert_eq!(scan["artistName"], "Artist Name");
|
||||
assert_eq!(scan["format"], "opus");
|
||||
assert_eq!(scan["sampleRate"], 48_000);
|
||||
assert!(scan.get("metadataFromFilename").is_none());
|
||||
assert_eq!(
|
||||
read_audio_tags(&mut reader, suffix, &|| Ok(()))
|
||||
.unwrap()
|
||||
.title,
|
||||
"Track Title"
|
||||
);
|
||||
let metadata = read_file_metadata(&mut reader, &path, "", &|| Ok(())).unwrap();
|
||||
assert_eq!(metadata["title"], "Track Title");
|
||||
assert_eq!(metadata["audio_codec"], "opus");
|
||||
assert_eq!(
|
||||
extract_cover(&mut reader, suffix, &|| Ok(())).unwrap().data,
|
||||
cover
|
||||
);
|
||||
assert_eq!(reader.into_inner(), &data);
|
||||
}
|
||||
let mut reader = Cursor::new(&data);
|
||||
assert!(read_file_metadata(&mut reader, "misnamed.flac", "", &|| Ok(())).is_err());
|
||||
assert_eq!(
|
||||
read_audio_tags(&mut reader, "opus", &|| Err("cancelled".into())).unwrap_err(),
|
||||
"cancelled"
|
||||
);
|
||||
assert_eq!(
|
||||
extract_cover(&mut reader, "opus", &|| Err("cancelled".into())).unwrap_err(),
|
||||
"cancelled"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Application metadata JSON, preserving the distinct legacy tag-reader JSON.
|
||||
|
||||
use super::{AudioMetadata, read_audio_tags};
|
||||
use super::{AudioMetadata, read_audio_tags, read_container_format};
|
||||
use crate::media::{mp3_quality, ogg_quality, probe_mp4_quality, probe_quality, riff_quality};
|
||||
use serde_json::{Map, Value};
|
||||
use std::io::{self, Read, Seek, SeekFrom};
|
||||
@@ -31,20 +31,21 @@ pub fn read_file_metadata(
|
||||
|
||||
fn read_metadata(file: &mut (impl Read + Seek), path: &str, hint: &str) -> Result<Value, String> {
|
||||
let extension = file_metadata_extension(path, hint)?;
|
||||
let (mut format, mut codec) = match extension.as_str() {
|
||||
".flac" => ("flac", "flac"),
|
||||
".m4a" | ".mp4" | ".aac" => ("m4a", ""),
|
||||
".mp3" => ("mp3", "mp3"),
|
||||
".ogg" | ".opus" => ("opus", "opus"),
|
||||
".ape" => ("ape", "ape"),
|
||||
".wv" => ("wv", "wv"),
|
||||
".mpc" => ("mpc", "mpc"),
|
||||
".wav" => ("wav", "pcm"),
|
||||
".aiff" | ".aif" | ".aifc" => ("aiff", "pcm"),
|
||||
let container = read_container_format(file, &extension[1..])?;
|
||||
let (mut format, mut codec) = match container {
|
||||
"flac" => ("flac", "flac"),
|
||||
"m4a" | "mp4" | "aac" => ("m4a", ""),
|
||||
"mp3" => ("mp3", "mp3"),
|
||||
"ogg" | "opus" => ("opus", "opus"),
|
||||
"ape" => ("ape", "ape"),
|
||||
"wv" => ("wv", "wv"),
|
||||
"mpc" => ("mpc", "mpc"),
|
||||
"wav" => ("wav", "pcm"),
|
||||
"aiff" | "aif" | "aifc" => ("aiff", "pcm"),
|
||||
_ => unreachable!("validated metadata extension"),
|
||||
};
|
||||
let size = file.seek(SeekFrom::End(0)).map_err(|e| e.to_string())? as i64;
|
||||
let mut metadata = read_audio_tags(file, &extension[1..], &|| Ok(()));
|
||||
let mut metadata = read_audio_tags(file, container, &|| Ok(()));
|
||||
if format == "flac" && metadata.is_err() {
|
||||
let fallback = read_audio_tags(file, "ogg", &|| Ok(()));
|
||||
if fallback.is_ok() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Library scan DTOs composed from the existing tag and quality readers.
|
||||
|
||||
use super::{AudioMetadata, CoverArt, file::ObservedReader, read_tags};
|
||||
use super::{AudioMetadata, CoverArt, file::ObservedReader, read_container_format, read_tags};
|
||||
use crate::lyrics::lrc::has_usable_content;
|
||||
use crate::media::{mp3_quality, ogg_quality, probe_mp4_quality, probe_quality, riff_quality};
|
||||
use serde_json::{Value, json};
|
||||
@@ -140,19 +140,20 @@ fn read(
|
||||
mut cover: Option<&mut Option<CoverArt>>,
|
||||
) -> Result<Value, String> {
|
||||
let mut result = library_metadata(path, hint, scan_time, mod_time);
|
||||
let format = library_extension(path, hint);
|
||||
let metadata = read_tags(file, &format, &|| Ok(()), cover.as_deref_mut());
|
||||
let extension = library_extension(path, hint);
|
||||
let format = read_container_format(file, &extension)?;
|
||||
let metadata = read_tags(file, format, &|| Ok(()), cover.as_deref_mut());
|
||||
let tagged = metadata.is_ok();
|
||||
if !tagged && let Some(cover) = cover {
|
||||
*cover = None;
|
||||
}
|
||||
if let Ok(metadata) = metadata {
|
||||
apply_tags(&mut result, metadata, &format, path, hint)?;
|
||||
apply_tags(&mut result, metadata, format, path, hint)?;
|
||||
}
|
||||
// MP4 and RIFF probe quality independently of whether tags are present.
|
||||
if !tagged
|
||||
&& !matches!(
|
||||
format.as_str(),
|
||||
format,
|
||||
"m4a" | "mp4" | "aac" | "wav" | "aiff" | "aif" | "aifc"
|
||||
)
|
||||
{
|
||||
@@ -163,7 +164,7 @@ fn read(
|
||||
.map_err(|error| error.to_string())?;
|
||||
file.seek(SeekFrom::Start(0))
|
||||
.map_err(|error| error.to_string())?;
|
||||
let quality = match format.as_str() {
|
||||
let quality = match format {
|
||||
"flac" => probe_quality(file, &|| Ok(())),
|
||||
"m4a" | "mp4" | "aac" => probe_mp4_quality(file, &|| Ok(())),
|
||||
"mp3" => mp3_quality(file, size as i64),
|
||||
@@ -181,7 +182,7 @@ fn read(
|
||||
result[key] = value.into();
|
||||
}
|
||||
}
|
||||
let bitrate = match format.as_str() {
|
||||
let bitrate = match format {
|
||||
"flac" if quality.total_samples > 0 && quality.sample_rate > 0 => {
|
||||
(size as f64 * 8.0
|
||||
/ (quality.total_samples as f64 / quality.sample_rate as f64)
|
||||
|
||||
Reference in new issue
Block a user