├── requirements.txt ├── config.json ├── modules ├── module.py ├── tidal.py ├── jiosaavn.py ├── spotify.py ├── deezer.py └── applemusic.py ├── README.md ├── utility.py ├── flyrics.py └── .gitignore /requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4==4.12.2 2 | requests==2.25.1 3 | sanitize_filename==1.2.0 4 | lxml 5 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "synced_lyrics": true, 3 | "plain_lyrics": true, 4 | "lyric_file_path": "downloads/{artist} - {album} [{year}]/{trackNo}. {title}", 5 | "applemusic": { 6 | "auth_bearer": "Bearer ey....", 7 | "media-user-token": "AkvJ......" 8 | }, 9 | "spotify": { 10 | "auth_bearer": "Bearer BQ..." 11 | }, 12 | "tidal":{ 13 | "auth_bearer": "Bearer ey..." 14 | }, 15 | "deezer":{ 16 | "arl": "" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /modules/module.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class Module(ABC): 5 | @abstractmethod 6 | def __init__(self, url: str, config: dict, api: bool) -> None: 7 | pass 8 | 9 | @abstractmethod 10 | def getAlbumLyrics(self, albumid: str, data: dict, api: bool) -> None: 11 | pass 12 | 13 | @abstractmethod 14 | def getTrackLyrics(self, trackid: str, data: dict, api: bool) -> None: 15 | pass 16 | 17 | @abstractmethod 18 | def getResponse(self) -> dict: 19 | pass 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # F-Lyrics 2 | A Modular Lyrics Fetcher 3 | 4 | ## Supported services 5 | - Apple Music 6 | - Spotify 7 | - Tidal 8 | - Deezer 9 | - Jiosaavn 10 | 11 | `Other services module can be added or made if required` 12 | 13 | ## Installation 14 | - `git clone https://github.com/bunnykek/F-Lyrics` 15 | - `cd F-Lyrics` 16 | - `pip install -r requirements.txt` 17 | - Open the `config.json` file and fill the tokens. 18 | 19 | ## How to use 20 | ### Terminal 21 | `py flyrics.py URL` 22 | ``` 23 | py flyrics.py https://open.spotify.com/track/5jbDih9bLGmI8ycUKkN5XA 24 | ``` 25 | 26 | ### API usage 27 | ``` 28 | from flyrics import Flyrics 29 | lyric = Flyrics() 30 | lyricJson = lyric.fetch("https://open.spotify.com/track/5jbDih9bLGmI8ycUKkN5XA") 31 | print(lyricJson) 32 | ``` 33 | Response Json ex: 34 | ``` 35 | [ 36 | { 37 | 'synced': " ", 38 | 'plain': " " 39 | }, 40 | { 41 | 'synced': " ", 42 | 'plain': " " 43 | } 44 | ] 45 | ``` 46 | 47 | ![WindowsTerminal_59swFyULxo](https://github.com/bunnykek/F-Lyrics/assets/67633271/b05c4c33-0f3f-4c11-ae58-46323ba92c9e) 48 | 49 | 50 | - I will not be responsible for how you use F-Lyrics 51 | -------------------------------------------------------------------------------- /utility.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from pathlib import Path 4 | 5 | _config = None 6 | 7 | 8 | def getConfig(): 9 | global _config 10 | if _config is None: 11 | with open("config.json") as f: 12 | _config = json.load(f) 13 | return _config 14 | 15 | 16 | class Utility: 17 | @staticmethod 18 | def getConfig(): 19 | return getConfig() 20 | 21 | @staticmethod 22 | def saveLyrics(synced_lyric, plain_lyric, title, artist, album, trackNo, year): 23 | config = getConfig() 24 | lyric_path = config['lyric_file_path'].format( 25 | title=title, artist=artist, album=album, trackNo=str(trackNo), year=year 26 | ) 27 | 28 | # Create directory if it doesn't exist 29 | Path(lyric_path).parent.mkdir(parents=True, exist_ok=True) 30 | 31 | # Save synced lyrics 32 | if config['synced_lyrics'] and synced_lyric: 33 | with open(f"{lyric_path}.lrc", "w", encoding='utf-8') as f: 34 | f.write(synced_lyric) 35 | 36 | # Save plain lyrics 37 | if config['plain_lyrics'] and plain_lyric: 38 | with open(f"{lyric_path}.txt", "w", encoding='utf-8') as f: 39 | f.write(plain_lyric) 40 | -------------------------------------------------------------------------------- /flyrics.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import importlib 4 | 5 | from modules.deezer import Deezer 6 | from modules.spotify import Spotify 7 | from modules.tidal import Tidal 8 | from modules.applemusic import AppleMusic 9 | from modules.jiosaavn import Jiosaavn 10 | 11 | from modules.module import Module 12 | from utility import Utility 13 | 14 | banner = """ 15 | /$$$$$$$$ /$$ /$$ 16 | | $$_____/ | $$ |__/ 17 | | $$ | $$ /$$ /$$ /$$$$$$ /$$ /$$$$$$$ /$$$$$$$ 18 | | $$$$$ /$$$$$$| $$ | $$ | $$ /$$__ $$| $$ /$$_____/ /$$_____/ 19 | | $$__/ |______/| $$ | $$ | $$| $$ \__/| $$| $$ | $$$$$$ 20 | | $$ | $$ | $$ | $$| $$ | $$| $$ \____ $$ 21 | | $$ | $$$$$$$$| $$$$$$$| $$ | $$| $$$$$$$ /$$$$$$$/ 22 | |__/ |________/ \____ $$|__/ |__/ \_______/|_______/ 23 | /$$ | $$ 24 | | $$$$$$/ 25 | \______/ 26 | 27 | A Modular lyrics fetcher 28 | --by @bunnykek""" 29 | 30 | 31 | class Flyrics: 32 | 33 | """ 34 | from flyrics import Flyrics\n 35 | lyric = Flyrics()\n 36 | lyricsList = lyric.fetch("https://open.spotify.com/album/xxxxxxxxxx") 37 | """ 38 | 39 | def __init__(self): 40 | self.modules: list[type[Module]] = [ 41 | Deezer, Spotify, Tidal, AppleMusic, Jiosaavn] 42 | 43 | def fetch(self, url, api=True): 44 | for module in self.modules: 45 | result = module.REGEX.search(url) 46 | if result: 47 | instance = module(url, Utility.getConfig(), api=api) 48 | return instance.getResponse() 49 | 50 | 51 | if __name__ == "__main__": 52 | print(banner) 53 | parser = argparse.ArgumentParser(description="Modular lyrics fetcher") 54 | parser.add_argument('URL', help="Album or track URL") 55 | args = parser.parse_args() 56 | url = args.URL 57 | 58 | flyricsInstance = Flyrics() 59 | flyricsInstance.fetch(url, api=False) 60 | -------------------------------------------------------------------------------- /modules/tidal.py: -------------------------------------------------------------------------------- 1 | # made by @bunnykek 2 | 3 | from modulefinder import Module 4 | import re 5 | import requests 6 | from sanitize_filename import sanitize 7 | from utility import Utility 8 | 9 | 10 | class Tidal(Module): 11 | NAME = "Tidal" 12 | REGEX = re.compile(r"https:\/\/tidal\.com\/(track|album)\/(\d+)") 13 | 14 | def __init__(self, url: str, config: dict, api: bool): 15 | self.session = requests.session() 16 | 17 | HEADERS = { 18 | 'authorization': config['tidal']['auth_bearer'], 19 | } 20 | self.session.headers.update(HEADERS) 21 | kind, id_ = Tidal.REGEX.search(url).groups() 22 | 23 | if kind == 'track': 24 | self.jsonResponse = [self.getTrackLyrics(id_, dict(), api)] 25 | 26 | else: 27 | self.jsonResponse = self.getAlbumLyrics(id_, dict(), api) 28 | 29 | def getResponse(self): 30 | return self.jsonResponse 31 | 32 | def getAlbumLyrics(self, albumid: str, data: dict, api: bool): 33 | params = { 34 | 'albumId': albumid, 35 | 'countryCode': 'US', 36 | 'locale': 'en_US', 37 | 'deviceType': 'BROWSER', 38 | } 39 | response = self.session.get( 40 | 'https://tidal.com/v1/pages/album', params=params) 41 | 42 | metadata = response.json() 43 | 44 | # print(response.text) 45 | 46 | lyricsJson = list() 47 | for track in metadata['rows'][1]['modules'][0]['pagedList']['items']: 48 | trackid = track['item']['id'] 49 | trackJson = self.getTrackLyrics(trackid, data, api) 50 | lyricsJson.append(trackJson) 51 | 52 | return lyricsJson 53 | 54 | def getTrackLyrics(self, trackid: str, data: dict, api: bool): 55 | params = { 56 | 'countryCode': 'US', 57 | 'locale': 'en_US', 58 | 'deviceType': 'BROWSER', 59 | } 60 | 61 | response = self.session.get( 62 | f'https://listen.tidal.com/v1/tracks/{trackid}', params=params) 63 | metadata = response.json() 64 | 65 | title = sanitize(metadata['title']) 66 | trackNo = metadata['trackNumber'] 67 | artist = sanitize(metadata['artist']['name']) 68 | album = sanitize(metadata['album']['title']) 69 | year = metadata['streamStartDate'][0:4] 70 | 71 | lyric_response = self.session.get( 72 | f'https://listen.tidal.com/v1/tracks/{trackid}/lyrics', 73 | params=params, 74 | ).json() 75 | 76 | plain_lyric = f"Title: {title}\nAlbum: {album}\nArtist: {artist}\n\n" 77 | synced_lyric = f"[ti:{title}]\n[ar:{artist}]\n[al:{album}]\n\n" 78 | 79 | plain_lyric += lyric_response.get('lyrics') 80 | synced_lyric += lyric_response.get('subtitles') 81 | 82 | if not api: 83 | Utility.saveLyrics(synced_lyric, plain_lyric, title, 84 | artist, album, trackNo, year) 85 | 86 | return { 87 | 'synced': synced_lyric, 88 | 'plain': plain_lyric 89 | } 90 | -------------------------------------------------------------------------------- /modules/jiosaavn.py: -------------------------------------------------------------------------------- 1 | # made by @bunnykek 2 | 3 | import re 4 | import requests 5 | from sanitize_filename import sanitize 6 | from utility import Utility 7 | from modules.module import Module 8 | 9 | 10 | class Jiosaavn(Module): 11 | NAME = "Jiosaavn" 12 | REGEX = re.compile(r"https://www\.jiosaavn\.com/(album|song)/.+?/(.+)") 13 | CLEANR = re.compile('<.*?>') 14 | 15 | def __init__(self, url: str, config: dict, api: bool): 16 | self.session = requests.session() 17 | kind, id_ = Jiosaavn.REGEX.search(url).groups() 18 | 19 | if kind == 'song': 20 | self.jsonResponse = [self.getTrackLyrics(id_, dict(), api)] 21 | 22 | else: 23 | self.jsonResponse = self.getAlbumLyrics(id_, dict(), api) 24 | 25 | def __cleanhtml(self, raw_html): 26 | cleantext = re.sub(Jiosaavn.CLEANR, '', raw_html) 27 | return cleantext 28 | 29 | def getResponse(self): 30 | return self.jsonResponse 31 | 32 | def getAlbumLyrics(self, albumid: str, data: dict, api: bool): 33 | response = self.session.get( 34 | f'https://www.jiosaavn.com/api.php?__call=webapi.get&token={albumid}&type=album&_format=json') 35 | metadata = response.json() 36 | album_artist = metadata['primary_artists'] 37 | lyricsJson = list() 38 | for i, track in enumerate(metadata['songs']): 39 | trackid = Jiosaavn.REGEX.search(track['perma_url']).group(2) 40 | data = { 41 | "trackNo": i+1, 42 | "albumArtist": album_artist, 43 | "trackjson": track 44 | } 45 | trackJson = self.getTrackLyrics(trackid, data, api) 46 | lyricsJson.append(trackJson) 47 | 48 | return lyricsJson 49 | 50 | def getTrackLyrics(self, trackid: str, data: dict, api: bool): 51 | trackjson = data.get("trackjson", None) 52 | if not trackjson: 53 | trackjson = self.session.get( 54 | f"https://www.jiosaavn.com/api.php?__call=webapi.get&token={trackid}&type=song&_format=json").json() 55 | trackjson = trackjson[f"{list(trackjson.keys())[0]}"] 56 | artist = sanitize(trackjson['primary_artists']) 57 | else: 58 | artist = sanitize( 59 | data.get("albumArtist", trackjson['primary_artists'])) 60 | 61 | year = trackjson['year'] 62 | title = sanitize(trackjson['song']) 63 | album = sanitize(trackjson['album']) 64 | 65 | lyric_response = self.session.get( 66 | "https://www.jiosaavn.com/api.php?__call=lyrics.getLyrics&ctx=web6dot0&api_version=4&_format=json&_marker=0%3F_marker%3D0&lyrics_id=" + trackjson['id']).json() 67 | # print(lyric_response) 68 | 69 | plain_lyric = f"Title: {title}\nAlbum: {album}\nArtist: {artist}\n\n" 70 | synced_lyric = f"[ti:{title}]\n[ar:{artist}]\n[al:{album}]\n\n" 71 | 72 | try: 73 | lyrics = lyric_response.get("lyrics") 74 | if lyrics is not None: 75 | lyrics = lyrics.replace('
', '\n') 76 | lyrics = self.__cleanhtml(lyrics) 77 | # print(lyric_response) 78 | 79 | plain_lyric += lyrics 80 | except: 81 | pass 82 | 83 | if not api: 84 | Utility.saveLyrics(synced_lyric, plain_lyric, title, 85 | artist, album, data.get("trackNo", 1), year) 86 | 87 | return { 88 | 'synced': synced_lyric, 89 | 'plain': plain_lyric 90 | } 91 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | config.json 162 | test.json 163 | -------------------------------------------------------------------------------- /modules/spotify.py: -------------------------------------------------------------------------------- 1 | # made by @bunnykek 2 | 3 | import datetime 4 | import json 5 | import re 6 | import requests 7 | from sanitize_filename import sanitize 8 | from modules.module import Module 9 | from utility import Utility 10 | 11 | 12 | class Spotify(Module): 13 | NAME = "Spotify" 14 | REGEX = re.compile( 15 | r"https:\/\/open\.spotify\.com\/(track|album)\/([\d|\w]+)") 16 | 17 | def __init__(self, url: str, config: dict, api: bool): 18 | self.session = requests.session() 19 | self.headers = { 20 | 'sec-ch-ua': '"Microsoft Edge";v="123", "Not:A-Brand";v="8", "Chromium";v="123"', 21 | 'DNT': '1', 22 | 'accept-language': 'en-GB', 23 | 'sec-ch-ua-mobile': '?0', 24 | 'app-platform': 'WebPlayer', 25 | 'authorization': config['spotify']['auth_bearer'], 26 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0', 27 | 'accept': 'application/json', 28 | 'Referer': 'https://open.spotify.com/', 29 | 'spotify-app-version': '1.2.79.37.g2c2d65c8', 30 | 'sec-ch-ua-platform': '"Windows"', 31 | } 32 | self.session.headers.update(self.headers) 33 | kind, id_ = Spotify.REGEX.search(url).groups() 34 | 35 | # tokencheckupdate() 36 | 37 | if kind == 'track': 38 | self.jsonResponse = [self.getTrackLyrics(id_, dict(), api)] 39 | 40 | else: 41 | self.jsonResponse = self.getAlbumLyrics(id_, dict(), api) 42 | 43 | def getResponse(self): 44 | return self.jsonResponse 45 | 46 | def getAlbumLyrics(self, albumid: str, data: dict, api: bool): 47 | response = self.session.get( 48 | f'https://api.spotify.com/v1/albums?ids={albumid}&market=from_token') 49 | 50 | # print(response.text) 51 | metadata = response.json()['albums'][0] 52 | 53 | lyricsJson = list() 54 | for track in metadata['tracks']['items']: 55 | trackid = track['id'] 56 | trackJson = self.getTrackLyrics(trackid, data, api) 57 | lyricsJson.append(trackJson) 58 | 59 | return lyricsJson 60 | 61 | def getTrackLyrics(self, trackid: str, data: dict, api: bool): 62 | response = self.session.get( 63 | f'https://api.spotify.com/v1/tracks?ids={trackid}&market=from_token') 64 | metadata = response.json()['tracks'][0] 65 | 66 | title = sanitize(metadata['name']) 67 | trackNo = metadata['track_number'] 68 | artworkcode = metadata['album']['images'][0]['url'].split("/")[-1] 69 | 70 | artist = sanitize(metadata['album']['artists'][0]['name']) 71 | album = sanitize(metadata['album']['name']) 72 | year = metadata['album']['release_date'][0:4] 73 | 74 | params = { 75 | 'format': 'json', 76 | 'vocalRemoval': 'false', 77 | 'market': 'from_token', 78 | } 79 | 80 | response = self.session.get( 81 | f'https://spclient.wg.spotify.com/color-lyrics/v2/track/{trackid}/image/https%3A%2F%2Fi.scdn.co%2Fimage%2F{artworkcode}', 82 | params=params, 83 | ) 84 | 85 | plain_lyric = f"Title: {title}\nAlbum: {album}\nArtist: {artist}\n\n" 86 | synced_lyric = f"[ti:{title}]\n[ar:{artist}]\n[al:{album}]\n\n" 87 | 88 | lines = response.json()['lyrics']['lines'] 89 | for line in lines: 90 | startms = int(line['startTimeMs']) 91 | words = line['words'] 92 | timeStamp = self.__convert_milliseconds(startms) 93 | plain_lyric += words+'\n' 94 | synced_lyric += f'[{timeStamp}]{words}\n' 95 | 96 | if not api: 97 | Utility.saveLyrics(synced_lyric, plain_lyric, title, 98 | artist, album, trackNo, year) 99 | 100 | return { 101 | 'synced': synced_lyric, 102 | 'plain': plain_lyric 103 | } 104 | 105 | def __convert_milliseconds(self, milliseconds): 106 | # Create a timedelta object with the provided milliseconds 107 | delta = datetime.timedelta(milliseconds=milliseconds) 108 | 109 | # Extract the hours, minutes, seconds, and milliseconds from the timedelta 110 | hours = delta.seconds // 3600 111 | minutes = (delta.seconds // 60) % 60 112 | seconds = delta.seconds % 60 113 | milliseconds = delta.microseconds // 1000 114 | 115 | # Format the hours, minutes, seconds, and milliseconds into the desired format 116 | formatted_time = "{:02d}:{:02d}.{:03d}".format( 117 | minutes, seconds, milliseconds) 118 | 119 | return formatted_time 120 | -------------------------------------------------------------------------------- /modules/deezer.py: -------------------------------------------------------------------------------- 1 | # made by @bunnykek 2 | 3 | import re 4 | import requests 5 | from sanitize_filename import sanitize 6 | from utility import Utility 7 | from modules.module import Module 8 | 9 | 10 | class Deezer(Module): 11 | NAME = "Deezer" 12 | REGEX = re.compile(r"https:\/\/www\.deezer\.com\/(track|album)\/(\d+)") 13 | 14 | def __init__(self, url, config: dict, api=False): 15 | self.session = requests.session() 16 | self.cookies = { 17 | 'arl': config['deezer']['arl'] 18 | } 19 | self.session.cookies.update(self.cookies) 20 | self.__updatebearer() 21 | kind, id_ = Deezer.REGEX.search(url).groups() 22 | 23 | if kind == 'track': 24 | self.jsonResponse = [self.getTrackLyrics(id_, dict(), api)] 25 | 26 | else: 27 | self.jsonResponse = self.getAlbumLyrics(id_, dict(), api) 28 | 29 | def __updatebearer(self): 30 | params = { 31 | 'jo': 'p', 32 | 'rto': 'c', 33 | 'i': 'c', 34 | } 35 | response = self.session.post( 36 | 'https://auth.deezer.com/login/arl', params=params).json() 37 | self.session.headers.update( 38 | {'authorization': 'Bearer '+response['jwt']}) 39 | 40 | def getResponse(self): 41 | return self.jsonResponse 42 | 43 | def getAlbumLyrics(self, albumid, data: dict, api): 44 | response = self.session.get(f'https://api.deezer.com/album/{albumid}') 45 | metadata = response.json() 46 | year = metadata['release_date'][0:4] 47 | lyricsJson = list() 48 | for i, track in enumerate(metadata['tracks']['data']): 49 | trackid = track['id'] 50 | data = { 51 | "trackNo": i+1, 52 | "year": year, 53 | "trackjson": track 54 | } 55 | trackJson = self.getTrackLyrics(trackid, data, api) 56 | lyricsJson.append(trackJson) 57 | 58 | return lyricsJson 59 | 60 | def getTrackLyrics(self, trackid, data: dict, api): 61 | if not "trackjson" in data: 62 | data["trackjson"] = self.session.get( 63 | f"https://api.deezer.com/track/{trackid}").json() 64 | year = data["trackjson"]['release_date'][0:4] 65 | 66 | title = sanitize(data["trackjson"]['title']) 67 | artist = sanitize(data["trackjson"]['name']) 68 | album = sanitize(data["trackjson"]['title']) 69 | 70 | json_data = { 71 | 'operationName': 'SynchronizedTrackLyrics', 72 | 'variables': { 73 | 'trackId': str(trackid), 74 | }, 75 | 'query': 'query SynchronizedTrackLyrics($trackId: String!) {\n track(trackId: $trackId) {\n ...SynchronizedTrackLyrics\n __typename\n }\n}\n\nfragment SynchronizedTrackLyrics on Track {\n id\n lyrics {\n ...Lyrics\n __typename\n }\n album {\n cover {\n small: urls(pictureRequest: {width: 100, height: 100})\n medium: urls(pictureRequest: {width: 264, height: 264})\n large: urls(pictureRequest: {width: 800, height: 800})\n explicitStatus\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment Lyrics on Lyrics {\n id\n copyright\n text\n writers\n synchronizedLines {\n ...LyricsSynchronizedLines\n __typename\n }\n __typename\n}\n\nfragment LyricsSynchronizedLines on LyricsSynchronizedLine {\n lrcTimestamp\n line\n lineTranslated\n milliseconds\n duration\n __typename\n}', 76 | } 77 | 78 | lyric_response = self.session.post( 79 | 'https://pipe.deezer.com/api', json=json_data).json() 80 | # print(lyric_response) 81 | 82 | plain_lyric = f"Title: {title}\nAlbum: {album}\nArtist: {artist}\n" 83 | synced_lyric = f"[ti:{title}]\n[ar:{artist}]\n[al:{album}]\n\n" 84 | 85 | try: 86 | plain_lyric += lyric_response.get('data').get( 87 | 'track').get('lyrics').get('text')+"\n" 88 | 89 | for line in lyric_response.get('data').get('track').get('lyrics').get('synchronizedLines'): 90 | synced_lyric += f"{line['lrcTimestamp']} {line['line']}\n" 91 | 92 | plain_lyric += "\nWriters: " + lyric_response.get('data').get('track').get('lyrics').get( 93 | 'writers') + "\n" + "Copyright: " + lyric_response.get('data').get('track').get('lyrics').get('copyright') 94 | synced_lyric += "\nWriters: " + lyric_response.get('data').get('track').get('lyrics').get( 95 | 'writers') + "\n" + "Copyright: " + lyric_response.get('data').get('track').get('lyrics').get('copyright') 96 | except: 97 | pass 98 | 99 | if not api: 100 | Utility.saveLyrics(synced_lyric, plain_lyric, title, 101 | artist, album, data.get("trackNo", 1), year) 102 | 103 | return { 104 | 'synced': synced_lyric, 105 | 'plain': plain_lyric 106 | } 107 | -------------------------------------------------------------------------------- /modules/applemusic.py: -------------------------------------------------------------------------------- 1 | # made by @bunnykek 2 | 3 | import requests 4 | import bs4 5 | import re 6 | from sanitize_filename import sanitize 7 | from modules.module import Module 8 | from utility import Utility 9 | 10 | 11 | class AppleMusic(Module): 12 | NAME = "Apple Music" 13 | REGEX = re.compile( 14 | r"https://music.apple.com/(\w{2})/(album|song)/.+?/(\d+)(\?i=(\d+))?") 15 | 16 | def __init__(self, url: str, config: dict, api: bool) -> None: 17 | self.headers = { 18 | "authorization": config['applemusic']['auth_bearer'], 19 | "media-user-token": config['applemusic']['media-user-token'], 20 | "Origin": "https://music.apple.com", 21 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0", 22 | "Accept": "application/json", 23 | "Accept-Language": "en-US,en;q=0.5", 24 | "Accept-Encoding": "gzip, deflate, br", 25 | "Referer": "https://music.apple.com/", 26 | "content-type": "application/json", 27 | "x-apple-renewal": "true", 28 | "DNT": "1", 29 | "Connection": "keep-alive", 30 | 'l': 'en-US' 31 | } 32 | self.session = requests.Session() 33 | self.session.headers.update(self.headers) 34 | 35 | region, kind, albumid, trackFlag, trackid = AppleMusic.REGEX.search( 36 | url).groups() 37 | 38 | data = { 39 | 'region': region, 40 | 'albumid': albumid, 41 | 'trackFlag': trackFlag, 42 | 'trackid': trackid 43 | } 44 | 45 | if trackFlag: 46 | self.jsonResponse = [self.getTrackLyrics(trackid, data, api)] 47 | 48 | elif kind == 'song': 49 | self.jsonResponse = [self.getTrackLyrics(albumid, data, api)] 50 | 51 | else: 52 | self.jsonResponse = self.getAlbumLyrics(albumid, data, api) 53 | 54 | def getResponse(self) -> None: 55 | return self.jsonResponse 56 | 57 | def getAlbumLyrics(self, albumid: str, data: dict, api: bool) -> None: 58 | print("Getting lyrics for the whole album...") 59 | metadata = self.session.get( 60 | f"https://api.music.apple.com/v1/catalog/{data["region"]}/albums/{albumid}").json() 61 | metadata = metadata['data'][0] 62 | data["albumArtist"] = metadata['attributes']['artistName'] 63 | 64 | lyricsJson = list() 65 | for track in metadata['relationships']['tracks']['data']: 66 | trackJson = self.getTrackLyrics(track['id'], data, api) 67 | lyricsJson.append(trackJson) 68 | 69 | return lyricsJson 70 | 71 | def getTrackLyrics(self, trackid: str, data: dict, api: bool) -> None: 72 | metadata = self.session.get( 73 | f"https://api.music.apple.com/v1/catalog/{data["region"]}/songs/{trackid}").json() 74 | metadata = metadata['data'][0] 75 | 76 | title = sanitize(metadata['attributes']['name']) 77 | trackNo = metadata['attributes']['trackNumber'] 78 | 79 | artist = sanitize(metadata['attributes']['artistName']) 80 | album = sanitize(metadata['attributes']['albumName']) 81 | year = metadata['attributes'].get('releaseDate') 82 | 83 | print(f"{trackNo}. {title}...") 84 | # print(json.dumps(metadata, indent=2)) 85 | if not metadata['attributes']['hasLyrics']: 86 | print("Lyrics not available.") 87 | return 88 | 89 | response = self.session.get( 90 | f'https://amp-api.music.apple.com/v1/catalog/{data["region"]}/songs/{trackid}/lyrics') 91 | result = response.json() 92 | soup = bs4.BeautifulSoup( 93 | result['data'][0]['attributes']['ttml'], 'lxml') 94 | 95 | plain_lyric = f"Title: {title}\nAlbum: {album}\nArtist: {artist}\n\n" 96 | synced_lyric = f"[ti:{title}]\n[ar:{artist}]\n[al:{album}]\n\n" 97 | paragraphs = soup.find_all("p") 98 | 99 | if 'itunes:timing="None"' in result['data'][0]['attributes']['ttml']: 100 | synced_lyric = None 101 | for line in paragraphs: 102 | plain_lyric += line.text+'\n' 103 | 104 | else: 105 | for paragraph in paragraphs: 106 | begin = paragraph.get('begin') 107 | if begin.count(':') > 1: 108 | timeStamp = ':'.join(begin.split(':')[1:]) 109 | else: 110 | timeStamp = self.__convertTimeSec( 111 | begin) if 's' in begin else self.__convertTimeFormat(begin) 112 | text = paragraph.text 113 | plain_lyric += text+'\n' 114 | synced_lyric += f'[{timeStamp}]{text}\n' 115 | 116 | if not api: 117 | Utility.saveLyrics(synced_lyric, plain_lyric, title, data.get( 118 | "albumArtist", artist), album, trackNo, year[0:4] if year else 'NaN') 119 | 120 | return { 121 | 'synced': synced_lyric, 122 | 'plain': plain_lyric 123 | } 124 | 125 | def __convertTimeSec(self, original_time): 126 | # Extract the seconds from the original time 127 | seconds = float(original_time.split('s')[0]) 128 | 129 | # Convert seconds to minutes and remaining seconds 130 | minutes = int(seconds // 60) 131 | remaining_seconds = seconds % 60 132 | 133 | # Format minutes and remaining seconds into two-digit strings 134 | formatted_minutes = "{:02d}".format(minutes) 135 | formatted_seconds = "{:02.3f}".format(remaining_seconds) 136 | 137 | # Combine the formatted components into the desired format 138 | formatted_time = "{}:{}".format( 139 | formatted_minutes, formatted_seconds[:5]) 140 | 141 | return formatted_time 142 | 143 | def __convertTimeFormat(self, original_time): 144 | # Check if the time is in the format "X:XX.XXX" 145 | if ":" in original_time: 146 | # Split the original time into minutes, seconds, and milliseconds 147 | minutes, seconds = map(float, original_time.split(':')) 148 | minutes = minutes + (seconds // 60) # Add minutes from seconds 149 | seconds = seconds % 60 150 | 151 | # Convert the time to milliseconds 152 | milliseconds = (minutes * 60 * 1000) + (seconds * 1000) 153 | 154 | else: 155 | # Extract the milliseconds from the time 156 | milliseconds = int(float(original_time) * 1000) 157 | 158 | # Convert the milliseconds back to hours, minutes, seconds, and milliseconds 159 | hours = milliseconds // (60 * 60 * 1000) 160 | minutes = (milliseconds // (60 * 1000)) % 60 161 | seconds = (milliseconds // 1000) % 60 162 | milliseconds = milliseconds % 1000 163 | 164 | # Format the hours, minutes, seconds, and milliseconds into the desired format 165 | formatted_time = "{:02d}:{:02d}.{:03d}".format( 166 | int(minutes), int(seconds), int(milliseconds)) 167 | 168 | return formatted_time 169 | --------------------------------------------------------------------------------