mycode.py:

import json
import requests
from acrcloud.recognizer import ACRCloudRecognizer
from musixmatch_api import Musixmatch, CaptchaError, UserTokenError
from mutagen.mp3 import MP3
from mutagen.id3 import ID3, APIC, error


# ACRCloud API credentials
ACR_HOST = “”
ACR_ACCESS_KEY = “”
ACR_ACCESS_SECRET = “”


# ACR Cloud configuration (update with your credentials)
config = {
‘host’: ACR_HOST,
‘access_key’: ACR_ACCESS_KEY,
‘access_secret’: ACR_ACCESS_SECRET,
‘timeout’: 10 # seconds
}

recognizer = ACRCloudRecognizer(config)

# Initialize Musixmatch API (exception handling to be added as per your implementation)
musixmatch = Musixmatch(Exception)

# Function to recognize a song using ACRCloud
def recognize_song(audio_file_path):
buffer = open(audio_file_path, ‘rb’).read()
result = recognizer.recognize_by_filebuffer(buffer, 0)
try:
result_dict = json.loads(result)
return result_dict[‘metadata’][‘music’][0]
except (KeyError, IndexError, json.JSONDecodeError) as e:
print(f"Error while parsing result: {e}“)
return None

def format_time(ts):
‘’‘Converts time in seconds to the format [mm:ss.xx]’‘’
minutes = int(ts // 60)
seconds = int(ts % 60)
hundredths = int((ts - int(ts)) * 100)
return f’[{minutes:02d}:{seconds:02d}.{hundredths:02d}]‘

def process_rich_sync_lyrics(rich_sync_lyrics_json):
‘’‘Converts Musixmatch rich sync data to LRC format’’'
lrc_lines = []
try:
# Load the JSON string into a Python object
rich_sync_data = json.loads(rich_sync_lyrics_json)
except json.JSONDecodeError as e:
print(f"Error decoding JSON: {e}”)
return None

# Iterate through each line and create formatted LRC lines
for line in rich_sync_data:
ts = format_time(line[‘ts’]) # Start time of the line
lrc_line = f’{ts}{line[“x”]}’ # Use “x” for the entire line of lyrics
lrc_lines.append(lrc_line)

# Join the formatted lines with line breaks
return ‘\n’.join(lrc_lines)


# Function to get lyrics from Musixmatch given artist name and song title
def get_lyrics_from_musicxmatch(artist_name, song_title):
try:
user_token = musixmatch.get_user_token()

track_data = musixmatch.get_search_by_track(song_title, artist_name, “”)
if track_data:
track_id = track_data[‘track_id’]
rich_sync_data = musixmatch.get_rich_sync_by_id(track_id)

# Print the JSON response for debugging
print(json.dumps(track_data, indent=2))
print(json.dumps(rich_sync_data, indent=2))

if rich_sync_data and ‘richsync_body’ in rich_sync_data:
rich_sync_lyrics_json = rich_sync_data[‘richsync_body’]
lrc_lyrics = process_rich_sync_lyrics(rich_sync_lyrics_json)
return lrc_lyrics
else:
print(“No synced lyrics found.”)
return None
else:
print(“Track not found in Musixmatch.”)
return None
except (CaptchaError, UserTokenError) as e:
print(f"Error while working with Musixmatch: {e}“)
return None

def download_album_cover(album_cover_url, save_path, size=(1400, 1400)):
# Download the album cover image from the given URL
response = requests.get(album_cover_url, stream=True)
if response.status_code == 200:
with open(save_path, ‘wb’) as file:
for chunk in response:
file.write(chunk)
# Resize and save the image externally if size is specified
if size:
from PIL import Image
im = Image.open(save_path)
im = im.resize(size)
im.save(save_path)

def download_small_cover(album_cover_url, save_path, size=(350, 350)):
# If you want to save a smaller version separately
download_album_cover(album_cover_url, save_path, size)
# Note: You should check the appropriate usage rights before downloading images from the internet

def embed_album_art(audio_file_path, album_cover_path):
audio = MP3(audio_file_path, ID3=ID3)

# Add ID3 tag if it doesn’t exist
try:
audio.add_tags()
except error as e:
pass

with open(album_cover_path, ‘rb’) as album_art:
audio.tags.add(
APIC(
encoding=3, # 3 is for utf-8
mime=‘image/jpeg’, # image/jpeg or image/png
type=3, # 3 is for the cover image
desc=u’Cover’,
data=album_art.read()
)
)
audio.save(v2_version=3)
# Note: This function assumes you have already downloaded the album cover at ‘album_cover_path’

if name == “main”:
audio_file_path = ‘C:/Users/ILEG-i5-11/Downloads/Music/Unknown_file.mp3’ # Replace with actual path
lrc_file_path = ‘C:/Users/ILEG-i5-11/Downloads/Music/Unknown_file.lrc’ # Output LRC file path

# Recognize the song using ACRCloud
song_tags = recognize_song(audio_file_path)
if song_tags:
artist_name = song_tags[‘artists’][0][‘name’]
song_title = song_tags[‘title’]
print(f"Identified Song: {artist_name} - {song_title}”)

# Fetch track data using the recognized song’s title and artist name
track_data = musixmatch.get_search_by_track(song_title, artist_name)
if track_data:
track_id = track_data[‘track_id’]

# Fetch the album cover URL using the track ID
album_cover_url = musixmatch.get_album_cover_url_by_track_id(track_id)
if album_cover_url:
album_cover_save_path = ‘C:/Users/ILEG-i5-11/Downloads/Music/Album_Cover.jpg’
download_album_cover(album_cover_url, album_cover_save_path) # Download the high-resolution cover
print(f"Album cover saved to: {album_cover_save_path}“)

small_album_cover_save_path = ‘C:/Users/ILEG-i5-11/Downloads/Music/Small_Album_Cover.jpg’
download_small_cover(album_cover_url, small_album_cover_save_path) # For embedding
print(f"Small album cover saved to: {small_album_cover_save_path}”)

# Embed 350x350 album art into the MP3 file
embed_album_art(audio_file_path, small_album_cover_save_path)
print(“Album art embedded into the MP3 file.”)
else:
print(“Could not get album cover.”)

# Fetch the synced lyrics using the recognized song’s track ID
rich_sync_data = musixmatch.get_rich_sync_by_id(track_id)
if rich_sync_data and ‘richsync_body’ in rich_sync_data:
rich_sync_lyrics_json = rich_sync_data[‘richsync_body’]
lrc_lyrics = process_rich_sync_lyrics(rich_sync_lyrics_json)
if lrc_lyrics:
# Write the LRC lyrics to a file
with open(lrc_file_path, ‘w’, encoding=‘utf-8’) as lrc_file:
lrc_file.write(lrc_lyrics)
print(f"Saved LRC file to: {lrc_file_path}“)
else:
print(“No synced lyrics found.”)
else:
print(“Track not found in Musixmatch.”)
else:
print(“Could not identify the song.”)

modify above code and remove embeding album arts and remove lyrics from getting musixmatch.

get album covers, lyrics from apple music and save externally both of them and embed the album art to the unknown_file.mp3

interface.py:
import re
import base64
import pbkdf2
import hashlib

from Cryptodome.Hash import SHA256

from uuid import uuid4
from utils.utils import create_requests_session

from .fingerprint import Fingerprint


import srp.pysrp as srp
srp.rfc5054_enable()
srp.no_username_in_x()

def b64enc(data):
return base64.b64encode(data).decode()

def b64dec(data):
return base64.b64decode(data)


class AppleMusicApi(object):
def init(self, exception, storefront=‘US’, language=‘en-US’, lyrics_resource=‘lyrics’):
self.s = create_requests_session()
self.api_base = ‘https://amp-api.music.apple.com/v1/’

self.storefront = storefront
self.language = language
self.lyrics_storefront = storefront
self.lyrics_language = language
self.lyrics_resource = lyrics_resource

self.access_token = ‘’
self.user_token = ‘’

self.exception = exception

self.user_agent = ‘Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.66 Safari/537.36’

def headers(self):
return {
‘authorization’: ‘Bearer ’ + self.access_token,
‘Connection’: ‘Keep-Alive’,
‘Content-Type’: ‘application/json’,
‘Origin’: ‘https://music.apple.com’,
‘Referer’: ‘https://music.apple.com/’,
‘Accept-Encoding’: ‘gzip, deflate’,
‘Accept-Language’: f’{self.language},en;q=0.9’,
‘User-Agent’: self.user_agent,
‘Media-User-Token’: self.user_token,
‘x-apple-renewal’: ‘true’
}

def get_access_token(self):
s = create_requests_session()
r = s.get(‘https://music.apple.com/us/search’, headers=self.headers())
if r.status_code != 200: raise self.exception(r.text)

index_js = re.search(‘(?<=index-)(.?)(?=.js")‘, r.text).group(1)
r = s.get(f’https://music.apple.com/assets/index-{index_js}.js’, headers=self.headers())
if r.status_code != 200: raise self.exception(r.text)

self.access_token = re.search('(?=eyJh)(.?)(?=")’, r.text).group(1)
return self.access_token

def auth(self, email: str, password: str):
auth_url = ‘https://idmsa.apple.com/appleauth/’
client_id = ‘06f8d74b71c73757a2f82158d5e948ae7bae11ec45fda9a58690f55e35945c51’
frame_id = ‘auth-’ + str(uuid4()).lower()

# get “dslang”, “site” and “aasp” cookies
r = self.s.get(auth_url + ‘auth/authorize/signin’, headers=self.headers(), params={
‘frame_id’: frame_id,
‘language’: ‘en_us’,
‘skVersion’: ‘7’,
‘iframeId’: frame_id,
‘client_id’: client_id,
‘redirect_uri’: ‘https://music.apple.com’,
‘response_type’: ‘code’,
‘response_mode’: ‘web_message’,
‘account_ind’: ‘1’,
‘state’: frame_id,
‘authVersion’: ‘latest’
})
if r.status_code != 200: raise self.exception(r.text)
auth_attributes = r.headers[‘X-Apple-Auth-Attributes’]

# get “aa” cookie
r = self.s.post(auth_url + ‘jslog’, headers=self.headers(), json={
‘type’: ‘INFO’,
‘title’: ‘AppleAuthPerf-s-y’,
‘message’: ‘’‘APPLE ID : TTI {“data”:{“initApp”:{“startTime”:1154.2000000001863},“loadAuthComponent”:{“startTime”:1500.7000000001863},“startAppToTTI”:{“duration”:346.70000000018626}},“order”:[“initApp”,“loadAuthComponent”,“startAppToTTI”]}’‘’,
‘iframeId’: frame_id,
‘details’: ‘’‘{“pageVisibilityState”:“visible”}’‘’
})
assert (r.status_code == 200)

# actual login
headers = {
‘Accept’: ‘application/json’,
‘Referer’: ‘https://idmsa.apple.com/’,
‘Content-Type’: ‘application/json’,
‘X-Apple-Widget-Key’: client_id,
‘X-Apple-Frame-Id’: frame_id,
‘X-Apple-Domain-Id’: ‘3’,
‘X-Apple-Locale’: ‘en_us’,
‘X-Requested-With’: ‘XMLHttpRequest’,
‘Origin’: ‘https://idmsa.apple.com’,
‘X-Apple-I-Require-UE’: ‘true’,
‘X-Apple-I-FD-Client-Info’: ‘{’ + f’“U”:“{self.user_agent}”,“L”:“{self.language}”,“Z”:“GMT-8:00”,“V”:“1.1”,“F”:“{Fingerprint().create_fingerprint()}”’ + ‘}’,
‘X-Apple-Auth-Attributes’: auth_attributes,
‘User-Agent’: self.user_agent,
‘X-Apple-Mandate-Security-Upgrade’: ‘0’
}

json = {‘accountName’: email, ‘rememberMe’: ‘false’}
params_ = {‘isRememberMeEnabled’: ‘false’}

r = self.s.post(auth_url + ‘auth/federate’, headers=headers, params=params_, json=json_)
if ‘federated’ not in r.json(): raise self.exception(r.text)

# finally begin login
user = srp.User(email, bytes(), hash_alg=srp.SHA256, ng_type=srp.NG_2048)
, A = user.start_authentication()

json = {‘a’: b64enc(A), ‘accountName’: email, ‘protocols’: [‘s2k’, ‘s2k_fo’]}

r = self.s.post(auth_url + ‘auth/signin/init’, headers=headers, json=json_)
out_json = r.json()
if r.status_code != 200: raise self.exception(out_json[‘serviceErrors’][0][‘message’])
if ‘b’ not in out_json: raise self.exception(r.text)
if out_json.get(‘protocol’) != ‘s2k’: raise self.exception(‘Protocol not supported’)

salt = b64dec(out_json[‘salt’])
iterations = out_json[‘iteration’]
B = b64dec(out_json[‘b’])
c = out_json[‘c’]

pass_hash = hashlib.sha256(password.encode(“utf-8”)).digest()
enc_pass = pbkdf2.PBKDF2(pass_hash, salt, iterations, SHA256).read(32)

user.p = enc_pass
M1 = user.process_challenge(salt, B)
if M1 is None: raise self.exception(“Failed to process challenge”)

M2 = user.K

# real version uses m2 as well… hmmm
json_ = {‘accountName’: email, ‘c’: c, ‘m1’: b64enc(M1), ‘m2’: b64enc(M2), ‘rememberMe’: ‘false’}

r = self.s.post(auth_url + ‘auth/signin/complete’, headers=headers, params=params_, json=json_)
if r.status_code != 200: raise self.exception(r.json()[‘serviceErrors’][0][‘message’])

# exchange the “myacinfo” cookie with the “media-user-token”
r = self.s.post(‘https://buy.music.apple.com/account/web/auth’, headers=self.headers(), json={‘webAuthorizationFlowContext’: ‘music’})
if r.status_code != 200: raise self.exception(r.text)

self.user_token = self.s.cookies[‘media-user-token’]
return self.user_token

def get_account_details(self, force_region, selected_language, lyrics_language):
r = self.s.get(self.api_base + ‘me/account’, headers=self.headers(), params={‘meta’: ‘subscription’})
if r.status_code != 200: raise self.exception(r.text)

self.lyrics_storefront = r.json()[‘meta’][‘subscription’][‘storefront’]
if force_region.lower() == self.lyrics_storefront: force_region = None
if force_region: print(f"Apple Music: WARNING: Selected region {force_region} is not the same as your Apple Music region {self.lyrics_storefront}, lyrics will use the region {self.lyrics_storefront}. Only lyrics available in both regions will be used, maybe use a copy of the module with the folder name (which determines the name of the module) and the netlocation_constant changed for lyrics only if you want credits or playlists from other regions.”)

self.storefront = force_region.lower() if force_region else self.lyrics_storefront
account_active = r.json()[‘meta’][‘subscription’][‘active’]

storefront_endpoint = f’storefronts/{force_region.lower()}’ if force_region else ‘me/storefront’
endpoint_data = self.s.get(self.api_base + storefront_endpoint, headers=self.headers())
if endpoint_data.status_code != 200: raise self.exception(f’Region {force_region} is not supported’)

supported_languages = endpoint_data.json()[‘data’][0][‘attributes’][‘supportedLanguageTags’]
if selected_language:
for i in supported_languages:
if selected_language in i:
self.language = i
break
else:
print(f"Apple Music: WARNING: Selected language {selected_language} in region {force_region if force_region else self.lyrics_storefront} is unsupported, force a different region or use one of these: {‘, ‘.join(supported_languages)}")
self.language = supported_languages[0]
else:
self.language = supported_languages[0]

if not lyrics_language: lyrics_language = selected_language

if force_region:
supported_languages = self.s.get(f’{self.api_base}me/storefront’, headers=self.headers()).json()[‘data’][0][‘attributes’][‘supportedLanguageTags’]
if lyrics_language:
for i in supported_languages:
if selected_language in i:
self.lyrics_language = i
break
else:
print(f"Apple Music: WARNING: Selected language {selected_language} in lyrics region {self.lyrics_storefront} is unsupported, force a different region or use one of these: {‘, ‘.join(supported_languages)}")
self.lyrics_language = supported_languages[0]
else:
self.lyrics_language = supported_languages[0]

return self.storefront, account_active, self.language, self.lyrics_language, self.lyrics_storefront

def check_active_subscription(self):
url = f’{self.api_base}me/account’
params = {‘meta’: ‘subscription’, ‘challenge[subscriptionCapabilities]’: ‘voice,premium’}

response = self.s.get(url, headers=self.headers(), params=params)
if response.status_code != 200: raise self.exception(response.text)
response_data = response.json()

if ‘meta’ in response_data and ‘subscription’ in response_data[‘meta’]:
return response_data[‘meta’][‘subscription’].get(‘active’, False)

return False

def get(self, url: str, params=None, storefront=None, language=None):
if not params: params = {}
if not storefront: storefront = self.storefront
params[‘l’] = language if language else self.language

r = self.s.get(f’{self.api_base}catalog/{storefront}/{url}‘, params=params, headers=self.headers())
if r.status_code not in [200, 201, 202]: raise self.exception(r.text)

return r.json()

def search(self, query_type: str, query: str, limit: int = 10):
if limit > 25: limit = 25

params = {
‘term’: query,
‘types’: query_type,
‘limit’: limit
}

if query_type == ‘songs’:
params[‘extend[songs]’] = ‘attribution,composerName,contentRating,discNumber,durationInMillis,isrc,movementCount,movementName,movementNumber,releaseDate,trackNumber,workNamedata’
params[‘include[songs]’] = ‘artists,albums’ + (f’,{self.lyrics_resource}’ if self.storefront == self.lyrics_storefront else ‘’) # doesn’t give lyrics?
params[‘extend[albums]’] = ‘copyright,upc’
elif query_type == ‘playlists’:
params[‘include[playlists]’] = ‘curator’
params[‘extend[playlists]’] = ‘artwork,description,trackTypes,trackCount’

results = self.get(‘search’, params)[‘results’]
if query_type in results:
results = results[query_type][‘data’]
else:
results = []

return results

def get_playlist_base_data(self, playlist_id):
return self.get(f’playlists/{playlist_id}‘, params={
‘include’: ‘curator,tracks’,
‘extend’: ‘artwork,description,trackTypes,trackCount’,
‘include[songs]’: ‘artists,albums’ + (f’,{self.lyrics_resource}’ if self.storefront == self.lyrics_storefront else ‘’),
‘extend[songs]’: ‘extendedAssetUrls,attribution,composerName,contentRating,discNumber,durationInMillis,isrc,movementCount,movementName,movementNumber,releaseDate,trackNumber,workNamedata’,
‘extend[albums]’: ‘copyright,upc’
})[‘data’][0]

def get_playlist_tracks(self, playlist_data):
tracks_list, track_data = [], {}
tracks = list(playlist_data[‘relationships’][‘tracks’][‘data’])
offset = len(tracks)

while len(tracks) + offset <= playlist_data[‘attributes’][‘trackCount’]:
tracks += self.get(f’playlists/{playlist_data[“id”]}/tracks’, params={
‘offset’: offset,
‘include[songs]’: ‘artists,albums’ + (f’,{self.lyrics_resource}’ if self.storefront == self.lyrics_storefront else ‘’),
‘extend[songs]’: ‘extendedAssetUrls,attribution,composerName,contentRating,discNumber,durationInMillis,isrc,movementCount,movementName,movementNumber,releaseDate,trackNumber,workNamedata’,
‘extend[albums]’: ‘copyright,upc’,
‘limit’: 100
})[‘data’]
offset += 100

for track in tracks:
tracks_list.append(track[‘id’])
track_data[track[‘id’]] = track

return tracks_list, track_data

def get_tracks_by_ids(self, track_ids: list = None, isrc: str = None):
if not track_ids: track_ids = []

params = {‘filter[isrc]’: isrc} if isrc else {‘ids’: ‘,’.join(track_ids)}
params[‘include’] = ‘artists,albums’ + (f’,{self.lyrics_resource}’ if self.storefront == self.lyrics_storefront else ‘’)
params[‘extend’] = ‘attribution,composerName,contentRating,discNumber,durationInMillis,isrc,movementCount,movementName,movementNumber,releaseDate,trackNumber,workNamedata’
params[‘extend[albums]’] = ‘copyright,upc’

return self.get(‘songs’, params)[‘data’]

def get_track(self, track_id: str = None):
return self.get_tracks_by_ids([track_id])[0]

@staticmethod
def get_lyrics_support(track_attributes):
# could technically be a single line in the lambda
if track_attributes.get(‘hasTimeSyncedLyrics’):
return 1 if track_attributes.get(‘isVocalAttenuationAllowed’) else 2
else:
return 3 if track_attributes.get(‘hasLyrics’) else 4

def get_track_by_isrc(self, isrc: str, album_name: str):
results = self.get_tracks_by_ids(isrc=isrc)

correct_region_results = [i for i in results if i[‘attributes’][‘url’].split(‘i=’)[-1].split(‘&’)[0] == i[‘id’]]
incorrect_region_results = [i for i in results if i[‘attributes’][‘url’].split(‘i=’)[-1].split(‘&’)[0] != i[‘id’]]

correct_region_results_sorted_by_track_number = sorted(correct_region_results, key=lambda x: x[‘attributes’].get(‘trackNumber’, 1))

fix_results_by_album = lambda list_to_sort: sorted(list_to_sort, key=lambda x: (x[‘attributes’][‘albumName’] != album_name))
correct_album_correct_region_results = fix_results_by_album(correct_region_results_sorted_by_track_number)
correct_album_incorrect_region_results = fix_results_by_album(incorrect_region_results)

correct_album_prioritised_lyrics_results = sorted(correct_album_correct_region_results, key=lambda x: self.get_lyrics_support(x[‘attributes’]))
return correct_album_prioritised_lyrics_results + correct_album_incorrect_region_results

def get_lyrics(self, track_id, lyrics_resource=None):
if not lyrics_resource: lyrics_resource = self.lyrics_resource

try:
data = self.get(f’songs/{track_id}/{lyrics_resource}‘, storefront=self.lyrics_storefront, language=self.language)
except self.exception:
return None

return data#[‘data’][0][‘attributes’][‘ttml’]


fingerprint.py:
# This is likely not necessary at all, but I (OrfiDev) decided to reverse engineer and
# reimplement the fingerprinting algorithm used by Apple’s web login as used by Apple Music anyways.
#
# I’m not sure if this is reversible (as in even checkable if it’s correct)
# maybe the part which I assumed to be a checksum is actually a way to derive some variable required to decode?

import pytz
import random
import datetime
import urllib.parse

timezone = pytz.timezone(‘America/Los_Angeles’)

class Fingerprint:
def encode(cls, e):
y = [“%20”, “;;;”, “%3B”, “%2C”, “und”, “fin”, “ed;”, “%28”, “%29”, “%3A”, “/53”, “ike”, “Web”, “0;”, “.0”, “e;”, “on”, “il”, “ck”, “01”, “in”, “Mo”, “fa”, “00”, “32”, “la”, “.1”, “ri”, “it”, “%u”, “le”]
A = “.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz”
w = {
1: [4, 15],
110: [8, 239],
74: [8, 238],
57: [7, 118],
56: [7, 117],
71: [8, 233],
25: [8, 232],
101: [5, 28],
104: [7, 111],
4: [7, 110],
105: [6, 54],
5: [7, 107],
109: [7, 106],
103: [9, 423],
82: [9, 422],
26: [8, 210],
6: [7, 104],
46: [6, 51],
97: [6, 50],
111: [6, 49],
7: [7, 97],
45: [7, 96],
59: [5, 23],
15: [7, 91],
11: [8, 181],
72: [8, 180],
27: [8, 179],
28: [8, 178],
16: [7, 88],
88: [10, 703],
113: [11, 1405],
89: [12, 2809],
107: [13, 5617],
90: [14, 11233],
42: [15, 22465],
64: [16, 44929],
0: [16, 44928],
81: [9, 350],
29: [8, 174],
118: [8, 173],
30: [8, 172],
98: [8, 171],
12: [8, 170],
99: [7, 84],
117: [6, 41],
112: [6, 40],
102: [9, 319],
68: [9, 318],
31: [8, 158],
100: [7, 78],
84: [6, 38],
55: [6, 37],
17: [7, 73],
8: [7, 72],
9: [7, 71],
77: [7, 70],
18: [7, 69],
65: [7, 68],
48: [6, 33],
116: [6, 32],
10: [7, 63],
121: [8, 125],
78: [8, 124],
80: [7, 61],
69: [7, 60],
119: [7, 59],
13: [8, 117],
79: [8, 116],
19: [7, 57],
67: [7, 56],
114: [6, 27],
83: [6, 26],
115: [6, 25],
14: [6, 24],
122: [8, 95],
95: [8, 94],
76: [7, 46],
24: [7, 45],
37: [7, 44],
50: [5, 10],
51: [5, 9],
108: [6, 17],
22: [7, 33],
120: [8, 65],
66: [8, 64],
21: [7, 31],
106: [7, 30],
47: [6, 14],
53: [5, 6],
49: [5, 5],
86: [8, 39],
85: [8, 38],
23: [7, 18],
75: [7, 17],
20: [7, 16],
2: [5, 3],
73: [8, 23],
43: [9, 45],
87: [9, 44],
70: [7, 10],
3: [6, 4],
52: [5, 1],
54: [5, 0]
}

# the actual encoding function
def main_encode(e):
def t(r, o, input_tuple, n):
shift, value = input_tuple
r = (r << shift) | value
o += shift
while o >= 6:
e = (r >> (o - 6)) & 63
n += A[e]
r ^= e << (o - 6)
o -= 6
return n, r, o

n, r, o = “”, 0, 0
n, r, o = t(r, o, (6, (7 & len(e)) << 3 | 0), n)
n, r, o = t(r, o, (6, 56 & len(e) | 1), n)

for char in e:
char_code = ord(char)
if char_code not in w:
return “”
n, r, o = t(r, o, w[char_code], n)

n, r, o = t(r, o, w[0], n)
if o > 0:
n, r, o = t(r, o, (6 - o, 0), n)

return n

# replacing some stuff in the string?
n = e
for r, rep in enumerate(y):
n = n.replace(rep, chr(r + 1))

# checksum calculation I think
n_val = 65535
for char in e:
n_val = ((n_val >> 8) | (n_val << 8)) & 65535
n_val ^= 255 & ord(char)
n_val ^= (255 & n_val) >> 4
n_val ^= (n_val << 12) & 65535
n_val ^= ((255 & n_val) << 5) & 65535
n_val &= 65535
n_val &= 65535
checksum = A[n_val >> 12] + A[(n_val >> 6) & 63] + A[n_val & 63]

# adding checksum to the encoded string
return main_encode(n) + checksum

def generate(cls):
def get_timezone_offset(date):
local_time = timezone.localize(date)
return int(-local_time.utcoffset().total_seconds() / 60)

t1 = get_timezone_offset(datetime.datetime(2005, 1, 15))
t2 = get_timezone_offset(datetime.datetime(2005, 7, 15))

def base_is_dst():
return abs(t1 - t2) != 0

def base_is_dst_str():
return str(base_is_dst()).lower()

def is_dst(date):
return base_is_dst and get_timezone_offset(date) == min(t1, t2)

def is_dst_str(date):
return str(is_dst(date)).lower()

def calculate_offset(date):
return int(-(get_timezone_offset(date) + abs(t2 - t1) * is_dst(date)) / 60)

# technically not the same as the browser, but close enough
def get_locale_string(date):
return urllib.parse.quote(date.strftime(“%m/%d/%Y, %I:%M:%S %p”))

def get_timestamp(date):
return int(date.timestamp() * 1000)

current_time = datetime.datetime.now()

return f’TF1;020;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;{base_is_dst_str()};{is_dst_str(current_time)};{get_timestamp(current_time)};{calculate_offset(current_time)};{get_locale_string(datetime.datetime(2005,6,7,21,33,44,888))};;;;;;;;;{random.randint(1000, 9999)};{t1};{t2};{get_locale_string(current_time)};;;;;;;;;;;;;;;;;;;;;;;;25;;;;;;;;;;;;;;;5.6.1-0;;’

def create_fingerprint(cls):
return cls.encode(cls.generate())

# all the garbage that is tracked for fingerprinting if you’re curious
‘’‘
var t = new Date
, r = new Date
, o = [u(“TF1”), u(“020”), function() {
return ScriptEngineMajorVersion()
}
, function() {
return ScriptEngineMinorVersion()
}
, function() {
return ScriptEngineBuildVersion()
}
, function() {
return c(“{7790769C-0471-11D2-AF11-00C04FA35D02}”)
}
, function() {
return c(“{89820200-ECBD-11CF-8B85-00AA005B4340}”)
}
, function() {
return c(“{283807B5-2C60-11D0-A31D-00AA00B92C03}”)
}
, function() {
return c(“{4F216970-C90C-11D1-B5C7-0000F8051515}”)
}
, function() {
return c(“{44BBA848-CC51-11CF-AAFA-00AA00B6015C}”)
}
, function() {
return c(“{9381D8F2-0288-11D0-9501-00AA00B911A5}”)
}
, function() {
return c(“{4F216970-C90C-11D1-B5C7-0000F8051515}”)
}
, function() {
return c(“{5A8D6EE0-3E18-11D0-821E-444553540000}”)
}
, function() {
return c(“{89820200-ECBD-11CF-8B85-00AA005B4383}”)
}
, function() {
return c(“{08B0E5C0-4FCB-11CF-AAA5-00401C608555}”)
}
, function() {
return c(“{45EA75A0-A269-11D1-B5BF-0000F8051515}”)
}
, function() {
return c(“{DE5AED00-A4BF-11D1-9948-00C04F98BBC9}”)
}
, function() {
return c(“{22D6F312-B0F6-11D0-94AB-0080C74C7E95}”)
}
, function() {
return c(“{44BBA842-CC51-11CF-AAFA-00AA00B6015B}”)
}
, function() {
return c(“{3AF36230-A269-11D1-B5BF-0000F8051515}”)
}
, function() {
return c(“{44BBA840-CC51-11CF-AAFA-00AA00B6015C}”)
}
, function() {
return c(“{CC2A9BA0-3BDD-11D0-821E-444553540000}”)
}
, function() {
return c(“{08B0E5C0-4FCB-11CF-AAA5-00401C608500}”)
}
, function() {
return “”
}
, function() {
return “”
}
, function() {
return “”
}
, function() {
return s([“navigator.productSub”, “navigator.appMinorVersion”])
}
, function() {
return “”
}
, function() {
return “”
}
, function() {
return s([“navigator.oscpu”, “navigator.cpuClass”])
}
, function() {
return “”
}
, function() {
return “”
}
, function() {
return “”
}
, function() {
return “”
}
, function() {
return s([“navigator.language”, “navigator.userLanguage”])
}
, function() {
return “”
}
, function() {
return “”
}
, function() {
return “”
}
, function() {
return “”
}
, function() {
return “”
}
, function() {
return “”
}
, function() {
return 0 !== Math.abs(h - g)
}
, function() {
return a(t)
}
, function() {
return “@UTC@”
}
, function() {
var e = 0;
return e = 0,
a(t) && (e = Math.abs(h - g)),
-(t.getTimezoneOffset() + e) / 60
}
, function() {
return new Date(2005,5,7,21,33,44,888).toLocaleString()
}
, function() {
return “”
}
, function() {
return “”
}
, function() {
return v.Acrobat
}
, function() {
return v.Flash
}
, function() {
return v.QuickTime
}
, function() {
return v[“Java Plug-in”]
}
, function() {
return v.Director
}
, function() {
return v.Office
}
, function() {
return “@CT@”
}
, function() {
return h
}
, function() {
return g
}
, function() {
return t.toLocaleString()
}
, function() {
return “”
}
, function() {
return “”
}
, function() {
return “”
}
, function() {
return “”
}
, function() {
return “”
}
, function() {
return n(“Acrobat”)
}
, function() {
return n(“Adobe SVG”)
}
, function() {
return n(“Authorware”)
}
, function() {
return n(“Citrix ICA”)
}
, function() {
return n(“Director”)
}
, function() {
return n(“Flash”)
}
, function() {
return n(“MapGuide”)
}
, function() {
return n(“MetaStream”)
}
, function() {
return n(“PDFViewer”)
}
, function() {
return n(“QuickTime”)
}
, function() {
return n(“RealOne”)
}
, function() {
return n(“RealPlayer Enterprise”)
}
, function() {
return n(“RealPlayer Plugin”)
}
, function() {
return n(“Seagate Software Report”)
}
, function() {
return n(“Silverlight”)
}
, function() {
return n(“Windows Media”)
}
, function() {
return n(“iPIX”)
}
, function() {
return n(“nppdf.so”)
}
, function() {
var e = document.createElement(“span”);
e.innerHTML = " ",
e.style.position = “absolute”,
e.style.left = “-9999px”,
document.body.appendChild(e);
var t = e.offsetHeight;
return document.body.removeChild(e),
t
}
, m(), m(), m(), m(), m(), m(), m(), m(), m(), m(), m(), m(), m(), m(), function() {
return “5.6.1-0”
}
, m()];
’‘’


applemusic_api.py:
import re
import base64
import pbkdf2
import hashlib

from Cryptodome.Hash import SHA256

from uuid import uuid4
from utils.utils import create_requests_session

from .fingerprint import Fingerprint


import srp.pysrp as srp
srp.rfc5054_enable()
srp.no_username_in_x()

def b64enc(data):
return base64.b64encode(data).decode()

def b64dec(data):
return base64.b64decode(data)


class AppleMusicApi(object):
def init(self, exception, storefront=‘US’, language=‘en-US’, lyrics_resource=‘lyrics’):
self.s = create_requests_session()
self.api_base = ‘https://amp-api.music.apple.com/v1/’

self.storefront = storefront
self.language = language
self.lyrics_storefront = storefront
self.lyrics_language = language
self.lyrics_resource = lyrics_resource

self.access_token = ‘’
self.user_token = ‘’

self.exception = exception

self.user_agent = ‘Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.66 Safari/537.36’

def headers(self):
return {
‘authorization’: ‘Bearer ’ + self.access_token,
‘Connection’: ‘Keep-Alive’,
‘Content-Type’: ‘application/json’,
‘Origin’: ‘https://music.apple.com’,
‘Referer’: ‘https://music.apple.com/’,
‘Accept-Encoding’: ‘gzip, deflate’,
‘Accept-Language’: f’{self.language},en;q=0.9’,
‘User-Agent’: self.user_agent,
‘Media-User-Token’: self.user_token,
‘x-apple-renewal’: ‘true’
}

def get_access_token(self):
s = create_requests_session()
r = s.get(‘https://music.apple.com/us/search’, headers=self.headers())
if r.status_code != 200: raise self.exception(r.text)

index_js = re.search(‘(?<=index-)(.?)(?=.js")‘, r.text).group(1)
r = s.get(f’https://music.apple.com/assets/index-{index_js}.js’, headers=self.headers())
if r.status_code != 200: raise self.exception(r.text)

self.access_token = re.search('(?=eyJh)(.?)(?=")’, r.text).group(1)
return self.access_token

def auth(self, email: str, password: str):
auth_url = ‘https://idmsa.apple.com/appleauth/’
client_id = ‘06f8d74b71c73757a2f82158d5e948ae7bae11ec45fda9a58690f55e35945c51’
frame_id = ‘auth-’ + str(uuid4()).lower()

# get “dslang”, “site” and “aasp” cookies
r = self.s.get(auth_url + ‘auth/authorize/signin’, headers=self.headers(), params={
‘frame_id’: frame_id,
‘language’: ‘en_us’,
‘skVersion’: ‘7’,
‘iframeId’: frame_id,
‘client_id’: client_id,
‘redirect_uri’: ‘https://music.apple.com’,
‘response_type’: ‘code’,
‘response_mode’: ‘web_message’,
‘account_ind’: ‘1’,
‘state’: frame_id,
‘authVersion’: ‘latest’
})
if r.status_code != 200: raise self.exception(r.text)
auth_attributes = r.headers[‘X-Apple-Auth-Attributes’]

# get “aa” cookie
r = self.s.post(auth_url + ‘jslog’, headers=self.headers(), json={
‘type’: ‘INFO’,
‘title’: ‘AppleAuthPerf-s-y’,
‘message’: ‘’‘APPLE ID : TTI {“data”:{“initApp”:{“startTime”:1154.2000000001863},“loadAuthComponent”:{“startTime”:1500.7000000001863},“startAppToTTI”:{“duration”:346.70000000018626}},“order”:[“initApp”,“loadAuthComponent”,“startAppToTTI”]}’‘’,
‘iframeId’: frame_id,
‘details’: ‘’‘{“pageVisibilityState”:“visible”}’‘’
})
assert (r.status_code == 200)

# actual login
headers = {
‘Accept’: ‘application/json’,
‘Referer’: ‘https://idmsa.apple.com/’,
‘Content-Type’: ‘application/json’,
‘X-Apple-Widget-Key’: client_id,
‘X-Apple-Frame-Id’: frame_id,
‘X-Apple-Domain-Id’: ‘3’,
‘X-Apple-Locale’: ‘en_us’,
‘X-Requested-With’: ‘XMLHttpRequest’,
‘Origin’: ‘https://idmsa.apple.com’,
‘X-Apple-I-Require-UE’: ‘true’,
‘X-Apple-I-FD-Client-Info’: ‘{’ + f’“U”:“{self.user_agent}”,“L”:“{self.language}”,“Z”:“GMT-8:00”,“V”:“1.1”,“F”:“{Fingerprint().create_fingerprint()}”’ + ‘}’,
‘X-Apple-Auth-Attributes’: auth_attributes,
‘User-Agent’: self.user_agent,
‘X-Apple-Mandate-Security-Upgrade’: ‘0’
}

json = {‘accountName’: email, ‘rememberMe’: ‘false’}
params = {‘isRememberMeEnabled’: ‘false’}

r = self.s.post(auth_url + ‘auth/federate’, headers=headers, params=params, json=json)
if ‘federated’ not in r.json(): raise self.exception(r.text)

# finally begin login
user = srp.User(email, bytes(), hash_alg=srp.SHA256, ng_type=srp.NG_2048)
, A = user.start_authentication()

json = {‘a’: b64enc(A), ‘accountName’: email, ‘protocols’: [‘s2k’, ‘s2k_fo’]}

r = self.s.post(auth_url + ‘auth/signin/init’, headers=headers, json=json)
out_json = r.json()
if r.status_code != 200: raise self.exception(out_json[‘serviceErrors’][0][‘message’])
if ‘b’ not in out_json: raise self.exception(r.text)
if out_json.get(‘protocol’) != ‘s2k’: raise self.exception(‘Protocol not supported’)

salt = b64dec(out_json[‘salt’])
iterations = out_json[‘iteration’]
B = b64dec(out_json[‘b’])
c = out_json[‘c’]

pass_hash = hashlib.sha256(password.encode(“utf-8”)).digest()
enc_pass = pbkdf2.PBKDF2(pass_hash, salt, iterations, SHA256).read(32)

user.p = enc_pass
M1 = user.process_challenge(salt, B)
if M1 is None: raise self.exception(“Failed to process challenge”)

M2 = user.K

# real version uses m2 as well… hmmm
json = {‘accountName’: email, ‘c’: c, ‘m1’: b64enc(M1), ‘m2’: b64enc(M2), ‘rememberMe’: ‘false’}

r = self.s.post(auth_url + ‘auth/signin/complete’, headers=headers, params=params, json=json_)
if r.status_code != 200: raise self.exception(r.json()[‘serviceErrors’][0][‘message’])

# exchange the “myacinfo” cookie with the “media-user-token”
r = self.s.post(‘https://buy.music.apple.com/account/web/auth’, headers=self.headers(), json={‘webAuthorizationFlowContext’: ‘music’})
if r.status_code != 200: raise self.exception(r.text)

self.user_token = self.s.cookies[‘media-user-token’]
return self.user_token

def get_account_details(self, force_region, selected_language, lyrics_language):
r = self.s.get(self.api_base + ‘me/account’, headers=self.headers(), params={‘meta’: ‘subscription’})
if r.status_code != 200: raise self.exception(r.text)

self.lyrics_storefront = r.json()[‘meta’][‘subscription’][‘storefront’]
if force_region.lower() == self.lyrics_storefront: force_region = None
if force_region: print(f"Apple Music: WARNING: Selected region {force_region} is not the same as your Apple Music region {self.lyrics_storefront}, lyrics will use the region {self.lyrics_storefront}. Only lyrics available in both regions will be used, maybe use a copy of the module with the folder name (which determines the name of the module) and the netlocation_constant changed for lyrics only if you want credits or playlists from other regions.“)

self.storefront = force_region.lower() if force_region else self.lyrics_storefront
account_active = r.json()[‘meta’][‘subscription’][‘active’]

storefront_endpoint = f’storefronts/{force_region.lower()}’ if force_region else ‘me/storefront’
endpoint_data = self.s.get(self.api_base + storefront_endpoint, headers=self.headers())
if endpoint_data.status_code != 200: raise self.exception(f’Region {force_region} is not supported’)

supported_languages = endpoint_data.json()[‘data’][0][‘attributes’][‘supportedLanguageTags’]
if selected_language:
for i in supported_languages:
if selected_language in i:
self.language = i
break
else:
print(f"Apple Music: WARNING: Selected language {selected_language} in region {force_region if force_region else self.lyrics_storefront} is unsupported, force a different region or use one of these: {', '.join(supported_languages)}”)
self.language = supported_languages[0]
else:
self.language = supported_languages[0]

if not lyrics_language: lyrics_language = selected_language

if force_region:
supported_languages = self.s.get(f’{self.api_base}me/storefront’, headers=self.headers()).json()[‘data’][0][‘attributes’][‘supportedLanguageTags’]
if lyrics_language:
for i in supported_languages:
if selected_language in i:
self.lyrics_language = i
break
else:
print(f"Apple Music: WARNING: Selected language {selected_language} in lyrics region {self.lyrics_storefront} is unsupported, force a different region or use one of these: {‘, ‘.join(supported_languages)}")
self.lyrics_language = supported_languages[0]
else:
self.lyrics_language = supported_languages[0]

return self.storefront, account_active, self.language, self.lyrics_language, self.lyrics_storefront

def check_active_subscription(self):
url = f’{self.api_base}me/account’
params = {‘meta’: ‘subscription’, ‘challenge[subscriptionCapabilities]’: ‘voice,premium’}

response = self.s.get(url, headers=self.headers(), params=params)
if response.status_code != 200: raise self.exception(response.text)
response_data = response.json()

if ‘meta’ in response_data and ‘subscription’ in response_data[‘meta’]:
return response_data[‘meta’][‘subscription’].get(‘active’, False)

return False

def _get(self, url: str, params=None, storefront=None, language=None):
if not params: params = {}
if not storefront: storefront = self.storefront
params[‘l’] = language if language else self.language

r = self.s.get(f’{self.api_base}catalog/{storefront}/{url}‘, params=params, headers=self.headers())
if r.status_code not in [200, 201, 202]: raise self.exception(r.text)

return r.json()

def search(self, query_type: str, query: str, limit: int = 10):
if limit > 25: limit = 25

params = {
‘term’: query,
‘types’: query_type,
‘limit’: limit
}

if query_type == ‘songs’:
params[‘extend[songs]’] = ‘attribution,composerName,contentRating,discNumber,durationInMillis,isrc,movementCount,movementName,movementNumber,releaseDate,trackNumber,workNamedata’
params[‘include[songs]’] = ‘artists,albums’ + (f’,{self.lyrics_resource}’ if self.storefront == self.lyrics_storefront else ‘’) # doesn’t give lyrics?
params[‘extend[albums]’] = ‘copyright,upc’
elif query_type == ‘playlists’:
params[‘include[playlists]’] = ‘curator’
params[‘extend[playlists]’] = ‘artwork,description,trackTypes,trackCount’

results = self._get(‘search’, params)[‘results’]
if query_type in results:
results = results[query_type][‘data’]
else:
results = []

return results

def get_playlist_base_data(self, playlist_id):
return self._get(f’playlists/{playlist_id}‘, params={
‘include’: ‘curator,tracks’,
‘extend’: ‘artwork,description,trackTypes,trackCount’,
‘include[songs]’: ‘artists,albums’ + (f’,{self.lyrics_resource}’ if self.storefront == self.lyrics_storefront else ‘’),
‘extend[songs]’: ‘extendedAssetUrls,attribution,composerName,contentRating,discNumber,durationInMillis,isrc,movementCount,movementName,movementNumber,releaseDate,trackNumber,workNamedata’,
‘extend[albums]’: ‘copyright,upc’
})[‘data’][0]

def get_playlist_tracks(self, playlist_data):
tracks_list, track_data = [], {}
tracks = list(playlist_data[‘relationships’][‘tracks’][‘data’])
offset = len(tracks)

while len(tracks) + offset <= playlist_data[‘attributes’][‘trackCount’]:
tracks += self._get(f’playlists/{playlist_data[“id”]}/tracks’, params={
‘offset’: offset,
‘include[songs]’: ‘artists,albums’ + (f’,{self.lyrics_resource}’ if self.storefront == self.lyrics_storefront else ‘’),
‘extend[songs]’: ‘extendedAssetUrls,attribution,composerName,contentRating,discNumber,durationInMillis,isrc,movementCount,movementName,movementNumber,releaseDate,trackNumber,workNamedata’,
‘extend[albums]’: ‘copyright,upc’,
‘limit’: 100
})[‘data’]
offset += 100

for track in tracks:
tracks_list.append(track[‘id’])
track_data[track[‘id’]] = track

return tracks_list, track_data

def get_tracks_by_ids(self, track_ids: list = None, isrc: str = None):
if not track_ids: track_ids = []

params = {‘filter[isrc]’: isrc} if isrc else {‘ids’: ‘,’.join(track_ids)}
params[‘include’] = ‘artists,albums’ + (f’,{self.lyrics_resource}’ if self.storefront == self.lyrics_storefront else ‘’)
params[‘extend’] = ‘attribution,composerName,contentRating,discNumber,durationInMillis,isrc,movementCount,movementName,movementNumber,releaseDate,trackNumber,workNamedata’
params[‘extend[albums]’] = ‘copyright,upc’

return self._get(‘songs’, params)[‘data’]

def get_track(self, track_id: str = None):
return self.get_tracks_by_ids([track_id])[0]

@staticmethod
def get_lyrics_support(track_attributes):
# could technically be a single line in the lambda
if track_attributes.get(‘hasTimeSyncedLyrics’):
return 1 if track_attributes.get(‘isVocalAttenuationAllowed’) else 2
else:
return 3 if track_attributes.get(‘hasLyrics’) else 4

def get_track_by_isrc(self, isrc: str, album_name: str):
results = self.get_tracks_by_ids(isrc=isrc)

correct_region_results = [i for i in results if i[‘attributes’][‘url’].split(‘i=’)[-1].split(‘&’)[0] == i[‘id’]]
incorrect_region_results = [i for i in results if i[‘attributes’][‘url’].split(‘i=’)[-1].split(‘&’)[0] != i[‘id’]]

correct_region_results_sorted_by_track_number = sorted(correct_region_results, key=lambda x: x[‘attributes’].get(‘trackNumber’, 1))

fix_results_by_album = lambda list_to_sort: sorted(list_to_sort, key=lambda x: (x[‘attributes’][‘albumName’] != album_name))
correct_album_correct_region_results = fix_results_by_album(correct_region_results_sorted_by_track_number)
correct_album_incorrect_region_results = fix_results_by_album(incorrect_region_results)

correct_album_prioritised_lyrics_results = sorted(correct_album_correct_region_results, key=lambda x: self.get_lyrics_support(x[‘attributes’]))
return correct_album_prioritised_lyrics_results + correct_album_incorrect_region_results

def get_lyrics(self, track_id, lyrics_resource=None):
if not lyrics_resource: lyrics_resource = self.lyrics_resource

try:
data = self._get(f’songs/{track_id}/{lyrics_resource}', storefront=self.lyrics_storefront, language=self.language)
except self.exception:
return None

return data#[‘data’][0][‘attributes’][‘ttml’]