mirror of
https://github.com/zhaarey/AppleMusicDecrypt.git
synced 2025-10-23 15:11:06 +00:00
feat: mitm download
This commit is contained in:
@@ -23,3 +23,7 @@ afterDownloaded = ""
|
|||||||
embedMetadata = ["title", "artist", "album", "album_artist", "composer",
|
embedMetadata = ["title", "artist", "album", "album_artist", "composer",
|
||||||
"genre", "created", "track", "tracknum", "disk", "lyrics", "cover", "copyright",
|
"genre", "created", "track", "tracknum", "disk", "lyrics", "cover", "copyright",
|
||||||
"record_company", "upc", "isrc"]
|
"record_company", "upc", "isrc"]
|
||||||
|
|
||||||
|
[mitm]
|
||||||
|
host = "127.0.0.1"
|
||||||
|
port = "11080"
|
||||||
1809
poetry.lock
generated
1809
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,7 @@ pure-python-adb = "^0.3.0.dev0"
|
|||||||
frida = "^16.2.1"
|
frida = "^16.2.1"
|
||||||
tenacity = "^8.2.3"
|
tenacity = "^8.2.3"
|
||||||
prompt-toolkit = "^3.0.43"
|
prompt-toolkit = "^3.0.43"
|
||||||
|
mitmproxy = "^10.3.0"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
|
|||||||
64
src/cmd.py
64
src/cmd.py
@@ -13,9 +13,9 @@ from src.api import get_token
|
|||||||
from src.config import Config
|
from src.config import Config
|
||||||
from src.rip import rip_song, rip_album
|
from src.rip import rip_song, rip_album
|
||||||
from src.types import GlobalAuthParams
|
from src.types import GlobalAuthParams
|
||||||
from src.url import AppleMusicURL, URLType
|
|
||||||
from src.url import AppleMusicURL, URLType, Song
|
from src.url import AppleMusicURL, URLType, Song
|
||||||
from src.utils import get_song_id_from_m3u8
|
from src.utils import get_song_id_from_m3u8
|
||||||
|
from src.mitm import start_proxy
|
||||||
|
|
||||||
|
|
||||||
class NewInteractiveShell:
|
class NewInteractiveShell:
|
||||||
@@ -47,6 +47,11 @@ class NewInteractiveShell:
|
|||||||
default="alac")
|
default="alac")
|
||||||
m3u8_parser.add_argument("-f", "--force", type=bool, default=False)
|
m3u8_parser.add_argument("-f", "--force", type=bool, default=False)
|
||||||
subparser.add_parser("exit")
|
subparser.add_parser("exit")
|
||||||
|
mitm_parser = subparser.add_parser("mitm")
|
||||||
|
mitm_parser.add_argument("-c", "--codec",
|
||||||
|
choices=["alac", "ec3", "aac", "aac-binaural", "aac-downmix", "ac3"],
|
||||||
|
default="alac")
|
||||||
|
mitm_parser.add_argument("-f", "--force", type=bool, default=False)
|
||||||
|
|
||||||
logger.remove()
|
logger.remove()
|
||||||
logger.add(lambda msg: print_formatted_text(ANSI(msg), end=""), colorize=True, level="INFO")
|
logger.add(lambda msg: print_formatted_text(ANSI(msg), end=""), colorize=True, level="INFO")
|
||||||
@@ -74,20 +79,17 @@ class NewInteractiveShell:
|
|||||||
match cmds[0]:
|
match cmds[0]:
|
||||||
case "download":
|
case "download":
|
||||||
await self.do_download(args.url, args.codec, args.force)
|
await self.do_download(args.url, args.codec, args.force)
|
||||||
|
case "m3u8":
|
||||||
|
await self.do_m3u8(args.url, args.codec, args.force)
|
||||||
|
case "mitm":
|
||||||
|
await self.do_mitm(args.codec, args.force)
|
||||||
case "exit":
|
case "exit":
|
||||||
self.loop.stop()
|
self.loop.stop()
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
async def do_download(self, raw_url: str, codec: str, force_download: bool):
|
async def do_download(self, raw_url: str, codec: str, force_download: bool):
|
||||||
url = AppleMusicURL.parse_url(raw_url)
|
url = AppleMusicURL.parse_url(raw_url)
|
||||||
devices = self.storefront_device_mapping.get(url.storefront)
|
available_device = await self._get_available_device(url.storefront)
|
||||||
if not devices:
|
|
||||||
logger.error(f"No device is available to decrypt the specified region: {url.storefront}")
|
|
||||||
available_devices = [device for device in devices if not device.decryptLock.locked()]
|
|
||||||
if not available_devices:
|
|
||||||
available_device: Device = random.choice(devices)
|
|
||||||
else:
|
|
||||||
available_device: Device = random.choice(available_devices)
|
|
||||||
global_auth_param = GlobalAuthParams.from_auth_params_and_token(available_device.get_auth_params(),
|
global_auth_param = GlobalAuthParams.from_auth_params_and_token(available_device.get_auth_params(),
|
||||||
self.anonymous_access_token)
|
self.anonymous_access_token)
|
||||||
match url.type:
|
match url.type:
|
||||||
@@ -97,6 +99,50 @@ class NewInteractiveShell:
|
|||||||
case URLType.Album:
|
case URLType.Album:
|
||||||
task = self.loop.create_task(rip_album(url, global_auth_param, codec, self.config, available_device))
|
task = self.loop.create_task(rip_album(url, global_auth_param, codec, self.config, available_device))
|
||||||
|
|
||||||
|
async def do_m3u8(self, m3u8_url: str, codec: str, force_download: bool):
|
||||||
|
song_id = get_song_id_from_m3u8(m3u8_url)
|
||||||
|
song = Song(id=song_id, storefront=self.config.region.defaultStorefront, url="", type=URLType.Song)
|
||||||
|
available_device = await self._get_available_device(self.config.region.defaultStorefront)
|
||||||
|
global_auth_param = GlobalAuthParams.from_auth_params_and_token(available_device.get_auth_params(),
|
||||||
|
self.anonymous_access_token)
|
||||||
|
self.loop.create_task(
|
||||||
|
rip_song(song, global_auth_param, codec, self.config, available_device, force_save=force_download,
|
||||||
|
specified_m3u8=m3u8_url)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def do_mitm(self, codec: str, force_download: bool):
|
||||||
|
available_device = await self._get_available_device(self.config.region.defaultStorefront)
|
||||||
|
global_auth_param = GlobalAuthParams.from_auth_params_and_token(available_device.get_auth_params(),
|
||||||
|
self.anonymous_access_token)
|
||||||
|
m3u8_urls = set()
|
||||||
|
tasks = set()
|
||||||
|
|
||||||
|
def callback(m3u8_url):
|
||||||
|
if m3u8_url in m3u8_urls:
|
||||||
|
return
|
||||||
|
song_id = get_song_id_from_m3u8(m3u8_url)
|
||||||
|
song = Song(id=song_id, storefront=self.config.region.defaultStorefront, url="", type=URLType.Song)
|
||||||
|
task = self.loop.create_task(
|
||||||
|
rip_song(song, global_auth_param, codec, self.config, available_device, force_save=force_download,
|
||||||
|
specified_m3u8=m3u8_url)
|
||||||
|
)
|
||||||
|
tasks.update(task)
|
||||||
|
task.add_done_callback(tasks.remove)
|
||||||
|
m3u8_urls.update(m3u8_url)
|
||||||
|
|
||||||
|
self.loop.create_task(start_proxy(self.config.mitm.host, self.config.mitm.port, callback))
|
||||||
|
|
||||||
|
async def _get_available_device(self, storefront: str):
|
||||||
|
devices = self.storefront_device_mapping.get(storefront)
|
||||||
|
if not devices:
|
||||||
|
logger.error(f"No device is available to decrypt the specified region: {storefront}")
|
||||||
|
available_devices = [device for device in devices if not device.decryptLock.locked()]
|
||||||
|
if not available_devices:
|
||||||
|
available_device: Device = random.choice(devices)
|
||||||
|
else:
|
||||||
|
available_device: Device = random.choice(available_devices)
|
||||||
|
return available_device
|
||||||
|
|
||||||
async def handle_command(self):
|
async def handle_command(self):
|
||||||
session = PromptSession("> ")
|
session = PromptSession("> ")
|
||||||
|
|
||||||
|
|||||||
@@ -31,11 +31,17 @@ class Metadata(BaseModel):
|
|||||||
embedMetadata: list[str]
|
embedMetadata: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
class Mitm(BaseModel):
|
||||||
|
host: str
|
||||||
|
port: int
|
||||||
|
|
||||||
|
|
||||||
class Config(BaseModel):
|
class Config(BaseModel):
|
||||||
language: Language
|
region: Region
|
||||||
devices: list[Device]
|
devices: list[Device]
|
||||||
download: Download
|
download: Download
|
||||||
metadata: Metadata
|
metadata: Metadata
|
||||||
|
mitm: Mitm
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load_from_config(cls, config_file: str = "config.toml"):
|
def load_from_config(cls, config_file: str = "config.toml"):
|
||||||
|
|||||||
33
src/mitm.py
Normal file
33
src/mitm.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import plistlib
|
||||||
|
|
||||||
|
import mitmproxy.http
|
||||||
|
from mitmproxy import options
|
||||||
|
from mitmproxy.tools import dump
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
|
||||||
|
class RequestHandler:
|
||||||
|
def __init__(self, callback):
|
||||||
|
self.callback = callback
|
||||||
|
|
||||||
|
def response(self, flow: mitmproxy.http.HTTPFlow):
|
||||||
|
if flow.request.host == "play.itunes.apple.com" and flow.request.path == "/WebObjects/MZPlay.woa/wa/subPlaybackDispatch":
|
||||||
|
data = plistlib.loads(flow.response.content)
|
||||||
|
m3u8 = data["songList"][0]["hls-playlist-url"]
|
||||||
|
flow.response.status_code = 500
|
||||||
|
self.callback(m3u8)
|
||||||
|
|
||||||
|
|
||||||
|
async def start_proxy(host, port, callback):
|
||||||
|
opts = options.Options(listen_host=host, listen_port=port, mode=["socks5"])
|
||||||
|
|
||||||
|
master = dump.DumpMaster(
|
||||||
|
opts,
|
||||||
|
with_termlog=False,
|
||||||
|
with_dumper=False,
|
||||||
|
)
|
||||||
|
master.addons.add(RequestHandler(callback))
|
||||||
|
|
||||||
|
logger.info(f"Mitmproxy started at socks5://{host}:{port}")
|
||||||
|
|
||||||
|
await master.run()
|
||||||
Reference in New Issue
Block a user