fix: format code

pull/1/head
WorldObservationLog 5 months ago
parent efe1f4efd5
commit 3b00e3175a
  1. 37
      README.md
  2. 1
      main.py
  3. 9
      src/adb.py
  4. 21
      src/api.py
  5. 11
      src/cmd.py
  6. 4
      src/decrypt.py
  7. 2
      src/models/__init__.py
  8. 12
      src/mp4.py
  9. 5
      src/rip.py
  10. 9
      src/utils.py

@ -1,10 +1,13 @@
# AppleMusicDecrypt
Apple Music decryption tool, based on [zhaarey/apple-music-alac-atmos-downloader](https://github.com/zhaarey/apple-music-alac-atmos-downloader)
Apple Music decryption tool, based
on [zhaarey/apple-music-alac-atmos-downloader](https://github.com/zhaarey/apple-music-alac-atmos-downloader)
**WARNING: This project is currently in an extremely early stage, and there are still a large number of undiscovered bugs and unfinished features. USE IT WITH CAUTION.**
**WARNING: This project is currently in an extremely early stage, and there are still a large number of undiscovered
bugs and unfinished features. USE IT WITH CAUTION.**
# Usage
```shell
# Download song/album with default codec (alac)
download https://music.apple.com/jp/album/nameless-name-single/1688539265
@ -22,22 +25,33 @@ download https://music.apple.com/jp/song/caribbean-blue/339592231 -c aac
- `aac-downmix (audio-stereo-downmix)`
# Support Link
- Apple Music Song Share Link (https://music.apple.com/jp/album/%E5%90%8D%E3%82%82%E3%81%AA%E3%81%8D%E4%BD%95%E3%82%82%E3%81%8B%E3%82%82/1688539265?i=1688539274)
- Apple Music Song Share
Link (https://music.apple.com/jp/album/%E5%90%8D%E3%82%82%E3%81%AA%E3%81%8D%E4%BD%95%E3%82%82%E3%81%8B%E3%82%82/1688539265?i=1688539274)
- Apple Music Album Share Link (https://music.apple.com/jp/album/nameless-name-single/1688539265)
- Apple Music Song Link (https://music.apple.com/jp/song/caribbean-blue/339592231)
# Deploy
## Prepare Local Environment
1. Install [GPAC](https://gpac.io/downloads/gpac-nightly-builds/)
2. Download [Bento4 MP4Tools](https://www.bento4.com/downloads/) and add the executable files to the environment variables
2. Download [Bento4 MP4Tools](https://www.bento4.com/downloads/) and add the executable files to the environment
variables
3. Run `gpac -version`, `mp4box -version`, `mp4extract`, `mp4edit` and make sure all the commands run fine
## Prepare Android Environment
### For WSA (Recommend):
1. Install Apple Music (3.6.0-beta) and login
2. Play a song in Apple Music
3. Install WSA from [LSPosed/MagiskOnWSALocal](https://github.com/LSPosed/MagiskOnWSALocal). Choose the version that includes Magisk but not GApps
4. Install following Magisk modules: [magisk-frida](https://github.com/ViRb3/magisk-frida), [sqlite3-magisk-module](https://github.com/rojenzaman/sqlite3-magisk-module)
3. Install WSA from [LSPosed/MagiskOnWSALocal](https://github.com/LSPosed/MagiskOnWSALocal). Choose the version that
includes Magisk but not GApps
4. Install following Magisk
modules: [magisk-frida](https://github.com/ViRb3/magisk-frida), [sqlite3-magisk-module](https://github.com/rojenzaman/sqlite3-magisk-module)
5. Edit `config.toml`
```toml
[[devices]]
host = "127.0.0.1"
@ -46,11 +60,14 @@ agentPort = 10020
fridaPath = "/system/bin/frida-server"
suMethod = "su -c"
```
### For Google Android Emulator
1. Install Apple Music (3.6.0-beta) and login
2. Play a song in Apple Music
3. Manually install Frida and start frida-server in background
4. Edit `config.toml`
```toml
[[devices]]
host = "127.0.0.1"
@ -59,10 +76,16 @@ agentPort = 10020
fridaPath = "/data/local/tmp/frida-server-16.2.1-android-x86_64" # Replace this value to your frida-server path!
suMethod = "su 0"
```
## Run Script
### Use pre-built script (For Windows)
Download latest build from [Actions](https://github.com/WorldObservationLog/AppleMusicDecrypt/actions) (need login your GitHub account). Unzip it, and run `main.exe`
Download latest build from [Actions](https://github.com/WorldObservationLog/AppleMusicDecrypt/actions) (need login your
GitHub account). Unzip it, and run `main.exe`
### Manually Run
```shell
git clone https://github.com/WorldObservationLog/AppleMusicDecrypt.git
cd AppleMusicDecrypt

@ -2,7 +2,6 @@ import asyncio
from src.cmd import NewInteractiveShell
if __name__ == '__main__':
loop = asyncio.get_event_loop()
cmd = NewInteractiveShell(loop)

@ -100,7 +100,8 @@ class Device:
def _get_dsid(self) -> str:
logger.debug("getting dsid")
dsid = self._execute_command(
"sqlite3 /data/data/com.apple.android.music/files/mpl_db/cookies.sqlitedb \"select value from cookies where name='X-Dsid';\"", True)
"sqlite3 /data/data/com.apple.android.music/files/mpl_db/cookies.sqlitedb \"select value from cookies where name='X-Dsid';\"",
True)
if not dsid:
raise FailedGetAuthParamException
return dsid.strip()
@ -108,7 +109,8 @@ class Device:
def _get_account_token(self, dsid: str) -> str:
logger.debug("getting account token")
account_token = self._execute_command(
f"sqlite3 /data/data/com.apple.android.music/files/mpl_db/cookies.sqlitedb \"select value from cookies where name='mz_at_ssl-{dsid}';\"", True)
f"sqlite3 /data/data/com.apple.android.music/files/mpl_db/cookies.sqlitedb \"select value from cookies where name='mz_at_ssl-{dsid}';\"",
True)
if not account_token:
raise FailedGetAuthParamException
return account_token.strip()
@ -124,7 +126,8 @@ class Device:
def _get_storefront(self) -> str | None:
logger.debug("getting storefront")
storefront_id = self._execute_command(
"sqlite3 /data/data/com.apple.android.music/files/mpl_db/accounts.sqlitedb \"select storeFront from account;\"", True)
"sqlite3 /data/data/com.apple.android.music/files/mpl_db/accounts.sqlitedb \"select storeFront from account;\"",
True)
if not storefront_id:
raise FailedGetAuthParamException
with open("assets/storefront_ids.json") as f:

