mirror of
https://github.com/zhaarey/AppleMusicDecrypt.git
synced 2025-10-23 15:11:06 +00:00
feat: hyper decryption device
This commit is contained in:
@@ -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"
|
# 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
|
# 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"
|
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]
|
[m3u8Api]
|
||||||
# Use zhaarey's m3u8 api to get higher song m3u8.
|
# Use zhaarey's m3u8 api to get higher song m3u8.
|
||||||
|
|||||||
37
src/adb.py
37
src/adb.py
@@ -14,8 +14,24 @@ from src.exceptions import ADBConnectException, FailedGetAuthParamException, \
|
|||||||
from src.types import AuthParams
|
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:
|
class Device:
|
||||||
host: str
|
host: str
|
||||||
|
serial: str
|
||||||
client: AdbClient
|
client: AdbClient
|
||||||
device: AdbDevice
|
device: AdbDevice
|
||||||
fridaPort: int
|
fridaPort: int
|
||||||
@@ -25,6 +41,7 @@ class Device:
|
|||||||
authParams: AuthParams = None
|
authParams: AuthParams = None
|
||||||
suMethod: str
|
suMethod: str
|
||||||
decryptLock: asyncio.Lock
|
decryptLock: asyncio.Lock
|
||||||
|
hyperDecryptDevices: list[HyperDecryptDevice] = []
|
||||||
|
|
||||||
def __init__(self, host="127.0.0.1", port=5037, su_method: str = "su -c"):
|
def __init__(self, host="127.0.0.1", port=5037, su_method: str = "su -c"):
|
||||||
self.client = AdbClient(host, port)
|
self.client = AdbClient(host, port)
|
||||||
@@ -41,6 +58,7 @@ class Device:
|
|||||||
if not status:
|
if not status:
|
||||||
raise ADBConnectException
|
raise ADBConnectException
|
||||||
self.device = self.client.device(f"{host}:{port}")
|
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]:
|
def _execute_command(self, cmd: str, su: bool = False, sh: bool = False) -> Optional[str]:
|
||||||
whoami = self.device.shell("whoami")
|
whoami = self.device.shell("whoami")
|
||||||
@@ -143,3 +161,22 @@ class Device:
|
|||||||
self.authParams = AuthParams(dsid=dsid, accountToken=token,
|
self.authParams = AuthParams(dsid=dsid, accountToken=token,
|
||||||
accountAccessToken=access_token, storefront=storefront)
|
accountAccessToken=access_token, storefront=storefront)
|
||||||
return self.authParams
|
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)
|
||||||
|
|||||||
@@ -67,7 +67,10 @@ class NewInteractiveShell:
|
|||||||
if not self.storefront_device_mapping.get(auth_params.storefront.lower()):
|
if not self.storefront_device_mapping.get(auth_params.storefront.lower()):
|
||||||
self.storefront_device_mapping.update({auth_params.storefront.lower(): []})
|
self.storefront_device_mapping.update({auth_params.storefront.lower(): []})
|
||||||
self.storefront_device_mapping[auth_params.storefront.lower()].append(device)
|
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):
|
async def command_parser(self, cmd: str):
|
||||||
if not cmd.strip():
|
if not cmd.strip():
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ class Device(BaseModel):
|
|||||||
port: int
|
port: int
|
||||||
agentPort: int
|
agentPort: int
|
||||||
suMethod: str
|
suMethod: str
|
||||||
|
hyperDecrypt: bool
|
||||||
|
hyperDecryptNum: int
|
||||||
|
|
||||||
|
|
||||||
class M3U8Api(BaseModel):
|
class M3U8Api(BaseModel):
|
||||||
|
|||||||
@@ -2,26 +2,31 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from loguru import logger
|
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.exceptions import DecryptException, RetryableDecryptException
|
||||||
from src.models.song_data import Datum
|
from src.models.song_data import Datum
|
||||||
from src.mp4 import SongInfo, SampleInfo
|
from src.mp4 import SongInfo, SampleInfo
|
||||||
from src.types import defaultId, prefetchKey
|
from src.types import defaultId, prefetchKey
|
||||||
|
from src.utils import timeit
|
||||||
|
|
||||||
retry_count = {}
|
retry_count = {}
|
||||||
|
|
||||||
|
|
||||||
@retry(retry=retry_if_exception_type(RetryableDecryptException), stop=stop_after_attempt(3),
|
@retry(retry=retry_if_exception_type(RetryableDecryptException), stop=stop_after_attempt(3),
|
||||||
before_sleep=before_sleep_log(logger, logging.WARN))
|
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:
|
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:
|
try:
|
||||||
reader, writer = await asyncio.open_connection(device.host, device.fridaPort)
|
reader, writer = await asyncio.open_connection(device.host, device.fridaPort)
|
||||||
except ConnectionRefusedError:
|
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()
|
device.restart_inject_frida()
|
||||||
raise RetryableDecryptException
|
raise RetryableDecryptException
|
||||||
decrypted = bytes()
|
decrypted = bytes()
|
||||||
@@ -42,7 +47,7 @@ async def decrypt(info: SongInfo, keys: list[str], manifest: Datum, device: Devi
|
|||||||
try:
|
try:
|
||||||
result = await decrypt_sample(writer, reader, sample)
|
result = await decrypt_sample(writer, reader, sample)
|
||||||
except RetryableDecryptException as e:
|
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")
|
logger.warning(f"Failed to decrypt song: {manifest.attributes.artistName} - {manifest.attributes.name}, retrying")
|
||||||
writer.write(bytes([0, 0, 0, 0]))
|
writer.write(bytes([0, 0, 0, 0]))
|
||||||
writer.close()
|
writer.close()
|
||||||
|
|||||||
14
src/rip.py
14
src/rip.py
@@ -1,4 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import random
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from loguru import logger
|
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.save import save
|
||||||
from src.types import GlobalAuthParams, Codec
|
from src.types import GlobalAuthParams, Codec
|
||||||
from src.url import Song, Album, URLType, Artist, Playlist
|
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)
|
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)
|
codec = get_codec_from_codec_id(codec_id)
|
||||||
raw_song = await download_song(song_uri)
|
raw_song = await download_song(song_uri)
|
||||||
song_info = await extract_song(raw_song, codec)
|
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)
|
song = await encapsulate(song_info, decrypted_song, config.download.atmosConventToM4a)
|
||||||
if not if_raw_atmos(codec, 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)
|
metadata_song = await write_metadata(song, song_metadata, config.metadata.embedMetadata, config.download.coverFormat)
|
||||||
|
|||||||
Reference in New Issue
Block a user