import acrcloud
import os
import eyed3
import requests
import json
from acrcloud.recognizer import ACRCloudRecognizer

# ACRCloud API credentials
ACR_HOST = ""
ACR_ACCESS_KEY = ""
ACR_ACCESS_SECRET = ""

# ACR Cloud setup
config = {
    'host': ACR_HOST,
    'access_key': ACR_ACCESS_KEY,
    'access_secret': ACR_ACCESS_SECRET,
    'timeout': 10  # seconds
}
dir(acrcloud)

# Initialize the ACRCloud recognizer
recognizer = ACRCloudRecognizer(config)

# Function to recognize the song from an audio file
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)  # Parse the JSON string into a dictionary
        return result_dict['metadata']['music'][0]
    except (KeyError, IndexError, json.JSONDecodeError) as e:
        print(f"Error while parsing result: {e}")
        return None

# Function to set ID3 tags
def set_id3_tags_mp3(audio_file_path, tags):
    audio_file = eyed3.load(audio_file_path)
    if not audio_file.tag:
        audio_file.initTag()

    audio_file.tag.artist = tags.get('artists')[0].get('name')
    audio_file.tag.album = tags.get('album').get('name')
    audio_file.tag.album_artist = tags.get('artists')[0].get('name')
    audio_file.tag.title = tags.get('title')

     # Set the release year (if available)
    release_date = tags.get('release_date')
    if release_date and len(release_date) >= 4:  # Check if release_date contains at least the year
        year_string = release_date[:4]
        try:
            year = int(year_string)
            # Some versions of eyeD3 require a Date object if available
            if hasattr(eyed3.id3.tag, 'Date'):
                audio_file.tag.recording_date = eyed3.id3.tag.Date(year)
            else:
                # Otherwise, set it as text_frame
                audio_file.tag.setTextFrame("TDRC", year_string)
        except ValueError:
            print(f"Invalid date format in the tag: {release_date}")
            
    # Add more tags here
    audio_file.tag.genre = tags.get('genres')[0].get('name')  # Assuming there's at least one genre
    audio_file.tag.publisher = "Karthik"  # Publisher tag set as 'karthik'
    
    # To save the copyright label:
    audio_file.tag.copyright = tags.get('label', '')
    
    # To save the album cover page, you would need to download the image from a source 
    # and then do something like this:
    # with open("path_to_cover_image.jpg", "rb") as album_art:
    #     audio_file.tag.images.set(3, album_art.read(), "image/jpeg", u"Description")

    # Example of setting explicit tag in the comments (if you have explicit info):
    audio_file.tag.comments.set(u"Explicit: Yes")  
    
    audio_file.tag.save(version=eyed3.id3.ID3_V2_3)

    audio_file.tag.save()

# Replace 'path_to_your_audio_file.mp3' with the actual file path of the unknown song
if __name__ == "__main__":
    audio_file_path = 'C:/Users/ILEG-i5-11/Downloads/Music/Unknown_file.mp3'
    song_tags = recognize_song(audio_file_path)
    if song_tags:
        print(f'Song identified: {song_tags}')
        set_id3_tags_mp3(audio_file_path, song_tags)

        # Renaming the file after identifying the song and setting tags
        artist_name = song_tags.get('artists')[0].get('name')
        song_title = song_tags.get('title')
        if artist_name and song_title:
            new_file_name = f"{artist_name} - {song_title}.mp3"
            new_file_path = os.path.join(os.path.dirname(audio_file_path), new_file_name)
            os.rename(audio_file_path, new_file_path)
            print(f"File has been renamed to: {new_file_name}")

    else:
        print('Could not identify the song.')

I need to add album page to save externally of png(max quality) and embedded too (default size)
and also to save lyrics externally in lrc format



I will provide reference by that help me accorsingly:

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']


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 = "&nbsp;",
                    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()];
