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 # 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 # Usage
```shell ```shell
# Download song/album with default codec (alac) # Download song/album with default codec (alac)
download https://music.apple.com/jp/album/nameless-name-single/1688539265 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)` - `aac-downmix (audio-stereo-downmix)`
# Support Link # 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 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) - Apple Music Song Link (https://music.apple.com/jp/song/caribbean-blue/339592231)
# Deploy # Deploy
## Prepare Local Environment ## Prepare Local Environment
1. Install [GPAC](https://gpac.io/downloads/gpac-nightly-builds/) 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 3. Run `gpac -version`, `mp4box -version`, `mp4extract`, `mp4edit` and make sure all the commands run fine
## Prepare Android Environment ## Prepare Android Environment
### For WSA (Recommend): ### For WSA (Recommend):
1. Install Apple Music (3.6.0-beta) and login 1. Install Apple Music (3.6.0-beta) and login
2. Play a song in Apple Music 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 3. Install WSA from [LSPosed/MagiskOnWSALocal](https://github.com/LSPosed/MagiskOnWSALocal). Choose the version that
4. Install following Magisk modules: [magisk-frida](https://github.com/ViRb3/magisk-frida), [sqlite3-magisk-module](https://github.com/rojenzaman/sqlite3-magisk-module) 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` 5. Edit `config.toml`
```toml ```toml
[[devices]] [[devices]]
host = "127.0.0.1" host = "127.0.0.1"
@ -46,11 +60,14 @@ agentPort = 10020
fridaPath = "/system/bin/frida-server" fridaPath = "/system/bin/frida-server"
suMethod = "su -c" suMethod = "su -c"
``` ```
### For Google Android Emulator ### For Google Android Emulator
1. Install Apple Music (3.6.0-beta) and login 1. Install Apple Music (3.6.0-beta) and login
2. Play a song in Apple Music 2. Play a song in Apple Music
3. Manually install Frida and start frida-server in background 3. Manually install Frida and start frida-server in background
4. Edit `config.toml` 4. Edit `config.toml`
```toml ```toml
[[devices]] [[devices]]
host = "127.0.0.1" 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! fridaPath = "/data/local/tmp/frida-server-16.2.1-android-x86_64" # Replace this value to your frida-server path!
suMethod = "su 0" suMethod = "su 0"
``` ```
## Run Script ## Run Script
### Use pre-built script (For Windows) ### 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 ### Manually Run
```shell ```shell
git clone https://github.com/WorldObservationLog/AppleMusicDecrypt.git git clone https://github.com/WorldObservationLog/AppleMusicDecrypt.git
cd AppleMusicDecrypt cd AppleMusicDecrypt

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

@ -100,7 +100,8 @@ class Device:
def _get_dsid(self) -> str: def _get_dsid(self) -> str:
logger.debug("getting dsid") logger.debug("getting dsid")
dsid = self._execute_command( 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: if not dsid:
raise FailedGetAuthParamException raise FailedGetAuthParamException
return dsid.strip() return dsid.strip()
@ -108,7 +109,8 @@ class Device:
def _get_account_token(self, dsid: str) -> str: def _get_account_token(self, dsid: str) -> str:
logger.debug("getting account token") logger.debug("getting account token")
account_token = self._execute_command( 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: if not account_token:
raise FailedGetAuthParamException raise FailedGetAuthParamException
return account_token.strip() return account_token.strip()
@ -124,7 +126,8 @@ class Device:
def _get_storefront(self) -> str | None: def _get_storefront(self) -> str | None:
logger.debug("getting storefront") logger.debug("getting storefront")
storefront_id = self._execute_command( 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: if not storefront_id:
raise FailedGetAuthParamException raise FailedGetAuthParamException
with open("assets/storefront_ids.json") as f: with open("assets/storefront_ids.json") as f:

@ -5,9 +5,8 @@ from ssl import SSLError
import httpcore import httpcore
import httpx import httpx
import regex import regex
from tenacity import retry, retry_if_exception_type, stop_after_attempt, before_sleep_log
from loguru import logger from loguru import logger
from tenacity import retry, retry_if_exception_type, stop_after_attempt, before_sleep_log
from src.models import * 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)" 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)) before_sleep=before_sleep_log(logger, logging.WARN))
async def get_token(): async def get_token():
req = await client.get("https://beta.music.apple.com") req = await client.get("https://beta.music.apple.com")
@ -28,14 +28,16 @@ async def get_token():
return 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)) before_sleep=before_sleep_log(logger, logging.WARN))
async def download_song(url: str) -> bytes: async def download_song(url: str) -> bytes:
async with lock: async with lock:
return (await client.get(url)).content 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)) before_sleep=before_sleep_log(logger, logging.WARN))
async def get_meta(album_id: str, token: str, storefront: str): async def get_meta(album_id: str, token: str, storefront: str):
if "pl." in album_id: if "pl." in album_id:
@ -69,7 +71,8 @@ async def get_meta(album_id: str, token: str, storefront: str):
return result 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)) before_sleep=before_sleep_log(logger, logging.WARN))
async def get_cover(url: str, cover_format: str): async def get_cover(url: str, cover_format: str):
formatted_url = regex.sub('bb.jpg', f'bb.{cover_format}', url) 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 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)) before_sleep=before_sleep_log(logger, logging.WARN))
async def get_info_from_adam(adam_id: str, token: str, storefront: str): 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}", 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 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)) 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: 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", 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 = subparser.add_parser("download")
download_parser.add_argument("url", type=str) download_parser.add_argument("url", type=str)
download_parser.add_argument("-c", "--codec", 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) download_parser.add_argument("-f", "--force", type=bool, default=False)
subparser.add_parser("exit") subparser.add_parser("exit")
@ -79,12 +80,14 @@ class NewInteractiveShell:
available_device: Device = random.choice(devices) available_device: Device = random.choice(devices)
else: else:
available_device: Device = random.choice(available_devices) 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: match url.type:
case URLType.Song: 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: 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): async def handle_command(self):
session = PromptSession("> ") session = PromptSession("> ")

