From fd03f074a9b95d45e3eaf1d1167616b7cd9a00db Mon Sep 17 00:00:00 2001 From: WorldObservationLog Date: Sat, 18 May 2024 17:22:45 +0800 Subject: [PATCH] fix: MP4Box encapsulation error --- src/mp4.py | 18 +++++++++++++++++- src/rip.py | 5 +++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/mp4.py b/src/mp4.py index c881e02..b2650f5 100644 --- a/src/mp4.py +++ b/src/mp4.py @@ -156,7 +156,7 @@ def encapsulate(song_info: SongInfo, decrypted_media: bytes, atmos_convent: bool f.write(str(nhml_xml)) subprocess.run(f"gpac -i {nhml_name.absolute()} nhmlr -o {song_name.absolute()}", stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - subprocess.run(f'mp4box -ipod {song_name.absolute()}', + subprocess.run(f'mp4box -brand "M4A " -ab "M4A " -ab "mp42" {song_name.absolute()}', stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) with open(song_name.absolute(), "rb") as f: final_song = f.read() @@ -188,3 +188,19 @@ def write_metadata(song: bytes, metadata: SongMetadata, embed_metadata: list[str embed_song = f.read() tmp_dir.cleanup() return embed_song + + +# There are suspected errors in M4A files encapsulated by MP4Box and GPAC, +# causing some applications to be unable to correctly process Metadata (such as Android.media, Salt Music) +# Using FFMPEG re-encapsulating solves this problem +def fix_encapsulate(song: bytes) -> bytes: + tmp_dir = TemporaryDirectory() + name = uuid.uuid4().hex + song_name = Path(tmp_dir.name) / Path(f"{name}.m4a") + new_song_name = Path(tmp_dir.name) / Path(f"{name}_fixed.m4a") + with open(song_name.absolute(), "wb") as f: + f.write(song) + subprocess.run(["ffmpeg", "-y", "-i", song_name.absolute(), "-fflags", "+bitexact", "-c:a", "copy", "-c:v", "copy", + new_song_name.absolute()], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + with open(new_song_name.absolute(), "rb") as f: + return f.read() diff --git a/src/rip.py b/src/rip.py index 6a2e9c3..32245f0 100644 --- a/src/rip.py +++ b/src/rip.py @@ -11,7 +11,7 @@ from src.adb import Device from src.decrypt import decrypt from src.metadata import SongMetadata from src.models import PlaylistInfo -from src.mp4 import extract_media, extract_song, encapsulate, write_metadata +from src.mp4 import extract_media, extract_song, encapsulate, write_metadata, fix_encapsulate from src.save import save from src.types import GlobalAuthParams, Codec from src.url import Song, Album, URLType, Artist, Playlist @@ -73,7 +73,8 @@ async def rip_song(song: Song, auth_params: GlobalAuthParams, codec: str, config decrypted_song = await decrypt(song_info, keys, song_data, device) song = encapsulate(song_info, decrypted_song, config.download.atmosConventToM4a) if not if_raw_atmos(codec, config.download.atmosConventToM4a): - song = write_metadata(song, song_metadata, config.metadata.embedMetadata, config.download.coverFormat) + metadata_song = write_metadata(song, song_metadata, config.metadata.embedMetadata, config.download.coverFormat) + song = fix_encapsulate(metadata_song) filename = save(song, codec, song_metadata, config.download, playlist) logger.info(f"Song {song_metadata.artist} - {song_metadata.title} saved!") if config.download.afterDownloaded: