Compare commits

...

5 Commits

Author SHA1 Message Date
WorldObservationLog c4cd35662e fix: do not exit when typed wrong command 4 months ago
WorldObservationLog 0779a6aead docs: download from a file including urls 4 months ago
WorldObservationLog 1a555440ac feat: download from a file including urls 4 months ago
WorldObservationLog 6f264e547b fix: more stable get_m3u8 func 4 months ago
WorldObservationLog 430d144958 feat: remove mitm feature 4 months ago
  1. 2
      README.md
  2. 17
      src/adb.py
  3. 56
      src/cmd.py
  4. 3
      src/exceptions.py
  5. 33
      src/mitm.py

@ -23,6 +23,8 @@ dl https://music.apple.com/jp/artist/%E3%83%88%E3%82%B2%E3%83%8A%E3%82%B7%E3%83%
dl --include-participate-songs https://music.apple.com/jp/artist/%E3%83%88%E3%82%B2%E3%83%8A%E3%82%B7%E3%83%88%E3%82%B2%E3%82%A2%E3%83%AA/1688539273 dl --include-participate-songs https://music.apple.com/jp/artist/%E3%83%88%E3%82%B2%E3%83%8A%E3%82%B7%E3%83%88%E3%82%B2%E3%82%A2%E3%83%AA/1688539273
# Download all songs of specified playlist # Download all songs of specified playlist
dl https://music.apple.com/jp/playlist/bocchi-the-rock/pl.u-Ympg5s39LRqp dl https://music.apple.com/jp/playlist/bocchi-the-rock/pl.u-Ympg5s39LRqp
# Download from a file including links
dlf urls.txt
# Download song from specified m3u8 with default codec (alac) # Download song from specified m3u8 with default codec (alac)
m3u8 https://aod.itunes.apple.com/itunes-assets/HLSMusic116/v4/cb/f0/91/cbf09175-ce98-d133-1936-2e46b6992aa5/P631756252_lossless.m3u8 m3u8 https://aod.itunes.apple.com/itunes-assets/HLSMusic116/v4/cb/f0/91/cbf09175-ce98-d133-1936-2e46b6992aa5/P631756252_lossless.m3u8
``` ```

@ -6,11 +6,12 @@ from typing import Optional
import frida import frida
import regex import regex
from loguru import logger from loguru import logger
from tenacity import retry, retry_if_exception_type, wait_random_exponential, stop_after_attempt
from ppadb.client import Client as AdbClient from ppadb.client import Client as AdbClient
from ppadb.device import Device as AdbDevice from ppadb.device import Device as AdbDevice
from src.exceptions import ADBConnectException, FailedGetAuthParamException, \ from src.exceptions import ADBConnectException, FailedGetAuthParamException, \
FridaNotRunningException FridaNotRunningException, FailedGetM3U8FromDeviceException
from src.types import AuthParams from src.types import AuthParams
@ -43,6 +44,7 @@ class Device:
decryptLock: asyncio.Lock decryptLock: asyncio.Lock
hyperDecryptDevices: list[HyperDecryptDevice] = [] hyperDecryptDevices: list[HyperDecryptDevice] = []
m3u8Script: frida.core.Script m3u8Script: frida.core.Script
_m3u8ScriptLock = asyncio.Lock()
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)
@ -50,16 +52,19 @@ class Device:
self.host = host self.host = host
self.decryptLock = asyncio.Lock() self.decryptLock = asyncio.Lock()
@retry(retry=retry_if_exception_type(FailedGetM3U8FromDeviceException), wait=wait_random_exponential(min=4, max=20),
stop=stop_after_attempt(8))
async def get_m3u8(self, adam_id: str): async def get_m3u8(self, adam_id: str):
async with self._m3u8ScriptLock:
try: try:
result: str = await self.m3u8Script.exports_async.getm3u8(adam_id) result = await self.m3u8Script.exports_async.getm3u8(adam_id)
except frida.core.RPCException: except frida.core.RPCException or isinstance(result, int):
# The script takes 8 seconds to start. # The script takes 8 seconds to start.
# If the script does not start when the function is called, wait 8 seconds and call again. # If the script does not start when the function is called, wait 8 seconds and call again.
await asyncio.sleep(8) await asyncio.sleep(8)
result: str = await self.m3u8Script.exports_async.getm3u8(adam_id) result = await self.m3u8Script.exports_async.getm3u8(adam_id)
if result.isdigit(): if isinstance(result, int):
return None raise FailedGetM3U8FromDeviceException
return result return result
def connect(self, host: str, port: int): def connect(self, host: str, port: int):