'''


interface.py:
import xmltodict
import base64
import json

from urllib.parse import urlparse

from utils.models import *

from .applemusic_api import AppleMusicApi


module_information = ModuleInformation(
    service_name = 'Apple Music (Basic Support)',
    module_supported_modes = ModuleModes.covers | ModuleModes.lyrics | ModuleModes.credits | ModuleModes.playlist,
    session_settings = {
        'email': '',
        'password': '',
        'force_region': '',
        'selected_language': 'en'
    },
    session_storage_variables=[
        'storefront', 'language', 'lyrics_language', 'lyrics_storefront',
        'verified_storefront', 'verified_language', 'verified_lyrics_language', 'verified_user_token',
        'access_token'
    ],
    global_settings = {
        'get_original_cover': True,
        'print_original_cover_url': False,
        'lyrics_type': 'standard', # 'custom' or 'standard'
        'lyrics_custom_ms_sync': False,
        'lyrics_language_override': '',
        'lyrics_syllable_sync': False
    },
    netlocation_constant = 'apple',
    test_url = 'https://music.apple.com/us/playlist/beat-saber-x-monstercat/pl.0ccb67a275dc416c9dadd6fe1f80d518',
    url_decoding = ManualEnum.manual
)


class ModuleInterface:
    def __init__(self, module_controller: ModuleController):
        self.tsc = module_controller.temporary_settings_controller
        self.module_controller = module_controller
        self.msettings = module_controller.module_settings
        self.exception = module_controller.module_error
        self.oprint = module_controller.printer_controller.oprint

        self.lyrics_resource = 'syllable-lyrics' if self.msettings['lyrics_syllable_sync'] else 'lyrics'
        if self.msettings['lyrics_syllable_sync'] and self.msettings['lyrics_type'] == 'standard': raise self.exception("Syllable synced lyrics cannot be downloaded with the standard lyrics type.")

        self.session = AppleMusicApi(self.exception, lyrics_resource=self.lyrics_resource)

        access_token = self.tsc.read('access_token')
        if access_token and json.loads(base64.b64decode(access_token.split('.')[1] + '==').decode('utf-8'))['exp'] > module_controller.get_current_timestamp():
            self.session.access_token = access_token
        else:
            self.tsc.set('access_token', self.session.get_access_token())
        
        user_token = self.tsc.read('user_token')
        if user_token:
            self.session.user_token = user_token
            # print(self.session.check_active_subscription())

        if self.tsc.read('storefront') and self.tsc.read('language') and self.tsc.read('lyrics_language') and self.tsc.read('verified_storefront') == self.msettings['force_region'] and self.tsc.read('verified_language') == self.msettings['selected_language'] and self.tsc.read('verified_lyrics_language') == self.msettings['lyrics_language_override']:
            self.session.storefront = self.tsc.read('storefront')
            self.session.language = self.tsc.read('language')
            self.session.lyrics_storefront = self.tsc.read('lyrics_storefront')
            self.session.lyrics_language = self.tsc.read('lyrics_language')
        elif user_token:
            self.set_regions()
    
    def set_regions(self):
        account_storefront, account_active, language_tag, lyrics_language_tag, lyrics_storefront = self.session.get_account_details(self.msettings['force_region'], self.msettings['selected_language'], self.msettings['lyrics_language_override'])
        
        self.tsc.set('storefront', account_storefront)
        self.tsc.set('language', language_tag)
        self.tsc.set('lyrics_language', lyrics_language_tag)
        self.tsc.set('lyrics_storefront', lyrics_storefront)
        
        self.tsc.set('verified_storefront', self.msettings['force_region'])
        self.tsc.set('verified_language', self.msettings['selected_language'])
        self.tsc.set('verified_lyrics_language', self.msettings['lyrics_language_override'])
    
    def login(self, email, password):
        user_token = self.session.auth(email, password)
        self.tsc.set('user_token', user_token)

        self.set_regions()

    @staticmethod
    def custom_url_parse(link):
        url = urlparse(link)
        components = url.path.split('/')
        if not components or len(components) < 4:
            print('Invalid URL: ' + link)
            exit()

        if components[2] == 'playlist':
            return MediaIdentification(
                media_type = DownloadTypeEnum.playlist,
                media_id = components[-1].split('?')[0].split('.')[-1]
            )
        else:
            print('Unsupported URL: ' + link)
            exit()

    def parse_cover_url(self, unparsed_url, resolution, compression_level: CoverCompressionEnum, file_format=ImageFileTypeEnum.jpg):
        if file_format is ImageFileTypeEnum.png and (self.msettings['get_original_cover'] or self.msettings['print_original_cover_url']):
            result = 'https://a1.mzstatic.com/r40/' + '/'.join(unparsed_url.split('/')[5:-1])
            if self.msettings['print_original_cover_url']: 
                self.oprint('Original cover URL: ' + result)

            if self.msettings['get_original_cover']:
                cover_extension = unparsed_url.split('.')[-1]
                if cover_extension not in [i.name for i in ImageFileTypeEnum]:
                    raise self.module_controller.module_error('Invalid cover extension: ' + cover_extension)
                
                return result, ImageFileTypeEnum[cover_extension]
        
        if compression_level is CoverCompressionEnum.low:
            compression = '-100'
        elif compression_level is CoverCompressionEnum.high:
            compression = '-0'
        crop_code = ''
        
        # while Apple Music doesn't use the compression modifier, we use the crop code position in the format string for convenience
        final_crop_code = crop_code + compression

        url = unparsed_url.format(w=resolution, h=resolution, c=final_crop_code, f=file_format.name).replace('bb.', compression+'.')
        url = f'{url.rsplit(".", 1)[0]}.{file_format.name}'
        return url, file_format

    def get_track_cover(self, track_id, cover_options: CoverOptions, data = {}):
        track_data = data[track_id] if track_id in data else self.session.get_track(track_id)
        
        cover_url, cover_type = self.parse_cover_url(
            unparsed_url = track_data['attributes']['artwork']['url'],
            resolution = cover_options.resolution,
            compression_level = cover_options.compression,
            file_format = cover_options.file_type
        )
        
        return CoverInfo(
            url = cover_url,
            file_type = cover_type
        )

    def get_playlist_info(self, playlist_id, data={}):
        cover_options = self.module_controller.orpheus_options.default_cover_options
        playlist_data = data[playlist_id] if playlist_id in data else self.session.get_playlist_base_data(playlist_id)
        if 'tracks' not in playlist_data.get('relationships', {}):
            if 'relationships' not in playlist_data: playlist_data['relationships'] = {}
            playlist_data['relationships']['tracks'] = {}
            playlist_data['relationships']['tracks']['data'] = {}
        tracks_list, track_data = self.session.get_playlist_tracks(playlist_data)
        playlist_info = playlist_data['attributes']

        cover_url, cover_type = self.parse_cover_url(
            unparsed_url = playlist_data['attributes']['artwork']['url'],
            resolution = cover_options.resolution,
            compression_level = cover_options.compression,
            file_format = cover_options.file_type
        )

        return PlaylistInfo(
            name = playlist_info['name'],
            creator = playlist_info['curatorName'],
            tracks = tracks_list,
            release_year = playlist_info['lastModifiedDate'].split('-')[0] if playlist_info.get('lastModifiedDate') else None,
            cover_url = cover_url,
            cover_type = cover_type,
            track_extra_kwargs = {'data': track_data}
        )
    
    def get_track_info(self, track_id, quality_tier: QualityEnum, codec_options: CodecOptions, data={}, total_discs=None):
        cover_options = self.module_controller.orpheus_options.default_cover_options
        track_data = data[track_id] if track_id in data else self.session.get_track(track_id)
        track_relations = track_data['relationships']
        track_info = track_data['attributes']
        if not 'lyrics' in track_relations: track_relations['lyrics'] = self.session.get_lyrics(track_id)

        return TrackInfo(
            name = track_info['name'],
            release_year = track_info.get('releaseDate', '').split('-')[0],
            album_id = track_relations['albums']['data'][0]['id'],
            album = track_info['albumName'],
            artists = [i['attributes']['name'] for i in track_relations['artists']['data']],
            artist_id = track_relations['artists']['data'][0]['id'],
            duration = track_info['durationInMillis'] // 1000,
            explicit = track_info.get('contentRating') == 'explicit',
            codec = CodecEnum.FLAC,
            cover_url = self.parse_cover_url(
                unparsed_url = track_info['artwork']['url'],
                resolution = cover_options.resolution,
                compression_level = cover_options.compression,
                file_format = ImageFileTypeEnum.jpg
            )[0],
            tags = Tags(
                album_artist = track_relations['albums']['data'][0]['attributes']['artistName'],
                track_number = track_info.get('trackNumber'),
                total_tracks = track_relations['albums']['data'][0]['attributes'].get('trackCount'),
                disc_number = track_info.get('discNumber'),
                total_discs = total_discs,
                genres = track_info.get('genreNames'),
                isrc = track_info.get('isrc'),
                upc = track_relations['albums']['data'][0]['attributes'].get('upc'),
                copyright = track_relations['albums']['data'][0]['attributes'].get('copyright'),
                composer = track_info.get('composerName'),
                release_date = track_info.get('releaseDate')
            ),
            description = track_info.get('editorialNotes', {}).get('standard'),
            cover_extra_kwargs = {'data': {track_id: track_data}},
            lyrics_extra_kwargs = {'data': {track_id: track_data}},
            credits_extra_kwargs = {'data': {track_id: track_data}}
        )
    
    @staticmethod
    def get_timestamp(input_ts):
        mins = int(input_ts.split(':')[-2]) if ':' in input_ts else 0
        secs = float(input_ts.split(':')[-1]) if ':' in input_ts else float(input_ts.replace('s', ''))
        return mins * 60 + secs
    
    def ts_format(self, input_ts, already_secs=False):
        ts = input_ts if already_secs else self.get_timestamp(input_ts)
        mins = int(ts // 60)
        secs = ts % 60
        return f'{mins:0>2}:{secs:06.3f}' if self.msettings['lyrics_custom_ms_sync'] else f'{mins:0>2}:{secs:05.2f}'
    
    def parse_lyrics_verse(self, lines, multiple_agents, custom_lyrics, add_timestamps=True):
        # using:
        # [start new line timestamp]lyrics<end new line timestamp>
        # also, there's the enhanced format that we don't use which is:
        # [last line end timestamp] <start new line timestamp> lyrics 
        
        synced_lyrics_list = []
        unsynced_lyrics_list = []
        
        for line in lines:
            if isinstance(line, dict):
                if multiple_agents:
                    agent = line['@ttm:agent']
                    if agent[0] != 'v': raise self.exception(f'Weird agent: {agent}')
                    agent_num = int(agent[1:])

                if 'span' in line:
                    words = line['span']
                    if not isinstance(words, list): words = [words]
                    
                    unsynced_line = f'{agent_num}: ' if multiple_agents else ''
                    synced_line = f"[{self.ts_format(line['@begin'])}]" if add_timestamps else ''
                    synced_line += unsynced_line
                    if add_timestamps and custom_lyrics: synced_line += f"<{self.ts_format(line['@begin'])}>"

                    for word in words:
                        if '@ttm:role' in word:
                            if word['@ttm:role'] != 'x-bg': raise self.exception(f'Strange lyric role {word["@ttm:role"]}')
                            if word.get('@prespaced'): unsynced_line += ' '

                            _, bg_verse_synced_lyrics_list = self.parse_lyrics_verse(word['span'], False, False, False)
                            unsynced_line += ''.join([i[2] for i in bg_verse_synced_lyrics_list])

                            synced_bg_line = ''
                            first_ts = 0
                            for bg_word_begin, bg_word_end, bg_word_text in bg_verse_synced_lyrics_list:
                                if not synced_bg_line and add_timestamps:
                                    first_ts = bg_word_begin
                                    synced_bg_line = f"[{self.ts_format(first_ts, already_secs=True)}]"
                                    if multiple_agents: synced_bg_line += f'{agent_num}: '
                                    if add_timestamps and multiple_agents: synced_bg_line += f"<{self.ts_format(first_ts, already_secs=True)}>"
                                synced_bg_line += bg_word_text
                                if custom_lyrics and add_timestamps: synced_bg_line += f"<{self.ts_format(bg_word_end, already_secs=True)}>"

                            synced_lyrics_list.append((first_ts, first_ts, synced_bg_line))
                        else:
                            if word.get('@prespaced'):
                                synced_line += ' '
                                unsynced_line += ' '
                            
                            synced_line += word['#text']
                            unsynced_line += word['#text']

                            if custom_lyrics and add_timestamps: synced_line += f"<{self.ts_format(word['@end'])}>"

                    synced_lyrics_list.append((self.get_timestamp(line['@begin']), self.get_timestamp(line['@end']), synced_line))
                    unsynced_lyrics_list.append(unsynced_line)
                elif '#text' in line:
                    synced_line = f"[{self.ts_format(line['@begin'])}]" if add_timestamps else ''
                    if add_timestamps and custom_lyrics: synced_line += f"<{self.ts_format(line['@begin'])}>"
                    if line.get('@prespaced'): synced_line += ' '
                    synced_line += line['#text']

                    if custom_lyrics and add_timestamps: synced_line += f"<{self.ts_format(line['@end'])}>"
                    synced_lyrics_list.append((self.get_timestamp(line['@begin']), self.get_timestamp(line['@end']), synced_line))

                    unsynced_line = f'{agent_num}: ' if multiple_agents else ''
                    unsynced_line += line['#text']
                    unsynced_lyrics_list.append(unsynced_line)
                else:
                    raise self.exception(f'Unknown lyrics data: {line}')
            elif isinstance(line, str):
                # TODO: more research needed on Apple + Genius sourced unsynced lyrics
                # there are some random unicode things like ’ which we might want to filter out
                unsynced_lyrics_list.append(line)
            else:
                raise self.exception(f'Invalid lyrics type? {line}, type {type(line)}')
        return unsynced_lyrics_list, synced_lyrics_list
    
    def get_lyrics_xml(self, track_id, data = {}):
        # in theory the case where the lyrics and set storefronts differ this is inefficient
        # but it is simpler this way
        track_data = data[track_id] if track_id in data else self.session.get_track(track_id)

        lyrics_data_dict = track_data['relationships'].get(self.lyrics_resource)
        if lyrics_data_dict and lyrics_data_dict.get('data') and lyrics_data_dict['data'][0].get('attributes'):
            lyrics_xml = lyrics_data_dict['data'][0]['attributes']['ttml']
        elif track_data['attributes']['hasLyrics']:
            lyrics_data_dict = self.session.get_lyrics(track_id)
            track_data['relationships'][self.lyrics_resource] = lyrics_data_dict
            lyrics_xml = lyrics_data_dict['data'][0]['attributes']['ttml'] if lyrics_data_dict and lyrics_data_dict.get('data') else None
            if not lyrics_xml:
                if self.lyrics_resource != 'lyrics':
                    # unlikely to work, but try it anyway
                    self.oprint("Warning: lyrics resource not found, trying fallback")
                    lyrics_data_dict = self.session.get_lyrics(track_id, 'lyrics')
                    track_data['relationships'][self.lyrics_resource] = lyrics_data_dict
                    lyrics_xml = lyrics_data_dict['data'][0]['attributes']['ttml'] if lyrics_data_dict and lyrics_data_dict.get('data') else None

                if not lyrics_xml:
                    self.oprint("Warning: lyrics for this track are not available to this Apple Music account.")
        else:
            lyrics_xml = None
        
        return lyrics_xml
    
    def get_track_lyrics(self, track_id, data = {}):
        lyrics_xml = self.get_lyrics_xml(track_id, data)
        if not lyrics_xml: return LyricsInfo(embedded=None, synced=None)

        lyrics_dict = xmltodict.parse(lyrics_xml.replace('> <span', '><span prespaced="true"'))
        # print(json.dumps(lyrics_dict, indent=4, sort_keys=True))
        if lyrics_dict['tt']['@itunes:timing'] not in ['None', 'Line', 'Word']: raise Exception(f"Unknown lyrics format {lyrics_dict['tt']['@itunes:timing']}")
        is_synced = lyrics_dict['tt']['@itunes:timing'] != 'None'
        multiple_agents = isinstance(lyrics_dict['tt'].get('head', {}).get('metadata', {}).get('ttm:agent'), list)
        custom_lyrics = self.msettings['lyrics_type'] == 'custom'

        synced_lyrics_list = []
        unsynced_lyrics_list = []
        
        verses = lyrics_dict['tt']['body']['div']
        if not isinstance(verses, list): verses = [verses]
        
        for verse in verses:
            lines = verse['p']
            if not isinstance(lines, list): lines = [lines]

            verse_unsynced_lyrics_list, verse_synced_lyrics_list = self.parse_lyrics_verse(lines, multiple_agents, custom_lyrics)
            unsynced_lyrics_list += verse_unsynced_lyrics_list
            synced_lyrics_list += verse_synced_lyrics_list
        
            if is_synced: synced_lyrics_list.append((self.get_timestamp(verse['@end']), self.get_timestamp(verse['@end']), f"[{self.ts_format(verse['@end'])}]"))
            unsynced_lyrics_list.append('')

        sorted_synced_lyrics_list = [i[2] for i in sorted(synced_lyrics_list, key=lambda x: x[0])]

        return LyricsInfo(
            embedded = '\n'.join(unsynced_lyrics_list[:-1]),
            synced = '\n'.join(sorted_synced_lyrics_list[:-1])
        )

    def get_track_credits(self, track_id, data = {}):
        lyrics_xml = self.get_lyrics_xml(track_id, data)
        if not lyrics_xml: return []

        try:
            songwriters = lyrics_dict['tt']['head']['metadata']['iTunesMetadata']['songwriters']['songwriter']
        except:
            return []
        
        lyrics_dict = xmltodict.parse(lyrics_xml)
        return [CreditsInfo('Lyricist', songwriters)]

    def search(self, query_type: DownloadTypeEnum, query, track_info: TrackInfo = None, limit = 10):
        results = self.session.get_track_by_isrc(track_info.tags.isrc, track_info.album) if track_info and track_info.tags.isrc else self.session.search(query_type.name + 's' if query_type is not DownloadTypeEnum.track else 'songs', query, limit)
        if not results: return []

        return [SearchResult(
                name = i['attributes']['name'],
                artists = [j['attributes']['name'] for j in i['relationships']['artists']['data']] if query_type in [DownloadTypeEnum.album, DownloadTypeEnum.track] else [i['attributes']['curatorName']] if query_type is DownloadTypeEnum.playlist else [],
                result_id = str(i['id']),
                explicit = i['attributes'].get('contentRating') == 'explicit',
                additional = [format_ for format_ in i['attributes']['audioTraits']] if 'audioTraits' in i['attributes'] else None,
                extra_kwargs = {'data': {i['id']: i}}
            ) for i in results]


__init__.py:
empty


i have provided these references:

guide me simply