@ -5,9 +5,8 @@ from ssl import SSLError
import httpcore
import httpx
import regex
from tenacity import retry, retry_if_exception_type, stop_after_attempt, before_sleep_log
from loguru import logger
from tenacity import retry, retry_if_exception_type, stop_after_attempt, before_sleep_log
from src.models import *
@ -18,7 +17,8 @@ user_agent_itunes = "iTunes/12.11.3 (Windows; Microsoft Windows 10 x64 Professio
user_agent_app = "Music/5.7 Android/10 model/Pixel6GR1YH build/1234 (dt:66)"
@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)), stop=stop_after_attempt(5),
@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)),
stop=stop_after_attempt(5),
before_sleep=before_sleep_log(logger, logging.WARN))
async def get_token():
req = await client.get("https://beta.music.apple.com")
@ -28,14 +28,16 @@ async def get_token():
return token
@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)), stop=stop_after_attempt(5),
@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)),
stop=stop_after_attempt(5),
before_sleep=before_sleep_log(logger, logging.WARN))
async def download_song(url: str) -> bytes:
async with lock:
return (await client.get(url)).content
@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)), stop=stop_after_attempt(5),
@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)),
stop=stop_after_attempt(5),
before_sleep=before_sleep_log(logger, logging.WARN))
async def get_meta(album_id: str, token: str, storefront: str):
if "pl." in album_id:
@ -69,7 +71,8 @@ async def get_meta(album_id: str, token: str, storefront: str):
return result
@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)), stop=stop_after_attempt(5),
@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)),
stop=stop_after_attempt(5),
before_sleep=before_sleep_log(logger, logging.WARN))
async def get_cover(url: str, cover_format: str):
formatted_url = regex.sub('bb.jpg', f'bb.{cover_format}', url)
@ -78,7 +81,8 @@ async def get_cover(url: str, cover_format: str):
return req.content
@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)), stop=stop_after_attempt(5),
@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)),
stop=stop_after_attempt(5),
before_sleep=before_sleep_log(logger, logging.WARN))
async def get_info_from_adam(adam_id: str, token: str, storefront: str):
req = await client.get(f"https://amp-api.music.apple.com/v1/catalog/{storefront}/songs/{adam_id}",
@ -92,7 +96,8 @@ async def get_info_from_adam(adam_id: str, token: str, storefront: str):
return None
@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)), stop=stop_after_attempt(5),
@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)),
stop=stop_after_attempt(5),
before_sleep=before_sleep_log(logger, logging.WARN))
async def get_song_lyrics(song_id: str, storefront: str, token: str, dsid: str, account_token: str) -> str:
req = await client.get(f"https://amp-api.music.apple.com/v1/catalog/{storefront}/songs/{song_id}/lyrics",

@ -35,7 +35,8 @@ class NewInteractiveShell:
download_parser = subparser.add_parser("download")
download_parser.add_argument("url", type=str)
download_parser.add_argument("-c", "--codec",
choices=["alac", "ec3", "aac", "aac-binaural", "aac-downmix", "ac3"], default="alac")
choices=["alac", "ec3", "aac", "aac-binaural", "aac-downmix", "ac3"],
default="alac")
download_parser.add_argument("-f", "--force", type=bool, default=False)
subparser.add_parser("exit")
@ -79,12 +80,14 @@ class NewInteractiveShell:
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(), self.anonymous_access_token)
global_auth_param = GlobalAuthParams.from_auth_params_and_token(available_device.get_auth_params(),
self.anonymous_access_token)
match url.type:
case URLType.Song:
self.loop.create_task(rip_song(url, global_auth_param, codec, self.config, available_device, force_download))
task = self.loop.create_task(
rip_song(url, global_auth_param, codec, self.config, available_device, force_download))
case URLType.Album:
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 handle_command(self):
session = PromptSession("> ")

