diff --git a/config.example.toml b/config.example.toml index d20b0e8..22375c6 100644 --- a/config.example.toml +++ b/config.example.toml @@ -18,6 +18,10 @@ agentPort = 10020 # For Magisk user, the recommend value is "su -c". For other Root solutions, the recommend value is "su 0" # If not sure which method to use, execute “su 0 ls /” and “su -c ls /” respectively in adb shell and choose the output method suMethod = "su -c" +# Inject multiple scripts into devices to simulate multi-device decryption, which can speed up decryption +# Experimental feature +hyperDecrypt = false +hyperDecryptNum = 2 [m3u8Api] # Use zhaarey's m3u8 api to get higher song m3u8. diff --git a/src/adb.py b/src/adb.py index b33e121..b0edf76 100644 --- a/src/adb.py +++ b/src/adb.py @@ -14,8 +14,24 @@ from src.exceptions import ADBConnectException, FailedGetAuthParamException, \ from src.types import AuthParams +class HyperDecryptDevice: + host: str + fridaPort: int + decryptLock: asyncio.Lock + serial: str + _father_device = None + + def __init__(self, host: str, port: int, father_device): + self.host = host + self.fridaPort = port + self.decryptLock = asyncio.Lock() + self.serial = f"{host}:{port}" + self._father_device = father_device + + class Device: host: str + serial: str client: AdbClient device: AdbDevice fridaPort: int @@ -25,6 +41,7 @@ class Device: authParams: AuthParams = None suMethod: str decryptLock: asyncio.Lock + hyperDecryptDevices: list[HyperDecryptDevice] = [] def __init__(self, host="127.0.0.1", port=5037, su_method: str = "su -c"): self.client = AdbClient(host, port) @@ -41,6 +58,7 @@ class Device: if not status: raise ADBConnectException self.device = self.client.device(f"{host}:{port}") + self.serial = self.device.serial def _execute_command(self, cmd: str, su: bool = False, sh: bool = False) -> Optional[str]: whoami = self.device.shell("whoami") @@ -143,3 +161,22 @@ class Device: self.authParams = AuthParams(dsid=dsid, accountToken=token, accountAccessToken=access_token, storefront=storefront) return self.authParams + + def hyper_decrypt(self, ports: list[int]): + if not self._if_frida_running(): + raise FridaNotRunningException + logger.debug("injecting agent script with hyper decrypt") + self.fridaPort = ports[0] + if not self.fridaDevice: + frida.get_device_manager().add_remote_device(self.device.serial) + self.fridaDevice = frida.get_device_manager().get_device(self.device.serial) + self.pid = self.fridaDevice.spawn("com.apple.android.music") + self.fridaSession = self.fridaDevice.attach(self.pid) + for port in ports: + self._start_forward(port, port) + with open("agent.js", "r") as f: + agent = f.read().replace("2147483647", str(port)) + script: frida.core.Script = self.fridaSession.create_script(agent) + script.load() + self.hyperDecryptDevices.append(HyperDecryptDevice(host=self.host, port=port, father_device=self)) + self.fridaDevice.resume(self.pid) diff --git a/src/cmd.py b/src/cmd.py index 834bdb3..47471c6 100644 --- a/src/cmd.py +++ b/src/cmd.py @@ -67,7 +67,10 @@ class NewInteractiveShell: if not self.storefront_device_mapping.get(auth_params.storefront.lower()): self.storefront_device_mapping.update({auth_params.storefront.lower(): []}) self.storefront_device_mapping[auth_params.storefront.lower()].append(device) - device.start_inject_frida(device_info.agentPort) + if device_info.hyperDecrypt: + device.hyper_decrypt(list(range(device_info.agentPort, device_info.agentPort + device_info.hyperDecryptNum))) + else: + device.start_inject_frida(device_info.agentPort) async def command_parser(self, cmd: str): if not cmd.strip(): diff --git a/src/config.py b/src/config.py index 19934ba..8ab439f 100644 --- a/src/config.py +++ b/src/config.py @@ -13,6 +13,8 @@ class Device(BaseModel): port: int agentPort: int suMethod: str + hyperDecrypt: bool + hyperDecryptNum: int class M3U8Api(BaseModel): diff --git a/src/decrypt.py b/src/decrypt.py index 9143740..eb19af1 100644 --- a/src/decrypt.py +++ b/src/decrypt.py @@ -2,26 +2,31 @@ import asyncio import logging from loguru import logger -from tenacity import retry, retry_if_exception_type, stop_after_attempt, before_sleep_log, RetryCallState +from tenacity import retry, retry_if_exception_type, stop_after_attempt, before_sleep_log -from src.adb import Device +from src.adb import Device, HyperDecryptDevice from src.exceptions import DecryptException, RetryableDecryptException from src.models.song_data import Datum from src.mp4 import SongInfo, SampleInfo from src.types import defaultId, prefetchKey +from src.utils import timeit retry_count = {} @retry(retry=retry_if_exception_type(RetryableDecryptException), stop=stop_after_attempt(3), before_sleep=before_sleep_log(logger, logging.WARN)) -async def decrypt(info: SongInfo, keys: list[str], manifest: Datum, device: Device) -> bytes: +@timeit +async def decrypt(info: SongInfo, keys: list[str], manifest: Datum, device: Device | HyperDecryptDevice) -> bytes: async with device.decryptLock: - logger.info(f"Decrypting song: {manifest.attributes.artistName} - {manifest.attributes.name}") + if isinstance(device, HyperDecryptDevice): + logger.info(f"Using hyperDecryptDevice {device.serial} to decrypt song: {manifest.attributes.artistName} - {manifest.attributes.name}") + else: + logger.info(f"Using device {device.serial} to decrypt song: {manifest.attributes.artistName} - {manifest.attributes.name}") try: reader, writer = await asyncio.open_connection(device.host, device.fridaPort) except ConnectionRefusedError: - logger.warning(f"Failed to connect to device {device.device.serial}, re-injecting") + logger.warning(f"Failed to connect to device {device.serial}, re-injecting") device.restart_inject_frida() raise RetryableDecryptException decrypted = bytes() @@ -42,7 +47,7 @@ async def decrypt(info: SongInfo, keys: list[str], manifest: Datum, device: Devi try: result = await decrypt_sample(writer, reader, sample) except RetryableDecryptException as e: - if 0 <= retry_count.get(device.device.serial, 0) < 3 or 4 <= retry_count.get(device.device.serial, 0) < 6: + if 0 <= retry_count.get(device.serial, 0) < 3 or 4 <= retry_count.get(device.serial, 0) < 6: logger.warning(f"Failed to decrypt song: {manifest.attributes.artistName} - {manifest.attributes.name}, retrying") writer.write(bytes([0, 0, 0, 0])) writer.close() diff --git a/src/rip.py b/src/rip.py index ccd474b..2b6396d 100644 --- a/src/rip.py +++ b/src/rip.py @@ -1,4 +1,5 @@ import asyncio +import random import subprocess from loguru import logger @@ -15,7 +16,7 @@ from src.mp4 import extract_media, extract_song, encapsulate, write_metadata, fi from src.save import save from src.types import GlobalAuthParams, Codec from src.url import Song, Album, URLType, Artist, Playlist -from src.utils import check_song_exists, if_raw_atmos, playlist_write_song_index, get_codec_from_codec_id +from src.utils import check_song_exists, if_raw_atmos, playlist_write_song_index, get_codec_from_codec_id, timeit task_lock = asyncio.Semaphore(16) @@ -80,7 +81,16 @@ async def rip_song(song: Song, auth_params: GlobalAuthParams, codec: str, config codec = get_codec_from_codec_id(codec_id) raw_song = await download_song(song_uri) song_info = await extract_song(raw_song, codec) - decrypted_song = await decrypt(song_info, keys, song_data, device) + if device.hyperDecryptDevices: + if all([hyper_device.decryptLock.locked() for hyper_device in device.hyperDecryptDevices]): + decrypted_song = await decrypt(song_info, keys, song_data, random.choice(device.hyperDecryptDevices)) + else: + for hyperDecryptDevice in device.hyperDecryptDevices: + if not hyperDecryptDevice.decryptLock.locked(): + decrypted_song = await decrypt(song_info, keys, song_data, hyperDecryptDevice) + break + else: + decrypted_song = await decrypt(song_info, keys, song_data, device) song = await encapsulate(song_info, decrypted_song, config.download.atmosConventToM4a) if not if_raw_atmos(codec, config.download.atmosConventToM4a): metadata_song = await write_metadata(song, song_metadata, config.metadata.embedMetadata, config.download.coverFormat)