@ -15,7 +15,6 @@ from src.rip import rip_song, rip_album, rip_artist, rip_playlist
from src.types import GlobalAuthParams from src.types import GlobalAuthParams
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:
@ -42,7 +41,12 @@ class NewInteractiveShell:
default="alac") default="alac")
download_parser.add_argument("-f", "--force", default=False, action="store_true") download_parser.add_argument("-f", "--force", default=False, action="store_true")
download_parser.add_argument("--include-participate-songs", default=False, dest="include", action="store_true") download_parser.add_argument("--include-participate-songs", default=False, dest="include", action="store_true")
# download_from_file_parser = subparser.add_parser("download-from-file", aliases=["dlf"]) download_from_file_parser = subparser.add_parser("download-from-file", aliases=["dlf"])
download_from_file_parser.add_argument("file", type=str)
download_from_file_parser.add_argument("-f", "--force", default=False, action="store_true")
download_from_file_parser.add_argument("-c", "--codec",
choices=["alac", "ec3", "aac", "aac-binaural", "aac-downmix", "ac3"],
default="alac")
m3u8_parser = subparser.add_parser("m3u8") m3u8_parser = subparser.add_parser("m3u8")
m3u8_parser.add_argument("url", type=str) m3u8_parser.add_argument("url", type=str)
m3u8_parser.add_argument("-c", "--codec", m3u8_parser.add_argument("-c", "--codec",
@ -50,11 +54,6 @@ class NewInteractiveShell:
default="alac") default="alac")
m3u8_parser.add_argument("-f", "--force", default=False, action="store_true") m3u8_parser.add_argument("-f", "--force", default=False, action="store_true")
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", default=False, action="store_true")
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")
@ -79,7 +78,7 @@ class NewInteractiveShell:
cmds = cmd.split(" ") cmds = cmd.split(" ")
try: try:
args = self.parser.parse_args(cmds) args = self.parser.parse_args(cmds)
except argparse.ArgumentError: except (argparse.ArgumentError, argparse.ArgumentTypeError, SystemExit):
logger.warning(f"Unknown command: {cmd}") logger.warning(f"Unknown command: {cmd}")
return return
match cmds[0]: match cmds[0]:
@ -87,8 +86,8 @@ class NewInteractiveShell:
await self.do_download(args.url, args.codec, args.force, args.include) await self.do_download(args.url, args.codec, args.force, args.include)
case "m3u8": case "m3u8":
await self.do_m3u8(args.url, args.codec, args.force) await self.do_m3u8(args.url, args.codec, args.force)
case "mitm": case "download-from-file" | "dlf":
await self.do_mitm(args.codec, args.force) await self.do_download_from_file(args.file, args.codec, args.force)
case "exit": case "exit":
self.loop.stop() self.loop.stop()
sys.exit() sys.exit()
@ -134,36 +133,13 @@ class NewInteractiveShell:
specified_m3u8=m3u8_url) specified_m3u8=m3u8_url)
) )
async def do_mitm(self, codec: str, force_download: bool): async def do_download_from_file(self, file: str, codec: str, force_download: bool):
available_device = await self._get_available_device(self.config.region.defaultStorefront) with open(file, "r", encoding="utf-8") as f:
global_auth_param = GlobalAuthParams.from_auth_params_and_token(available_device.get_auth_params(), urls = f.readlines()
self.anonymous_access_token) for url in urls:
m3u8_urls = set() task = self.loop.create_task(self.do_download(raw_url=url, codec=codec, force_download=force_download))
tasks = set() self.tasks.append(task)
task.add_done_callback(self.tasks.remove)
async def upload(song_id: str, m3u8_url: str):
song_info = await get_song_info(song_id, self.anonymous_access_token,
self.config.region.defaultStorefront, self.config.region.language)
await upload_m3u8_to_api(self.config.m3u8Api.endpoint, m3u8_url, song_info)
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)
rip_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(rip_task)
rip_task.add_done_callback(tasks.remove)
if self.config.m3u8Api.enable:
upload_task = self.loop.create_task(upload(song_id, m3u8_url))
tasks.update(upload_task)
upload_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): async def _get_available_device(self, storefront: str):
devices = self.storefront_device_mapping.get(storefront) devices = self.storefront_device_mapping.get(storefront)

@ -28,3 +28,6 @@ class CodecNotFoundException(Exception):
class RetryableDecryptException(Exception): class RetryableDecryptException(Exception):
... ...
class FailedGetM3U8FromDeviceException(Exception):
...

@ -1,33 +0,0 @@
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()
Loading…
Cancel
Save