├── requirements.txt ├── static ├── img │ ├── syrics.png │ ├── syrics16.png │ ├── syrics32.png │ └── syrics96.png ├── js │ ├── script.js │ └── spotify.js └── css │ └── style.css ├── vercel.json ├── README.md ├── templates ├── index.html ├── base.html └── spotify.html ├── index.py ├── .gitignore └── spotify.py /requirements.txt: -------------------------------------------------------------------------------- 1 | flask==2.2.2 2 | Werkzeug==2.2.2 3 | spotipy==2.22.1 4 | -------------------------------------------------------------------------------- /static/img/syrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akashrchandran/syrics-web/HEAD/static/img/syrics.png -------------------------------------------------------------------------------- /static/img/syrics16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akashrchandran/syrics-web/HEAD/static/img/syrics16.png -------------------------------------------------------------------------------- /static/img/syrics32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akashrchandran/syrics-web/HEAD/static/img/syrics32.png -------------------------------------------------------------------------------- /static/img/syrics96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akashrchandran/syrics-web/HEAD/static/img/syrics96.png -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "index.py", 6 | "use": "@vercel/python" 7 | } 8 | ], 9 | "routes": [ 10 | { 11 | "src": "/(.*)", 12 | "dest": "index.py" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![syrics image](https://ik.imagekit.io/gyzvlawdz/Projects/syrics/Black_Modern_Business_Logo__600___500_px___2240___1260_px__cYRO9HGTQ.png) 3 |
4 | A web application to download synced spotify lyrics in LRC format. 5 |
6 | 7 | *** 8 | 9 | # Live Deployment 10 | You can find site live deployed at https://syrics-web.vercel.app/ 11 | 12 | *** 13 | 14 | 15 | # Example 16 | ![msedge_jVd1HDSu2S](https://user-images.githubusercontent.com/78685510/218275201-2398b823-5228-4a11-abb8-a615ec6a14e1.gif) 17 | 18 | # Credits 19 | • [Me](https://akashrchandran.in) 20 | -> For everything. 21 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block body %} 3 | 18 | 21 | {% endblock %} 22 | 23 | {% block javascript %} 24 | {% if error %} 25 | 28 | {% endif %} 29 | {% endblock %} -------------------------------------------------------------------------------- /index.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify, render_template, request 2 | from spotify import ( 3 | get_album, 4 | get_all_trackids, 5 | get_track, 6 | get_play, 7 | check_regex, 8 | query_spotify, 9 | format_duration, 10 | ) 11 | 12 | app = Flask(__name__) 13 | 14 | 15 | @app.route("/") 16 | def index(): 17 | return render_template("index.html") 18 | 19 | 20 | @app.route("/spotify", methods=["POST"]) 21 | def download(): 22 | if not request.form: 23 | return "No arguments provided" 24 | url_type, id = check_regex(request.form.get("url")) 25 | if url_type == "album": 26 | return render_template("spotify.html", data=get_album(id), types="album") 27 | elif url_type == "track": 28 | return render_template("spotify.html", data=get_track(id), types="track") 29 | elif url_type == "playlist": 30 | return render_template("spotify.html", data=get_play(id), types="playlist") 31 | else: 32 | return ( 33 | render_template( 34 | "index.html", error="Invalid URL...Please check the URL and try again" 35 | ), 36 | 400, 37 | ) 38 | 39 | 40 | @app.route("/api/search") 41 | def api(): 42 | q = request.args.get("q") 43 | return query_spotify(q) if q else "No arguments provided" 44 | 45 | 46 | @app.get("/api/getalltracks") 47 | def get_all_tracks(): 48 | album_id = request.args.get("id") 49 | album = bool(request.args.get("album")) 50 | if album_id: 51 | return jsonify(get_all_trackids(album_id, album)) 52 | else: 53 | return "No arguments provided", 400 54 | 55 | @app.get("/api/tracks/") 56 | def track_details(track_id: str): 57 | if track_id: 58 | try: 59 | return jsonify(get_track(track_id)) 60 | except Exception as e: 61 | return "Invalid Track ID", 400 62 | else: 63 | return "No arguments provided", 400 64 | 65 | app.add_template_filter(format_duration) 66 | if __name__ == "__main__": 67 | app.run(debug=True, host="0.0.0.0", port=5000) 68 | -------------------------------------------------------------------------------- /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 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 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | #custom 132 | test.py -------------------------------------------------------------------------------- /static/js/script.js: -------------------------------------------------------------------------------- 1 | const toastmessage = document.getElementById('liveToast'); 2 | const toastbody = document.getElementsByClassName('toast-body'); 3 | let savedSettings = JSON.parse(localStorage.getItem('lyricsSettings')); 4 | 5 | const defaultSettings = { 6 | lyricsType: 'lrc', 7 | fileNameFormat: ["{track_number}", ". ", "{track_name}"] 8 | }; 9 | 10 | if (!savedSettings) { 11 | localStorage.setItem('lyricsSettings', JSON.stringify(defaultSettings)); 12 | savedSettings = defaultSettings; 13 | } 14 | 15 | showToast = (msg) => { 16 | const toast = new bootstrap.Toast(toastmessage) 17 | toastbody[0].textContent = msg; 18 | toast.show() 19 | } 20 | document.addEventListener('DOMContentLoaded', function () { 21 | const saveSettingsBtn = document.getElementById('saveSettings'); 22 | const settingsModal = new bootstrap.Modal(document.getElementById('settingsModal')); 23 | const fileNameFormat = document.getElementById('fileNameFormat'); 24 | 25 | const tagify = new Tagify(fileNameFormat, { 26 | whitelist: ["{track_name}", "{track_number}", "{track_album}", "{track_id}", "{track_artist}", "{track_explicit}", "{track_release_date}", "{track_popularity}", "{track_duration}"], 27 | dropdown: { 28 | enabled: 0, 29 | maxItems: 10, 30 | classname: "tags-look", 31 | fuzzySearch: false, 32 | closeOnSelect: false 33 | }, 34 | pattern: "^[a-zA-Z0-9_\-\.{} ]+$", 35 | trim: false 36 | }); 37 | var dragsort = new DragSort(tagify.DOM.scope, { 38 | selector: '.' + tagify.settings.classNames.tag, 39 | callbacks: { 40 | dragEnd: onDragEnd 41 | } 42 | }) 43 | 44 | // must update Tagify's value according to the re-ordered nodes in the DOM 45 | function onDragEnd(elm) { 46 | tagify.updateValueByDOMTags() 47 | } 48 | 49 | // Load settings from local storage 50 | document.getElementById('lyricsType').value = savedSettings.lyricsType; 51 | tagify.addTags(savedSettings.fileNameFormat); 52 | 53 | saveSettingsBtn.addEventListener('click', function () { 54 | const settingsForm = document.getElementById('settingsForm'); 55 | const formData = new FormData(settingsForm); 56 | const lyricsType = formData.get('lyricsType'); 57 | 58 | // Get the file name format 59 | const fileNameFormatValues = tagify.value.map(tag => tag.value); 60 | 61 | // Save settings to local storage 62 | const settings = { 63 | lyricsType: lyricsType, 64 | fileNameFormat: fileNameFormatValues 65 | }; 66 | localStorage.setItem('lyricsSettings', JSON.stringify(settings)); 67 | 68 | // Here you can handle the selected lyrics type and file name format 69 | console.log('Selected lyrics type:', lyricsType); 70 | console.log('File name format:', fileNameFormatValues); 71 | 72 | // Close the modal 73 | console.log('Settings saved successfully!'); 74 | settingsModal.hide(); 75 | 76 | // Show success message 77 | showToast("Settings saved successfully!"); 78 | }); 79 | }); -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-color: #30E368; 3 | --primary-dark: #27B653; 4 | --primary-light: #7FFAA7; 5 | --text-color: #333333; 6 | --background-color: #F0FFF4; 7 | } 8 | body { 9 | background: #1d976c; 10 | /* fallback for old browsers */ 11 | background: -webkit-linear-gradient(230deg, rgb(75, 207, 147), rgb(75, 121, 207), rgb(162, 75, 207)) 0% 0% / 300% 300%; 12 | /* Chrome 10-25, Safari 5.1-6 */ 13 | background: linear-gradient(230deg, rgb(75, 207, 147), rgb(75, 121, 207), rgb(162, 75, 207)) 0% 0% / 300% 300%; 14 | /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ 15 | -webkit-animation: gradientAnimate 60s ease infinite; 16 | -moz-animation: gradientAnimate 60s ease infinite; 17 | -o-animation: gradientAnimate 60s ease infinite; 18 | animation: gradientAnimate 60s ease infinite; 19 | /* font-family: Source Sans Pro, sans-serif; */ 20 | width: 100%; 21 | height: 100%; 22 | } 23 | 24 | .container { 25 | width: 100%; 26 | height: 100%; 27 | } 28 | .searchbar { 29 | width: 100vw; 30 | height: 100vh; 31 | } 32 | 33 | .syrics { 34 | height: 500px; 35 | width: 500px; 36 | margin-top: 100px; 37 | } 38 | 39 | footer { 40 | position: fixed; 41 | margin-bottom: 10px; 42 | } 43 | 44 | footer a { 45 | text-decoration: none; 46 | color: #30E368; 47 | } 48 | 49 | .settings-icon { 50 | position: fixed; 51 | bottom: 20px; 52 | right: 20px; 53 | z-index: 1000; 54 | background-color: #30E368; 55 | color: white; 56 | border-radius: 50%; 57 | width: 50px; 58 | height: 50px; 59 | display: flex; 60 | align-items: center; 61 | justify-content: center; 62 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); 63 | transition: background-color 0.3s, transform 0.3s; 64 | cursor: pointer; 65 | } 66 | 67 | .settings-icon a { 68 | color: #ffffff; 69 | text-decoration: none; 70 | font-size: 24px; 71 | } 72 | 73 | .settings-icon:hover { 74 | background-color: #28c35a; 75 | transform: scale(1.1); 76 | } 77 | .btn-primary { 78 | background-color: var(--primary-color); 79 | border-color: var(--primary-color); 80 | color: white; 81 | font-weight: 600; 82 | padding: 12px 24px; 83 | border-radius: 30px; 84 | text-transform: uppercase; 85 | letter-spacing: 1px; 86 | transition: all 0.3s ease; 87 | } 88 | .btn-primary:hover, .btn-primary:focus { 89 | background-color: var(--primary-dark); 90 | border-color: var(--primary-dark); 91 | transform: translateY(-2px); 92 | box-shadow: 0 4px 15px rgba(48, 227, 104, 0.4); 93 | } 94 | .modal-content { 95 | border-radius: 20px; 96 | border: none; 97 | box-shadow: 0 15px 35px rgba(48, 227, 104, 0.2); 98 | } 99 | .modal-header { 100 | background-color: var(--primary-color); 101 | color: white; 102 | border-top-left-radius: 20px; 103 | border-top-right-radius: 20px; 104 | border-bottom: none; 105 | padding: 20px 30px; 106 | } 107 | .modal-title { 108 | font-weight: 600; 109 | font-size: 1.5rem; 110 | } 111 | .modal-body { 112 | padding: 30px; 113 | background-color: white; 114 | } 115 | .modal-footer { 116 | background-color: white; 117 | border-top: none; 118 | border-bottom-left-radius: 20px; 119 | border-bottom-right-radius: 20px; 120 | padding: 20px 30px; 121 | } 122 | .form-label { 123 | font-weight: 600; 124 | color: var(--text-color); 125 | margin-bottom: 10px; 126 | } 127 | .form-select, .form-control { 128 | border: 2px solid var(--primary-light); 129 | border-radius: 15px; 130 | padding: 12px; 131 | font-size: 1rem; 132 | transition: all 0.3s ease; 133 | } 134 | .form-select:focus, .form-control:focus { 135 | border-color: var(--primary-color); 136 | box-shadow: 0 0 0 0.25rem rgba(48, 227, 104, 0.25); 137 | } 138 | .btn-secondary { 139 | background-color: #f0f0f0; 140 | border-color: #f0f0f0; 141 | color: var(--text-color); 142 | font-weight: 600; 143 | padding: 12px 24px; 144 | border-radius: 30px; 145 | text-transform: uppercase; 146 | letter-spacing: 1px; 147 | transition: all 0.3s ease; 148 | } 149 | .btn-secondary:hover, .btn-secondary:focus { 150 | background-color: #e0e0e0; 151 | border-color: #e0e0e0; 152 | color: var(--text-color); 153 | } 154 | .modal.fade .modal-dialog { 155 | transition: transform 0.3s ease-out; 156 | } 157 | .modal.show .modal-dialog { 158 | transform: none; 159 | } 160 | .dropdown { 161 | position: absolute; 162 | background-color: #fff; 163 | border: 1px solid #ced4da; 164 | border-radius: 0.25rem; 165 | z-index: 1000; 166 | max-height: 200px; 167 | overflow-y: auto; 168 | } 169 | .dropdown-item { 170 | padding: 0.5rem 1rem; 171 | cursor: pointer; 172 | } 173 | .dropdown-item:hover { 174 | background-color: #f8f9fa; 175 | } 176 | 177 | .tagify:focus-within { 178 | border-color: var(--primary-color); 179 | box-shadow: 0 0 0 0.25rem rgba(48, 227, 104, 0.25); 180 | } 181 | 182 | .tagify__tag { 183 | --tag-bg: var(--primary-color); 184 | --tag-hover: var(--primary-dark); 185 | } -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Syrics | WEB 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 37 | 38 | 39 | 40 | 42 | 43 | 44 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
57 | 67 |
68 | {% block body %} 69 | 70 | {% endblock %} 71 |
72 | 73 |
74 | 103 | 104 | 107 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /spotify.py: -------------------------------------------------------------------------------- 1 | import re 2 | import requests 3 | import spotipy 4 | from spotipy.oauth2 import SpotifyClientCredentials 5 | import os 6 | 7 | # from dotenv import load_dotenv 8 | # load_dotenv() 9 | 10 | cid = os.getenv("SPOTIFY_CLIENT_ID") 11 | secret = os.getenv("SPOTIFY_CLIENT_SECRET") 12 | client_credentials_manager = SpotifyClientCredentials( 13 | client_id=cid, client_secret=secret 14 | ) 15 | sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) 16 | 17 | REGEX = r"^(?:spotify:(track|album|playlist):|https:\/\/[a-z]+\.spotify\.com\/(track|playlist|album)\/)(.\w+)?.*$" 18 | SHORT_URL_REGEX = r'window.top.location = validateProtocol\("(\S+)"\);' 19 | 20 | 21 | def get_album(album_id): 22 | album_data = sp.album(album_id) 23 | album_data["artists"] = ",".join( 24 | [artist["name"] for artist in album_data["artists"]] 25 | ) 26 | return { 27 | "name": album_data["name"], 28 | "id": album_id, 29 | "artist": album_data["artists"], 30 | "total_tracks": album_data["total_tracks"], 31 | "release_date": album_data["release_date"], 32 | "label": album_data["label"], 33 | "image": album_data["images"][0]["url"], 34 | "tracks": album_data["tracks"]["items"], 35 | } 36 | 37 | 38 | def get_track(track_id): 39 | track_data = sp.track(track_id) 40 | track_data["artist"] = ",".join( 41 | [artist["name"] for artist in track_data["artists"]] 42 | ) 43 | return { 44 | "track_name": track_data["name"], 45 | "track_id": track_id, 46 | "track_artist": track_data["artist"], 47 | "track_album": track_data["album"]["name"], 48 | "image": track_data["album"]["images"][0]["url"], 49 | "track_explicit": "[E]" if track_data["explicit"] else "Not Explicit", 50 | "track_release_date": track_data["album"]["release_date"], 51 | "track_popularity": track_data["popularity"], 52 | "track_number": track_data["track_number"], 53 | "track_duration": format_duration(track_data["duration_ms"]), 54 | } 55 | 56 | 57 | def get_play(play_id): 58 | play_data = sp.playlist(play_id) 59 | play_data["owner"] = play_data["owner"]["display_name"] 60 | play_data["total_tracks"] = play_data["tracks"]["total"] 61 | play_data["collaborative"] = ( 62 | "Collaborative" if play_data["collaborative"] else "Not Collaborative" 63 | ) 64 | return { 65 | "name": play_data["name"], 66 | "id": play_id, 67 | "owner": play_data["owner"], 68 | "total_tracks": play_data["total_tracks"], 69 | "desc": play_data["description"] or "No Description", 70 | "followers": play_data["followers"]["total"], 71 | "image": play_data["images"][0]["url"], 72 | "tracks": play_data["tracks"]["items"], 73 | } 74 | 75 | 76 | def check_regex(url): 77 | url = requests.get(url, allow_redirects=True).url 78 | if "spotify.link" in url or "spotify.app.link" in url: 79 | req = requests.get(url, allow_redirects=True).text 80 | match = re.search(SHORT_URL_REGEX, req) 81 | if match: 82 | url = match[1] 83 | match = re.match(REGEX, url) 84 | if not match: 85 | payload = {"url": url, "country": "IN"} 86 | req = requests.post("https://songwhip.com/api/songwhip/create", json=payload) 87 | print(req.json()) 88 | if req.status_code != 200: 89 | return None, None 90 | link = req.json()["data"]["item"]["links"]["spotify"][0]["link"] 91 | match = re.match(REGEX, link) 92 | if match[2]: 93 | return match[2], match[3] 94 | elif match[1]: 95 | return match[1], match[3] 96 | else: 97 | return None, None 98 | 99 | 100 | def query_spotify(q=None, type="track,album,playlist"): 101 | data = sp.search(q=q, type=type, limit=1) 102 | response = [] 103 | if data["tracks"]["items"]: 104 | response.append( 105 | { 106 | "name": data["tracks"]["items"][0]["name"], 107 | "type": "track", 108 | "image;:": data["tracks"]["items"][0]["album"]["images"][0]["url"], 109 | } 110 | ) 111 | if data["albums"]["items"]: 112 | response.append( 113 | { 114 | "name": data["albums"]["items"][0]["name"], 115 | "type": "album", 116 | "image": data["albums"]["items"][0]["images"][0]["url"], 117 | } 118 | ) 119 | if data["playlists"]["items"]: 120 | response.append( 121 | { 122 | "name": data["playlists"]["items"][0]["name"], 123 | "type": "playlist", 124 | "image": data["playlists"]["items"][0]["images"][0]["url"], 125 | } 126 | ) 127 | return response 128 | 129 | 130 | def get_all_trackids(_id, album=False): 131 | offset = 0 132 | limit = 50 133 | tracks = {} 134 | if album: 135 | while True: 136 | results = sp.album_tracks(_id, offset=offset, limit=limit) 137 | for track in results["items"]: 138 | if not track["id"]: 139 | continue 140 | track["artist"] = ",".join( 141 | [artist["name"] for artist in track["artists"]] 142 | ) 143 | tracks[track["id"]] = { 144 | "name": track["name"], 145 | "track_number": track["track_number"], 146 | "artist": track["artist"], 147 | "duration": format_duration(track["duration_ms"]), 148 | } 149 | offset += limit 150 | if len(results["items"]) < limit: 151 | break 152 | else: 153 | while True: 154 | results = sp.playlist_tracks(_id, offset=offset, limit=limit) 155 | for track in results["items"]: 156 | if not track["track"]["id"]: 157 | continue 158 | track["track"]["artist"] = ",".join( 159 | [artist["name"] for artist in track["track"]["artists"]] 160 | ) 161 | tracks[track["track"]["id"]] = { 162 | "name": track["track"]["name"], 163 | "track_number": track["track"]["track_number"], 164 | "artist": track["track"]["artist"], 165 | "album": track["track"]["album"]["name"], 166 | "duration": format_duration(track["track"]["duration_ms"]), 167 | } 168 | offset += limit 169 | if len(results["items"]) < limit: 170 | break 171 | return tracks 172 | 173 | 174 | def format_duration(duration_ms): 175 | minutes = duration_ms // 60000 176 | seconds = (duration_ms % 60000) // 1000 177 | hundredths = (duration_ms % 1000) // 10 178 | return f"{minutes:02d}:{seconds:02d}.{hundredths:02d}" 179 | -------------------------------------------------------------------------------- /static/js/spotify.js: -------------------------------------------------------------------------------- 1 | const downloadbtn = document.getElementsByName('download'); 2 | const downzip = document.getElementById('downzip'); 3 | const album_name = document.getElementById('album_name').textContent; 4 | const lyricsSettings = JSON.parse(localStorage.getItem('lyricsSettings')); 5 | const lyricsType = lyricsSettings ? lyricsSettings.lyricsType : 'lrc'; 6 | const fileNameFormat = lyricsSettings ? lyricsSettings.fileNameFormat : ["track_name", "track_no", "album"]; 7 | 8 | async function get_lyrics(id) { 9 | const response = await fetch(`https://spotify-lyrics-api-pi.vercel.app/?trackid=${id}&format=${lyricsType}`); 10 | if (response.status != 200) { 11 | return [null, null]; 12 | } 13 | const data = await response.json(); 14 | lyrics = []; 15 | let sync = true; 16 | if (data.syncType == "UNSYNCED") { 17 | data.lines.forEach(line => { 18 | lyrics.push(`${line['words']}\n`); 19 | }); 20 | sync = false; 21 | } else { 22 | if (lyricsType === 'srt') { 23 | data.lines.forEach((line) => { 24 | lyrics.push(`${line["index"]}\n${line["startTime"]} --> ${line["endTime"]}\n${line["words"]}\n\n`); 25 | }); 26 | } else { 27 | data.lines.forEach(line => { 28 | lyrics.push(`[${line['timeTag']}] ${line['words']}\n`); 29 | }); 30 | } 31 | } 32 | return [lyrics, sync]; 33 | } 34 | 35 | function renameUsingFormat(string, data) { 36 | const matches = string.match(/{(.+?)}/g); 37 | if (matches) { 38 | matches.forEach(match => { 39 | console.log(match); 40 | const key = match.slice(1, -1); 41 | string = string.replace(match, data[key] || ''); 42 | }); 43 | } 44 | return string; 45 | } 46 | 47 | save_lyrics = (lyrics, track_details, type) => { 48 | const blob = new Blob(lyrics, { type: "text/plain;charset=utf-8" }); 49 | const nameTemplate = fileNameFormat.join(""); 50 | const name = renameUsingFormat(nameTemplate, track_details); 51 | window.saveAs(blob, sanitizeFilename(name + '.' + type)); 52 | } 53 | 54 | sanitizeFilename = (filename) => filename.replace(/[\/\\:*?"<>|]/g, '_'); 55 | 56 | noramlDownload = async () => { 57 | const zip = new JSZip(); 58 | const promises = []; 59 | let count = 0; 60 | downzip.innerHTML = ``; 61 | downzip.classList.add('disabled'); 62 | let bar = document.getElementById('progress'); 63 | downloadbtn.forEach((btn) => { 64 | const id = btn.getAttribute('data-id'); 65 | promises.push(get_lyrics(id).then(response => { 66 | const attributes = ['data-name', 'data-album', 'data-artist', 'data-title', 'data-length']; 67 | const [name, album, artist, title, length] = attributes.map(attr => btn.getAttribute(attr)); 68 | const lyrics = response[0] 69 | const sync = response[1] 70 | if (lyrics == null) { 71 | btn.innerHTML = ''; 72 | btn.classList.add('disabled'); 73 | btn.previousElementSibling.classList.add('badge', 'bg-danger'); 74 | btn.previousElementSibling.textContent = 'No lyrics found'; 75 | return; 76 | } 77 | else if (!sync) { 78 | btn.previousElementSibling.classList.add('badge', 'bg-warning'); 79 | btn.previousElementSibling.textContent = 'Synced lyrics not available'; 80 | } 81 | lyrics.unshift(`[ar:${artist}]\n[al:${album}]\n[ti:${title}]\n[length:${length}]\n\n`); 82 | if (lyricsType === 'lrc') { 83 | zip.file(`${sanitizeFilename(name)}.lrc`, lyrics.join("")); 84 | } else if (lyricsType === 'srt') { 85 | zip.file(`${sanitizeFilename(name)}.srt`, lyrics.join("")); 86 | } 87 | count++; 88 | let variable = ((count / downloadbtn.length) * 100).toFixed(2); 89 | bar.style.width = `${variable}%`; 90 | bar.textContent = `${variable}%`; 91 | console.log(variable); 92 | })); 93 | }); 94 | try { 95 | await Promise.all(promises); 96 | 97 | if (count !== 0) { 98 | const content = await zip.generateAsync({ type: "blob" }); 99 | window.saveAs(content, `${album_name}.zip`); 100 | downzip.innerHTML = ' ZIP'; 101 | downzip.classList.remove('disabled'); 102 | } else { 103 | showToast('None of the tracks have lyrics'); 104 | downzip.innerHTML = ' ZIP'; 105 | } 106 | } catch (error) { 107 | console.error(error); 108 | } finally { 109 | let myModalEl = document.getElementById('staticBackdrop'); 110 | let modal = bootstrap.Modal.getInstance(myModalEl); 111 | modal.hide(); 112 | } 113 | } 114 | 115 | maxDownload = async (type, id) => { 116 | const zip = new JSZip(); 117 | downzip.innerHTML = ``; 118 | downzip.classList.add('disabled'); 119 | response = await fetch(`/api/getalltracks?id=${id}&album=${type == 'album' ? 'true' : ''}`); 120 | const songs = await response.json(); 121 | const { length } = Object.keys(songs); 122 | let progress = 0; 123 | bar = document.getElementById('progress'); 124 | for (const trackid in songs) { 125 | let variable = parseInt((progress / length) * 100); 126 | bar.style.width = `${variable}%`; 127 | bar.textContent = `${variable}%`; 128 | res_lyric = await get_lyrics(trackid); 129 | const lyrics = res_lyric[0]; 130 | if (lyrics != null) { 131 | if (lyricsType === 'lrc') { 132 | lyrics.unshift(`[ar:${songs[trackid]["artist"]}]\n[al:${album_name}]\n[ti:${songs[trackid]['name']}]\n[length:${songs[trackid]['duration']}]\n\n`); 133 | zip.file(`${songs[trackid]["track_number"]}. ${sanitizeFilename(songs[trackid]["name"])}.lrc`, lyrics.join("")); 134 | } else if (lyricsType === 'srt') { 135 | zip.file(`${songs[trackid]["track_number"]}. ${sanitizeFilename(songs[trackid]["name"])}.srt`, lyrics.join("")); 136 | } 137 | } 138 | progress++; 139 | }; 140 | if (Object.keys(zip.files).length > 0) { 141 | document.getElementById('staticBackdropLabel').textContent = "Downloading..." 142 | zip.generateAsync({ type: "blob" }).then((content) => { 143 | window.saveAs(content, `${album_name}.zip`); 144 | setInterval(() => { 145 | downzip.innerHTML = ' ZIP'; 146 | downzip.classList.remove('disabled'); 147 | }, 2000); 148 | }) 149 | } 150 | else { 151 | showToast('None of the tracks have lyrics'); 152 | downzip.innerHTML = ' ZIP'; 153 | } 154 | let myModalEl = document.getElementById('staticBackdrop'); 155 | let modal = bootstrap.Modal.getInstance(myModalEl) 156 | modal.hide(); 157 | } 158 | 159 | function downlodDecider() { 160 | tracks = parseInt(document.getElementById('total_tracks').textContent.replace(" Tracks")); 161 | data = document.getElementById('music_cover'); 162 | type = data.getAttribute('data-type'); 163 | id = data.getAttribute('data-id'); 164 | if (tracks > 100 && type == 'playlist') 165 | maxDownload('playlist', id); 166 | else if (tracks > 50 && type == 'album') 167 | maxDownload('album', id); 168 | else 169 | noramlDownload(); 170 | } 171 | downzip.addEventListener('click', downlodDecider); 172 | 173 | downloadbtn.forEach((btn) => { 174 | btn.addEventListener('click', async () => { 175 | btn.innerHTML = ''; 176 | btn.classList.add('disabled'); 177 | const attributes = ['data-id', 'data-name', 'data-album', 'data-artist', 'data-title', 'data-length']; 178 | const track_details = await fetch(`/api/tracks/${btn.getAttribute('data-id')}`).then(response => response.json()); 179 | const [id, name, album, artist, title, length] = attributes.map(attr => btn.getAttribute(attr)); 180 | const response = await get_lyrics(id); 181 | let lyrics = response[0]; 182 | let sync = response[1]; 183 | if (lyrics == null) { 184 | btn.innerHTML = ''; 185 | btn.previousElementSibling.classList.add('badge', 'bg-danger'); 186 | btn.previousElementSibling.textContent = 'No lyrics found'; 187 | return; 188 | } 189 | else if (!sync) { 190 | btn.previousElementSibling.classList.add('badge', 'bg-warning'); 191 | btn.previousElementSibling.textContent = 'Synced lyrics not available'; 192 | } 193 | if (lyricsType === 'lrc') { 194 | lyrics.unshift(`[ar:${artist}]\n[al:${album}]\n[ti:${title}]\n[length:${length}]\n\n`); 195 | save_lyrics(lyrics, track_details, 'lrc'); 196 | } else if (lyricsType === 'srt') { 197 | save_lyrics(lyrics, track_details, 'srt'); 198 | } 199 | btn.innerHTML = ''; 200 | setInterval(() => { 201 | btn.innerHTML = ''; 202 | btn.classList.remove('disabled'); 203 | }, 2000); 204 | }); 205 | }); 206 | -------------------------------------------------------------------------------- /templates/spotify.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block body %} 3 | 4 |
5 |
6 |
7 |
8 | music cover pic 11 |
12 |
13 | {% if types == 'album' %} 14 |
15 |

{{ data["name"] }}

16 |

{{ data['artist'] }}

17 |

{{ data['release_date'] }}

18 |

{{ data['total_tracks'] }} Tracks

19 |

{{ data['label'] }}

20 |
21 | 25 | 26 | 27 | 53 | {% elif types == 'playlist' %} 54 |
55 |

{{ data["name"] }}

56 | {{ data["desc"] }} 57 |

{{ data['owner'] }}

58 |

{{ data['followers'] }} followers

59 |

{{ data['total_tracks'] }} Tracks

60 |
61 | 65 | 66 | 67 | 93 | {% elif types == 'track' %} 94 |
95 |

{{ data["track_name"] }}

96 |

{{ data['track_artist'] }}

97 |

{{ data['track_release_date'] }}

98 |

{{ data['track_explicit'] }} track

99 |

{{ data['track_popularity'] }} popularity

100 |
101 | 104 | {% endif %} 105 |
106 |
107 |
108 |
109 |
110 |
111 |
    112 | {% if types == 'album' %} 113 | {% for track in data['tracks'] %} 114 |
  • 115 | 116 | {{ track['track_number']}}. {{ track['name'] }} 117 | 118 | 125 |
  • 126 | {% endfor %} 127 | {% elif types == 'playlist' %} 128 | {% for track in data['tracks'] %} 129 |
  • 130 | 131 | {{ track['track']['track_number']}}. {{ track['track']['name'] }} 132 | 133 | 141 |
  • 142 | {% endfor %} 143 | {% elif types == 'track' %} 144 |
  • 145 | 146 | {{ data['track_number']}}. {{ data['track_name'] }} 147 | 148 | 155 |
  • 156 | {% endif %} 157 |
158 |
159 |
160 | 161 | {% endblock %} --------------------------------------------------------------------------------