@ -1,10 +1,6 @@
import asyncio import asyncio
import logging
import sys
from prompt_toolkit.shortcuts import ProgressBar
from loguru import logger 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.adb import Device
from src.exceptions import DecryptException from src.exceptions import DecryptException

@ -1,5 +1,5 @@
from src.models.album_meta import AlbumMeta from src.models.album_meta import AlbumMeta
from src.models.playlist_meta import PlaylistMeta 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_data import SongData
from src.models.song_lyrics import SongLyrics 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.exceptions import CodecNotFoundException
from src.metadata import SongMetadata from src.metadata import SongMetadata
from src.types import * 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]: 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: with open(cover_path.absolute(), "wb") as f:
f.write(metadata.cover) f.write(metadata.cover)
subprocess.run(["mp4box", "-time", "0", "-mtime", "0", "-keep-utc", "-name", f"1={metadata.title}", "-itags", 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) song_name.absolute()], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
with open(song_name.absolute(), "rb") as f: with open(song_name.absolute(), "rb") as f:
embed_song = f.read() 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.save import save
from src.types import GlobalAuthParams, Codec from src.types import GlobalAuthParams, Codec
from src.url import Song, Album, URLType 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, async def rip_song(song: Song, auth_params: GlobalAuthParams, codec: str, config: Config, device: Device,
force_save: bool = False): force_save: bool = False):
logger.debug(f"Task of song id {song.id} was created") 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: for track in album_info.data[0].relationships.tracks.data:
song = Song(id=track.id, storefront=album.storefront, url="", type=URLType.Song) 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)) 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(): async def rip_playlist():

@ -9,7 +9,6 @@ from bs4 import BeautifulSoup
from src.config import Download from src.config import Download
from src.exceptions import NotTimeSyncedLyricsException from src.exceptions import NotTimeSyncedLyricsException
from src.types import * from src.types import *
@ -117,3 +116,11 @@ def check_song_exists(metadata, config: Download, codec: str):
def get_valid_filename(filename: str): def get_valid_filename(filename: str):
return "".join(i for i in filename if i not in r"\/:*?<>|") 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