@ -1,10 +1,6 @@
import asyncio
import logging
import sys
from prompt_toolkit.shortcuts import ProgressBar
from loguru import logger
from tenacity import retry, retry_if_exception_type, stop_after_attempt, before_sleep_log
from src.adb import Device
from src.exceptions import DecryptException

@ -1,5 +1,5 @@
from src.models.album_meta import AlbumMeta
from src.models.playlist_meta import PlaylistMeta
from src.models.tracks_meta import TracksMeta
from src.models.song_data import SongData
from src.models.song_lyrics import SongLyrics
from src.models.tracks_meta import TracksMeta

@ -12,7 +12,14 @@ from bs4 import BeautifulSoup
from src.exceptions import CodecNotFoundException
from src.metadata import SongMetadata
from src.types import *
from src.utils import find_best_codec
from src.utils import find_best_codec, get_codec_from_codec_id
async def get_available_codecs(m3u8_url: str) -> Tuple[list[str], list[str]]:
parsed_m3u8 = m3u8.load(m3u8_url)
codec_ids = [playlist.stream_info.audio for playlist in parsed_m3u8.playlists]
codecs = [get_codec_from_codec_id(codec_id) for codec_id in codec_ids]
return codecs, codec_ids
async def extract_media(m3u8_url: str, codec: str) -> Tuple[str, list[str], str]:
@ -161,7 +168,8 @@ def write_metadata(song: bytes, metadata: SongMetadata, embed_metadata: list[str
with open(cover_path.absolute(), "wb") as f:
f.write(metadata.cover)
subprocess.run(["mp4box", "-time", "0", "-mtime", "0", "-keep-utc", "-name", f"1={metadata.title}", "-itags",
":".join(["tool=\"\"", f"cover={absolute_cover_path}", metadata.to_itags_params(embed_metadata, cover_format)]),
":".join(["tool=\"\"", f"cover={absolute_cover_path}",
metadata.to_itags_params(embed_metadata, cover_format)]),
song_name.absolute()], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
with open(song_name.absolute(), "rb") as f:
embed_song = f.read()

@ -10,10 +10,8 @@ from src.mp4 import extract_media, extract_song, encapsulate, write_metadata
from src.save import save
from src.types import GlobalAuthParams, Codec
from src.url import Song, Album, URLType
from src.utils import check_song_exists
@logger.catch
async def rip_song(song: Song, auth_params: GlobalAuthParams, codec: str, config: Config, device: Device,
force_save: bool = False):
logger.debug(f"Task of song id {song.id} was created")
@ -52,7 +50,8 @@ async def rip_album(album: Album, auth_params: GlobalAuthParams, codec: str, con
for track in album_info.data[0].relationships.tracks.data:
song = Song(id=track.id, storefront=album.storefront, url="", type=URLType.Song)
tg.create_task(rip_song(song, auth_params, codec, config, device, force_save))
logger.info(f"Album: {album_info.data[0].attributes.artistName} - {album_info.data[0].attributes.name} finished ripping")
logger.info(
f"Album: {album_info.data[0].attributes.artistName} - {album_info.data[0].attributes.name} finished ripping")
async def rip_playlist():

@ -9,7 +9,6 @@ from bs4 import BeautifulSoup
from src.config import Download
from src.exceptions import NotTimeSyncedLyricsException
from src.types import *
@ -117,3 +116,11 @@ def check_song_exists(metadata, config: Download, codec: str):
def get_valid_filename(filename: str):
return "".join(i for i in filename if i not in r"\/:*?<>|")
def get_codec_from_codec_id(codec_id: str) -> str:
codecs = [Codec.AC3, Codec.EC3, Codec.AAC, Codec.ALAC, Codec.AAC_BINAURAL, Codec.AAC_DOWNMIX]
for codec in codecs:
if regex.match(CodecRegex.get_pattern_by_codec(codec), codec_id):
return codec
return ""

Loading…
Cancel
Save