mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-11 02:08:44 +02:00
feat(metadata): remove ReplayGain from individual and selected tracks
This commit is contained in:
1 parent
c24132cb9b
commit
9d530cea9e
19 files changed
+835
-171
No files matched your search
@@ -19,6 +19,21 @@ use std::sync::LazyLock;
|
||||
type Fields = BTreeMap<String, String>;
|
||||
const MAX_TAG_BYTES: usize = 64 * 1024 * 1024;
|
||||
|
||||
fn clears_replay_gain(fields: &Fields) -> bool {
|
||||
[
|
||||
"replaygain_track_gain",
|
||||
"replaygain_track_peak",
|
||||
"replaygain_album_gain",
|
||||
"replaygain_album_peak",
|
||||
]
|
||||
.iter()
|
||||
.all(|key| {
|
||||
fields
|
||||
.get(*key)
|
||||
.is_some_and(|value| value.trim().is_empty())
|
||||
})
|
||||
}
|
||||
|
||||
struct Section {
|
||||
start: u64,
|
||||
end: u64,
|
||||
|
||||
@@ -16,7 +16,7 @@ pub(super) fn edit(
|
||||
cover: Option<&[u8]>,
|
||||
check: &dyn Fn() -> Result<(), String>,
|
||||
) -> Result<Section, String> {
|
||||
let end = seek(source, SeekFrom::End(0))?;
|
||||
let mut end = seek(source, SeekFrom::End(0))?;
|
||||
let mut start = end;
|
||||
let mut existing = None;
|
||||
for offset in [end.checked_sub(32), end.checked_sub(161).map(|_| end - 160)]
|
||||
@@ -32,6 +32,11 @@ pub(super) fn edit(
|
||||
start = (offset + 32)
|
||||
.checked_sub(size)
|
||||
.ok_or("invalid APE tag size")?;
|
||||
if fields.len() == 4 && super::clears_replay_gain(fields) {
|
||||
// A legacy ID3v1 tag can follow the APE footer. Removing gain
|
||||
// must leave that unrelated metadata in place too.
|
||||
end = offset + 32;
|
||||
}
|
||||
}
|
||||
if existing.is_none()
|
||||
&& let Ok(begin) = footer.items_start(offset)
|
||||
@@ -140,6 +145,13 @@ pub(super) fn edit(
|
||||
items.retain(|item| !remove.contains(&uppercase(&String::from_utf8_lossy(&item.key))));
|
||||
items.extend(added);
|
||||
if items.is_empty() {
|
||||
if super::clears_replay_gain(fields) {
|
||||
return Ok(Section {
|
||||
start,
|
||||
end,
|
||||
data: Vec::new(),
|
||||
});
|
||||
}
|
||||
return Err("empty APE tag".into());
|
||||
}
|
||||
let mut body = Vec::new();
|
||||
|
||||
@@ -82,8 +82,8 @@ pub(super) fn edit(
|
||||
}
|
||||
let replay_gain = replay_gain(fields);
|
||||
if !replay_gain.is_empty() {
|
||||
// Go replaces the entire ReplayGain group when any supplied value is
|
||||
// nonempty. A request containing only empty values leaves it alone.
|
||||
// Replace the entire group, or explicitly clear it when all four
|
||||
// ReplayGain fields are supplied empty (including derived Sound Check).
|
||||
remove_names.extend(
|
||||
[
|
||||
"REPLAYGAIN_TRACK_GAIN",
|
||||
@@ -95,7 +95,9 @@ pub(super) fn edit(
|
||||
.map(str::to_owned),
|
||||
);
|
||||
for (name, value) in replay_gain {
|
||||
appended.extend(freeform(&name, &value));
|
||||
if !value.is_empty() {
|
||||
appended.extend(freeform(&name, &value));
|
||||
}
|
||||
}
|
||||
}
|
||||
let edit_track = fields.contains_key("track_number") || fields.contains_key("track_total");
|
||||
@@ -499,8 +501,12 @@ fn replay_gain(fields: &Fields) -> Fields {
|
||||
.get(name)
|
||||
.map(|v| (name.to_owned(), v.trim().to_owned()))
|
||||
})
|
||||
.filter(|(_, value)| !value.is_empty())
|
||||
.collect();
|
||||
// Preserve partial empty updates as no-ops; clearing the whole group is an
|
||||
// explicit request so ordinary metadata edits cannot discard normalization.
|
||||
if !super::clears_replay_gain(fields) {
|
||||
result.retain(|_, value| !value.is_empty());
|
||||
}
|
||||
static NUMBER: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"[+-]?[0-9]+(?:\.[0-9]+)?").unwrap());
|
||||
let gain = result
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::{Fields, Section, bytes, id3, metadata_fields, seek};
|
||||
use crate::tags::{file::ObservedReader, read_audio_tags};
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom};
|
||||
|
||||
pub(super) fn edit(
|
||||
source: &mut (impl Read + Seek),
|
||||
@@ -30,6 +30,7 @@ pub(super) fn edit(
|
||||
let mut start = 12_u64;
|
||||
let mut body_size = 4_u64;
|
||||
let mut embedded_cover = None;
|
||||
let remove_gain_only = fields.len() == 4 && super::clears_replay_gain(fields);
|
||||
for _ in 0..65536 {
|
||||
check()?;
|
||||
if start + 8 > length {
|
||||
@@ -44,6 +45,38 @@ pub(super) fn edit(
|
||||
};
|
||||
let end = start + 8 + u64::from(size) + u64::from(size & 1);
|
||||
if header[..4].eq_ignore_ascii_case(b"id3 ") {
|
||||
if remove_gain_only {
|
||||
// Edit the existing ID3 frames so unknown tags and all artwork
|
||||
// survive removal, instead of rebuilding from parsed metadata.
|
||||
if size as usize > super::MAX_TAG_BYTES || end > length {
|
||||
return Err("invalid RIFF ID3 chunk size".into());
|
||||
}
|
||||
let (tag, _) = id3::header(
|
||||
&mut Cursor::new(bytes(source, size as usize)?),
|
||||
fields,
|
||||
None,
|
||||
check,
|
||||
)?;
|
||||
let tag_size = tag.len() as u32;
|
||||
let mut chunk = header[..4].to_vec();
|
||||
chunk.extend(if aiff {
|
||||
tag_size.to_be_bytes()
|
||||
} else {
|
||||
tag_size.to_le_bytes()
|
||||
});
|
||||
chunk.extend(tag);
|
||||
if tag_size & 1 == 1 {
|
||||
chunk.push(0);
|
||||
}
|
||||
body_size += chunk.len() as u64;
|
||||
sections.push(Section {
|
||||
start,
|
||||
end,
|
||||
data: chunk,
|
||||
});
|
||||
start = end;
|
||||
continue;
|
||||
}
|
||||
if cover.is_none()
|
||||
&& size > 0
|
||||
&& size <= 16 * 1024 * 1024
|
||||
@@ -68,6 +101,20 @@ pub(super) fn edit(
|
||||
if start + 8 <= length {
|
||||
return Err("RIFF chunk count exceeds 65536".into());
|
||||
}
|
||||
if remove_gain_only {
|
||||
let size = u32::try_from(body_size).map_err(|_| "RIFF container exceeds 32-bit size")?;
|
||||
sections.push(Section {
|
||||
start: 4,
|
||||
end: 8,
|
||||
data: if aiff {
|
||||
size.to_be_bytes()
|
||||
} else {
|
||||
size.to_le_bytes()
|
||||
}
|
||||
.to_vec(),
|
||||
});
|
||||
return Ok(sections);
|
||||
}
|
||||
let cover_path = fields.get("cover_path").map(|p| p.trim()).unwrap_or("");
|
||||
if !cover_path.is_empty() && cover.is_none() {
|
||||
return Err("read cover art: file not found".into());
|
||||
|
||||
Reference in new issue
Block a user