138 | Please save and pin your key on the clipboard as it is saved in the
139 | browser you use only. If you changed browsers, you'll need to
140 | re-enter it
141 |
142 |
143 |
144 | How can I return to use the tool API:
147 |
148 |
149 |
Delete your key from the page here or delete the site cookies
150 |
151 |
152 | Would the tool use my key for other users:
155 |
156 |
157 |
No, your key is saved in your cookies not in a DB
158 |
159 |
How secure will be the API Key?
160 |
161 |
162 | We are not setting the API Key directly to the cookie. Instead, we
163 | are generating a token based on your api key which expires in every
164 | one hour then it regenerates a new token automatically. So you can
165 | say your API Key is super secured.
166 |
215 |
216 |
217 |
--------------------------------------------------------------------------------
/mxm.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 | import jellyfish
4 | import Asyncmxm
5 | import asyncio
6 | from urllib.parse import unquote
7 | import redis
8 |
9 | class MXM:
10 | DEFAULT_KEY = os.environ.get("MXM_API")
11 | DEFAULT_KEY2 = os.environ.get("MXM_API2")
12 |
13 | def __init__(self, key=None, session=None):
14 | self.key = key or self.DEFAULT_KEY
15 | self.key2 = key or self.DEFAULT_KEY2
16 | if not self.key:
17 | r = redis.Redis(
18 | host=os.environ.get("REDIS_HOST"),
19 | port=os.environ.get("REDIS_PORT"),
20 | password=os.environ.get("REDIS_PASSWD"))
21 | key1 = r.get("live:1")
22 | key2 = r.get("live:2")
23 | self.key = key1.decode()
24 | self.key2 = key2.decode()
25 | print(self.key," ", self.key2)
26 | r.close()
27 |
28 |
29 | self.session = session
30 | self.musixmatch = Asyncmxm.Musixmatch(self.key,requests_session=session)
31 | self.musixmatch2 = Asyncmxm.Musixmatch(self.key2,requests_session=session)
32 |
33 | def change_key(self, key):
34 | self.key = key
35 |
36 | async def track_get(self, isrc=None, commontrack_id=None, vanity_id =None) -> dict:
37 | try:
38 | response = await self.musixmatch.track_get(
39 | track_isrc=isrc, commontrack_id=commontrack_id,
40 | commontrack_vanity_id= vanity_id
41 | )
42 | return response
43 | except Asyncmxm.exceptions.MXMException as e:
44 | return str(e)
45 |
46 | async def matcher_track(self, sp_id):
47 | try:
48 | response = await self.musixmatch2.matcher_track_get(
49 | q_track="null", track_spotify_id=sp_id
50 | )
51 | return response
52 | except Asyncmxm.exceptions.MXMException as e:
53 | return str(e)
54 |
55 | async def Track_links(self, sp_data):
56 | if isinstance(sp_data, dict):
57 | track = await self.track_get(sp_data.get("isrc"))
58 | try:
59 | id = track["message"]["body"]["track"]["commontrack_id"]
60 | except TypeError as e:
61 | return track
62 |
63 | track = track["message"]["body"]["track"]
64 | track["isrc"] = sp_data["isrc"]
65 | track["image"] = sp_data["image"]
66 | track["beta"] = str(track["track_share_url"]).replace("www.","com-beta.",1)
67 |
68 | return track
69 | else:
70 | return sp_data
71 |
72 | async def matcher_links(self, sp_data):
73 | id = sp_data["track"]["id"]
74 | track = await self.matcher_track(id)
75 | try:
76 | id = track["message"]["body"]["track"]["commontrack_id"]
77 | except TypeError as e:
78 | return track
79 |
80 | track = track["message"]["body"]["track"]
81 | track["isrc"] = sp_data["isrc"]
82 | track["image"] = sp_data["image"]
83 | track["beta"] = str(track["track_share_url"]).replace("www.","beta.",1)
84 | return track
85 |
86 | async def Tracks_Data(self, sp_data, split_check = False):
87 | links = []
88 | tracks = await self.tracks_get(sp_data)
89 |
90 |
91 | if isinstance(sp_data[0], dict) and sp_data[0].get("track"):
92 | matchers = await self.tracks_matcher(sp_data)
93 | else:
94 | return tracks
95 |
96 | for i in range(len(tracks)):
97 | track = tracks[i]
98 | matcher = matchers[i]
99 | if split_check:
100 | links.append(track)
101 | continue
102 |
103 | # detecting what issues can facing the track
104 | if isinstance(track, dict) and isinstance(matcher, dict):
105 |
106 | # the get call and the matcher call are the same and both have valid response
107 | if (track["commontrack_id"] == matcher["commontrack_id"]):
108 | track["matcher_album"] = [
109 | matcher["album_id"],
110 | matcher["album_name"],
111 | ]
112 | links.append(track)
113 | ''' when we get different data, the sp id attached to the matcher so we try to detect
114 | if the matcher one is vailid or it just a ISRC error.
115 | I used the probability here to choose the most accurate data to the spotify data
116 | '''
117 | else:
118 | matcher_title = re.sub(r'[()-.]', '', matcher.get("track_name"))
119 | matcher_album = re.sub(r'[()-.]', '', matcher.get("album_name"))
120 | sp_title = re.sub(r'[()-.]', '', sp_data[i]["track"]["name"])
121 | sp_album = re.sub(r'[()-.]', '', sp_data[i]["track"]["album"]["name"])
122 | track_title = re.sub(r'[()-.]', '', track.get("track_name"))
123 | track_album = re.sub(r'[()-.]', '', track.get("album_name"))
124 | if (matcher.get("album_name") == sp_data[i]["track"]["album"]["name"]
125 | and matcher.get("track_name") == sp_data[i]["track"]["name"]
126 | or jellyfish.jaro_similarity(matcher_title.lower(), sp_title.lower())
127 | * jellyfish.jaro_similarity(matcher_album.lower(), sp_album.lower()) >=
128 | jellyfish.jaro_similarity(track_title.lower(), sp_title.lower())
129 | * jellyfish.jaro_similarity(track_album.lower(), sp_album.lower()) ):
130 | matcher["note"] = f'''This track may having two pages with the same ISRC,
131 | the other page from album.'''
134 | links.append(matcher)
135 | else:
136 |
137 | track["note"] = f'''This track may be facing an ISRC issue
138 | as the Spotify ID is connected to another page from album.'''
141 | links.append(track)
142 | continue
143 |
144 | elif isinstance(track, str) and isinstance(matcher, str):
145 | if re.search("404", track):
146 | track = """
147 | The track hasn't been imported yet. Please try again after 1-5 minutes.
148 | Sometimes it may take longer, up to 15 minutes, depending on the MXM API and their servers.
149 | """
150 | links.append(track)
151 | continue
152 | else: links.append(track)
153 | elif isinstance(track, str) and isinstance(matcher, dict):
154 | if matcher.get("album_name") == sp_data[i]["track"]["album"]["name"]:
155 | links.append(matcher)
156 | continue
157 | else: links.append(matcher)
158 | elif isinstance(track, dict) and isinstance(matcher, str):
159 | track["note"] = "This track may missing its Spotify id"
160 | links.append(track)
161 | else:
162 | links.append(track)
163 | return links
164 |
165 | async def tracks_get(self, data):
166 | coro = [self.Track_links(isrc) for isrc in data]
167 | tasks = [asyncio.create_task(c) for c in coro]
168 | tracks = await asyncio.gather(*tasks)
169 | return tracks
170 |
171 | async def tracks_matcher(self, data):
172 | coro = [self.matcher_links(isrc) for isrc in data]
173 | tasks = [asyncio.create_task(c) for c in coro]
174 | tracks = await asyncio.gather(*tasks)
175 | return tracks
176 |
177 | async def album_sp_id(self,link):
178 | site = re.search(r"musixmatch.com",link)
179 | match = re.search(r'album/([^?]+/[^?]+)|album/(\d+)|lyrics/([^?]+/[^?]+)', unquote(link))
180 | if match and site:
181 | try:
182 | if match.group(1):
183 | album = await self.musixmatch.album_get(album_vanity_id=match.group(1))
184 | elif match.group(2):
185 | album = await self.musixmatch.album_get(match.group(2))
186 | else:
187 | track = await self.musixmatch.track_get(commontrack_vanity_id=match.group(3))
188 | album_id = track["message"]["body"]["track"]["album_id"]
189 | album = await self.musixmatch.album_get(album_id)
190 | print(album)
191 | return {"album": album["message"]["body"]["album"]}
192 | except Asyncmxm.exceptions.MXMException as e:
193 | return {"error": str(e)}
194 | else:
195 | return {"error": "Unsupported link."}
196 |
197 | async def abstrack(self, id : int) -> tuple[dict,dict]:
198 | """Get the track and the album data from the abstrack."""
199 | try:
200 | track = await self.musixmatch.track_get(commontrack_id=id)
201 | track = track["message"]["body"]["track"]
202 | album = await self.musixmatch.album_get(track["album_id"])
203 | album = album["message"]["body"]["album"]
204 | return track, album
205 | except Asyncmxm.exceptions.MXMException as e:
206 | return {"error": str(e)}, {"error": str(e)}
207 |
208 |
--------------------------------------------------------------------------------
/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
13 | Spotify to Musixmatch Link
14 |
19 |
20 |
21 |
22 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | ⚠️
33 |
34 | End of Life Notice: This tool will become defunct on August 25, 2025, following the termination of the Musixmatch API free tier. You can still set your own API key.
35 | Learn More
36 |
37 |
38 |
39 |
40 |
41 |
42 |
51 | No Internet Connection
52 |
53 |
62 | Make sure you're connected to the internet.
63 |
64 |
65 |
66 |
67 |
77 |
78 |
79 |
80 |
Spotify to Musixmatch Link
81 |
92 |
93 | {% if tracks_data %}
94 |
95 | {% for track in tracks_data %} {% if track.isrc %}
96 |
205 | Importing new release songs now works, but it may take some additional time.
206 |
207 |
208 |
209 |
210 | ×
211 |
How to Use
212 |
213 |
214 | Enter a valid Spotify link (track or album) or ISRC into the input
215 | field.
216 |
217 |
218 | Click the "Get links" button and wait until the processing ends.
219 |
220 |
221 |
If the track has not been imported yet:
222 |
223 |
224 | The app tries to import it automatically. You may get the links
225 | immediately or have to wait for 1 or 2 minutes (depending on the
226 | MXM API).
227 |
228 |
229 | If you see the same message after a while, it means that the app
230 | can't import it. You have to ask for help via Slack.
231 |
232 |
233 |
Spotify Limits:
234 |
235 |
You can't get more than 50 tracks from an album.
236 |
237 |
Hosting Limits:
238 |
239 |
240 | If you see a limit error, it mostly means that the album has many
241 | tracks. Try to get the links by using a track link, then open the
242 | album from MXM.
243 |
244 |
245 | You can use http://cifor55334.pythonanywhere.com/ or
246 | https://spotify-to-mxm.onrender.com/ as a mirror to the Vercel
247 | domain.
248 |
249 |
250 |
251 | Finally, if you face a problem or the app is malfunctioning, you can
252 | reach me via Slack "@Adel".
257 |
258 |
Hoping this app is useful.
259 |
260 |
261 |
262 |
263 |
264 |
265 |
--------------------------------------------------------------------------------
/static/styles.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@100;200;300;400;500;600;700;800&family=Nunito:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;0,1000;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900;1,1000&family=Source+Sans+Pro:ital,wght@0,200;0,300;0,400;0,600;0,700;0,900;1,200;1,300;1,400;1,600;1,700;1,900&family=Source+Serif+Pro:ital,wght@1,200;1,300;1,400;1,600;1,700;1,900&family=Ubuntu:wght@300;400;500;700&display=swap");
2 |
3 | body {
4 | background: #f5f5f5 url(bg-music.png) no-repeat top right;
5 | font-family: "Nunito", "Open Sans", sans-serif;
6 | }
7 |
8 | .container {
9 | max-width: 800px;
10 | margin: 0 auto;
11 | text-align: center;
12 | background: #fff;
13 | padding: 20px;
14 | border-radius: 10px;
15 | box-shadow: 0px 0px 10px #ccc;
16 | transition: background-color 0.3s ease;
17 | }
18 |
19 | h1 {
20 | font-size: 3em;
21 | font-weight: 600;
22 | margin-bottom: 20px;
23 | color: #1db954;
24 | }
25 |
26 | form {
27 | margin: 0 auto;
28 | width: 80%;
29 | display: flex;
30 | flex-wrap: wrap;
31 | align-items: center;
32 | justify-content: center;
33 | margin-bottom: 30px;
34 | }
35 |
36 | label {
37 | font-size: 1.2em;
38 | font-weight: 600;
39 | margin-right: 10px;
40 | }
41 |
42 | input[type="text"] {
43 | padding: 12px 20px;
44 | margin: 8px 0;
45 | box-sizing: border-box;
46 | border: 2px solid #ccc;
47 | border-radius: 4px;
48 | width: 100%;
49 | background-color: #f5f5f5;
50 | transition: border-color 0.3s ease;
51 | }
52 |
53 | input[type="text"]:focus {
54 | outline: none;
55 | border-color: #1db954;
56 | box-shadow: 0 0 5px #1db954;
57 | }
58 |
59 | input[type="text"]::placeholder {
60 | color: #999;
61 | }
62 |
63 | input[type="text"]:hover:not(:focus) {
64 | border-color: #999;
65 | }
66 |
67 | button[type="submit"] {
68 | width: 100%;
69 | padding: 14px 20px;
70 | margin: 8px 0;
71 | border: none;
72 | border-radius: 4px;
73 | background-color: #1db954;
74 | color: white;
75 | font-size: 1.2em;
76 | font-weight: 600;
77 | cursor: pointer;
78 | transition: background-color 0.3s ease;
79 | }
80 |
81 | button[type="submit"]:hover {
82 | background-color: #198649;
83 | }
84 |
85 | .output {
86 | text-align: left;
87 | margin-top: 30px;
88 | font-size: 1.2em;
89 | }
90 |
91 | .output p {
92 | margin: 10px 0;
93 | }
94 |
95 | .loading {
96 | display: none;
97 | margin: 0 auto;
98 | text-align: center;
99 | border: 8px solid #f3f3f3;
100 | border-radius: 50%;
101 | width: 40px;
102 | height: 40px;
103 | animation: spin 2s linear infinite;
104 | box-shadow: 0 4px 0 0 #5fe15b;
105 | }
106 |
107 | .error {
108 | color: red;
109 | display: none;
110 | margin: 10px 0;
111 | text-align: center;
112 | }
113 |
114 | .logo {
115 | display: flex;
116 | justify-content: space-between;
117 | align-items: center;
118 | margin-bottom: 20px;
119 | }
120 |
121 | .logo img {
122 | width: 50px;
123 | height: 50px;
124 | }
125 |
126 | .logo img:first-child {
127 | margin-right: 10px;
128 | }
129 |
130 | .logo img:last-child {
131 | margin-left: 10px;
132 | }
133 |
134 | .card {
135 | background-color: #fff;
136 | border-radius: 10px;
137 | box-shadow: 0px 0px 10px #ccc;
138 | margin: 20px;
139 | padding: 20px;
140 | display: flex;
141 | flex-direction: row;
142 | align-items: center;
143 | transition: transform 0.3s ease;
144 | }
145 |
146 | .card img {
147 | width: 150px;
148 | margin-right: 20px;
149 | }
150 |
151 | .card-details {
152 | flex: 1;
153 | margin-top: -10px;
154 | }
155 |
156 | .card-title {
157 | margin-bottom: 5px;
158 | }
159 |
160 | .card-text {
161 | margin-bottom: 0;
162 | }
163 |
164 | .card-text:last-child {
165 | margin-bottom: 5px;
166 | }
167 |
168 | .card-link {
169 | margin-top: 10px;
170 | text-decoration: none;
171 | color: #ff6050;
172 | transition: color 0.3s ease;
173 | position: relative;
174 | }
175 |
176 | .card-link:hover {
177 | margin-top: 10px;
178 | text-decoration: none;
179 | color: #f95546;
180 | }
181 |
182 | .card-link::before {
183 | content: "";
184 | position: absolute;
185 | bottom: -2px;
186 | left: 0;
187 | width: 100%;
188 | height: 2px;
189 | background-color: #ff6050;
190 | transform: scaleX(0);
191 | transition: transform 0.3s ease;
192 | }
193 |
194 | .card-link:hover::before {
195 | transform: scaleX(1);
196 | }
197 |
198 | @keyframes spin {
199 | 0% {
200 | transform: rotate(0deg);
201 | }
202 | 100% {
203 | transform: rotate(360deg);
204 | }
205 | }
206 |
207 | .instructions {
208 | position: sticky;
209 | top: 10px;
210 | right: 10px;
211 | z-index: 1;
212 | text-align: center;
213 | }
214 |
215 | .instructions a {
216 | display: inline-block;
217 | padding: 10px 20px;
218 | border: 1px solid #ccc;
219 | border-radius: 5px;
220 | background-color: #f8f8f8;
221 | text-decoration: none;
222 | color: #333;
223 | font-size: 14px;
224 | }
225 |
226 | .instructions a:hover {
227 | background-color: #ddd;
228 | }
229 |
230 | /* Modal styles */
231 | .modal {
232 | display: none;
233 | position: fixed;
234 | z-index: 1;
235 | left: 0;
236 | top: 0;
237 | width: 100%;
238 | height: 100%;
239 | overflow: auto;
240 | background-color: rgba(0, 0, 0, 0.5);
241 | }
242 |
243 | .modal-content {
244 | background-color: #fefefe;
245 | margin: 10% auto;
246 | padding: 20px;
247 | border: 1px solid #888;
248 | width: 80%;
249 | max-width: 600px;
250 | }
251 |
252 | .close {
253 | display: block;
254 | position: absolute;
255 | top: 10px;
256 | right: 10px;
257 | font-size: 24px;
258 | font-weight: bold;
259 | color: #aaa;
260 | text-shadow: 1px 1px #fff; /* add a subtle text shadow */
261 | opacity: 0.7; /* reduce opacity to make it semi-transparent */
262 | transition: opacity 0.2s ease-in-out; /* add a transition effect */
263 | }
264 |
265 | .close:hover,
266 | .close:focus {
267 | color: #000;
268 | text-decoration: none;
269 | cursor: pointer;
270 | opacity: 1; /* increase opacity on hover/focus */
271 | }
272 |
273 | .note {
274 | position: fixed;
275 | bottom: 10px;
276 | left: 10px;
277 | z-index: 10;
278 | background-color: #fff;
279 | padding: 10px;
280 | border: 1px solid #ccc;
281 | border-radius: 5px;
282 | max-width: 300px;
283 | box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.2);
284 | display: block;
285 | }
286 |
287 | .note-text {
288 | font-size: 16px;
289 | font-family: sans-serif;
290 | line-height: 1.5;
291 | color: #333;
292 | }
293 |
294 | .container:hover {
295 | background-color: #f8f8f8;
296 | }
297 |
298 | .card:hover {
299 | transform: scale(1.02);
300 | }
301 |
302 | .button-container {
303 | position: relative;
304 | width: 100%;
305 | height: 100%;
306 | }
307 |
308 | .button-container:hover .card-link {
309 | color: #1db954;
310 | }
311 |
312 | .button-container::before {
313 | content: "";
314 | position: absolute;
315 | top: 0;
316 | left: 0;
317 | width: 100%;
318 | height: 100%;
319 | background-color: rgba(29, 185, 84, 0.2);
320 | opacity: 0;
321 | transition: opacity 0.3s ease;
322 | }
323 |
324 | .button-container:hover::before {
325 | opacity: 1;
326 | }
327 |
328 | #offline-div {
329 | display: none;
330 | }
331 |
332 | .reach a {
333 | text-decoration: none;
334 | color: rgb(0, 128, 255);
335 | }
336 | table {
337 | width: 100%;
338 | border-collapse: collapse;
339 | margin-top: 20px;
340 | }
341 |
342 | th,
343 | td {
344 | padding: 10px;
345 | text-align: left;
346 | border-bottom: 1px solid #ddd;
347 | }
348 |
349 | th {
350 | background-color: #f2f2f2;
351 | font-weight: bold;
352 | color: #333;
353 | }
354 |
355 | td:first-child {
356 | font-weight: bold;
357 | color: #555;
358 | }
359 |
360 | tr:nth-child(even) {
361 | background-color: #f9f9f9;
362 | }
363 |
364 | tr:hover {
365 | background-color: #e9e9e9;
366 | }
367 |
368 | td:last-child {
369 | font-style: italic;
370 | text-align: center;
371 | }
372 |
373 | /* Navigation Bar */
374 | .navbar {
375 | max-width: 800px;
376 | margin: 0 auto;
377 | background-color: #1db954;
378 | padding: 20px;
379 | border-radius: 10px;
380 | display: flex;
381 | justify-content: center;
382 | }
383 |
384 | .navbar ul {
385 | list-style: none;
386 | margin: 0;
387 | padding: 0;
388 | display: flex;
389 | }
390 |
391 | .navbar li {
392 | margin: 0 15px;
393 | }
394 |
395 | .navbar a {
396 | text-decoration: none;
397 | color: #fff;
398 | font-size: 1.03em;
399 | font-weight: bold;
400 | padding: 5px 5px;
401 | border-radius: 5px;
402 | transition: background-color 0.3s ease;
403 | }
404 |
405 | .navbar a:hover {
406 | background-color: rgba(255, 255, 255, 0.2);
407 | }
408 |
409 | /* Active link style (optional) */
410 | .navbar a.active {
411 | background-color: rgba(255, 255, 255, 0.5);
412 | }
413 |
414 | /* Media Query for screens with a max-width of 768px */
415 | @media (max-width: 768px) {
416 | .container {
417 | max-width: 100%;
418 | padding: 10px;
419 | }
420 |
421 | .card {
422 | flex-direction: column;
423 | align-items: flex-start;
424 | }
425 |
426 | .card img {
427 | width: 100%;
428 | margin-right: 0;
429 | margin-bottom: 10px;
430 | }
431 |
432 | .card-details {
433 | margin-top: 0;
434 | text-align: left;
435 | }
436 |
437 | .navbar {
438 | padding: 10px;
439 | }
440 |
441 | .navbar ul {
442 | flex-direction: column;
443 | align-items: center;
444 | }
445 |
446 | .navbar li {
447 | margin: 5px 0;
448 | }
449 | }
450 |
451 | /* EOL banner */
452 | .eol-banner {
453 | background: #fff3cd;
454 | padding: 16px 0;
455 | position: sticky;
456 | top: 0;
457 | z-index: 1000;
458 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
459 | border-radius: 12px;
460 | margin-bottom: 20px;
461 | }
462 |
463 | .eol-content {
464 | max-width: 1200px;
465 | margin: 0 auto;
466 | display: flex;
467 | align-items: center;
468 | justify-content: space-between;
469 | padding: 0 20px;
470 | font-size: 14px;
471 | line-height: 1.4;
472 | }
473 |
474 | .eol-icon {
475 | font-size: 18px;
476 | margin-right: 10px;
477 | flex-shrink: 0;
478 | }
479 |
480 | .eol-text {
481 | flex: 1;
482 | margin-right: 15px;
483 | color: #856404;
484 | }
485 |
486 | .eol-learn-more {
487 | color: #856404;
488 | text-decoration: underline;
489 | font-weight: 600;
490 | margin-left: 8px;
491 | }
492 |
493 | .eol-learn-more:hover {
494 | color: #533f03;
495 | }
496 |
497 | .eol-close {
498 | background: none;
499 | border: none;
500 | font-size: 20px;
501 | font-weight: bold;
502 | color: #856404;
503 | cursor: pointer;
504 | padding: 0;
505 | width: 24px;
506 | height: 24px;
507 | display: flex;
508 | align-items: center;
509 | justify-content: center;
510 | border-radius: 50%;
511 | }
512 |
513 | .eol-close:hover {
514 | background: rgba(133, 100, 4, 0.1);
515 | }
516 |
517 | /* mobile optimization */
518 | @media (max-width: 768px) {
519 | .eol-banner {
520 | padding: 12px 0;
521 | border-radius: 8px;
522 | }
523 |
524 | .eol-content {
525 | padding: 0 16px;
526 | font-size: 13px;
527 | flex-direction: row;
528 | align-items: center;
529 | gap: 0;
530 | }
531 |
532 | .eol-text {
533 | margin-right: 10px;
534 | margin-bottom: 0;
535 | }
536 |
537 | .eol-learn-more {
538 | margin-left: 6px;
539 | }
540 |
541 | .eol-close {
542 | position: static;
543 | width: 20px;
544 | height: 20px;
545 | font-size: 16px;
546 | }
547 | }
548 |
549 | @media (max-width: 480px) {
550 | .eol-content {
551 | font-size: 12px;
552 | padding: 0 12px;
553 | }
554 |
555 | .eol-icon {
556 | font-size: 16px;
557 | margin-right: 8px;
558 | }
559 |
560 | .eol-text {
561 | line-height: 1.3;
562 | }
563 |
564 | .eol-learn-more {
565 | font-size: 11px;
566 | }
567 |
568 | .eol-close {
569 | width: 18px;
570 | height: 18px;
571 | font-size: 14px;
572 | }
573 | }
574 |
--------------------------------------------------------------------------------
/app.py:
--------------------------------------------------------------------------------
1 | import re
2 | import json
3 | import datetime
4 | import base64
5 | import hmac
6 | import hashlib
7 | import os
8 | import aiohttp
9 | from flask import Flask, request, render_template, make_response
10 | from asgiref.wsgi import WsgiToAsgi
11 | from mxm import MXM
12 | from spotify import Spotify
13 |
14 |
15 | secret_key_value = os.environ.get("secret_key")
16 |
17 | SECRET_KEY = secret_key_value
18 |
19 | SECRET_KEY = SECRET_KEY.encode('utf-8')
20 |
21 |
22 | def generate_token(payload):
23 | header = {'alg': 'HS256', 'typ': 'JWT'}
24 | encoded_header = base64.urlsafe_b64encode(
25 | json.dumps(header).encode('utf-8')).rstrip(b'=')
26 | encoded_payload = base64.urlsafe_b64encode(
27 | json.dumps(payload).encode('utf-8')).rstrip(b'=')
28 |
29 | signature = hmac.new(SECRET_KEY, encoded_header +
30 | b'.' + encoded_payload, hashlib.sha256).digest()
31 | encoded_signature = base64.urlsafe_b64encode(signature).rstrip(b'=')
32 |
33 | return encoded_header + b'.' + encoded_payload + b'.' + encoded_signature
34 |
35 |
36 | def verify_token(token):
37 | encoded_header, encoded_payload, encoded_signature = token.split('.')
38 |
39 | header = base64.urlsafe_b64decode(encoded_header + '==').decode('utf-8')
40 | payload = base64.urlsafe_b64decode(encoded_payload + '==').decode('utf-8')
41 |
42 | expected_signature = hmac.new(
43 | SECRET_KEY, (encoded_header + '.' + encoded_payload).encode('utf-8'), hashlib.sha256).digest()
44 | expected_encoded_signature = base64.urlsafe_b64encode(
45 | expected_signature).rstrip(b'=')
46 |
47 | if expected_encoded_signature != encoded_signature.encode('utf-8'):
48 | return False
49 |
50 | payload = json.loads(payload)
51 | return payload
52 |
53 | def jwt_ref(resp,payload):
54 | current_time = datetime.datetime.now()
55 | payload["exp"] = int(
56 | (current_time + datetime.timedelta(days=3)).timestamp())
57 | new_token = generate_token(payload)
58 | expire_date = current_time + datetime.timedelta(days=3)
59 | resp.set_cookie("api_token", new_token.decode('utf-8'), expires=expire_date)
60 | return resp
61 |
62 |
63 | class StartAiohttp:
64 | session = None
65 |
66 | def __init__(self, limit, limit_per_host) -> None:
67 | self.limit = limit
68 | self.limit_per_host = limit_per_host
69 |
70 | def start_session(self):
71 | self.close_session()
72 | connector = aiohttp.TCPConnector(
73 | limit=self.limit, limit_per_host=self.limit_per_host)
74 | self.session = aiohttp.ClientSession(connector=connector)
75 |
76 | def get_session(self):
77 | return self.session
78 |
79 | async def close_session(self):
80 | if self.session:
81 | await self.session.close()
82 | self.session = None
83 |
84 |
85 | client = StartAiohttp(7, 7)
86 |
87 |
88 | app = Flask(__name__)
89 | sp = Spotify()
90 |
91 |
92 | @app.route('/', methods=['GET'])
93 | async def index():
94 | if request.cookies.get('api_key'):
95 | payload = {"mxm-key": request.cookies.get('api_key'), "exp": int(
96 | (datetime.datetime.now() + datetime.timedelta(days=3)).timestamp())}
97 | token = generate_token(payload)
98 |
99 | resp = make_response(render_template(
100 | "index.html"))
101 | expire_date = datetime.datetime.now() + datetime.timedelta(hours=1)
102 | resp.delete_cookie("api_key")
103 | resp.set_cookie("api_token", token, expires=expire_date)
104 | return resp
105 |
106 |
107 | link = request.args.get('link')
108 | key = None
109 | token = request.cookies.get('api_token')
110 | if link:
111 | if token:
112 | payload = verify_token(token)
113 | if payload:
114 | key = payload.get("mxm-key")
115 |
116 | client.start_session()
117 | mxm = MXM(key, session=client.get_session())
118 | try:
119 | if (len(link) < 12):
120 | return render_template('index.html', tracks_data=["Wrong Spotify Link Or Wrong ISRC"])
121 | elif re.search(r'artist/(\w+)', link):
122 | return render_template('index.html', artist=sp.artist_albums(link, []))
123 | else:
124 | sp_data = sp.get_isrc(link) if len(link) > 12 else [
125 | {"isrc": link, "image": None}]
126 | except Exception as e:
127 | return render_template('index.html', tracks_data=[str(e)])
128 |
129 | mxmLinks = await mxm.Tracks_Data(sp_data)
130 | if isinstance(mxmLinks, str):
131 | return mxmLinks
132 |
133 | await client.close_session()
134 |
135 | return render_template('index.html', tracks_data=mxmLinks)
136 |
137 | # refresh the token every time the user enter the site
138 | if token:
139 | payload = verify_token(token)
140 | resp = make_response(render_template(
141 | "index.html"))
142 | resp = jwt_ref(resp,payload)
143 | return resp
144 |
145 | return render_template('index.html')
146 |
147 |
148 | @app.route('/split', methods=['GET'])
149 | async def split():
150 | link = request.args.get('link')
151 | link2 = request.args.get('link2')
152 | key = None
153 | if link and link2:
154 | token = request.cookies.get('api_token')
155 | if token:
156 | payload = verify_token(token)
157 | if payload:
158 | key = payload.get("mxm-key")
159 | client.start_session()
160 | mxm = MXM(key, session=client.get_session())
161 | match = re.search(r'open.spotify.com',
162 | link) and re.search(r'track', link)
163 | match = match and re.search(
164 | r'open.spotify.com', link2) and re.search(r'track', link2)
165 | if match:
166 | sp_data1 = sp.get_isrc(link)
167 | sp_data2 = sp.get_isrc(link2)
168 | track1 = await mxm.Tracks_Data(sp_data1, True)
169 | track1 = track1[0]
170 | if isinstance(track1, str):
171 | return render_template('split.html', error="track1: " + track1)
172 | track2 = await mxm.Tracks_Data(sp_data2, True)
173 | track2 = track2[0]
174 | if isinstance(track2, str):
175 | return render_template('split.html', error="track2: " + track1)
176 | await client.close_session()
177 | track1["track"] = sp_data1[0]["track"]
178 | track2["track"] = sp_data2[0]["track"]
179 | try:
180 | if track1["isrc"] != track2["isrc"] and track1["commontrack_id"] == track2["commontrack_id"]:
181 | message = f"""Can be splitted
182 | you can c/p:
183 | :mxm: MXM Page
184 | :spotify: {track1["track"]["name"]},
185 | :isrc: {track1["isrc"]}
186 | :spotify: {track2["track"]["name"]},
187 | :isrc: {track2["isrc"]}
188 | """
189 | elif track1["isrc"] == track2["isrc"] and track1["commontrack_id"] == track2["commontrack_id"]:
190 | message = "Can not be splitted as they have the Same ISRC"
191 | else:
192 | message = "They have different Pages"
193 | except:
194 | return render_template('split.html', error="Something went wrong")
195 |
196 | return render_template('split.html', split_result={"track1": track1, "track2": track2}, message=message)
197 | else:
198 | return render_template('split.html', error="Wrong Spotify Link")
199 |
200 | else:
201 | return render_template('split.html')
202 |
203 |
204 | @app.route('/spotify', methods=['GET'])
205 | def isrc():
206 | link = request.args.get('link')
207 | if link:
208 | match = re.search(r'open.spotify.com', link) and re.search(
209 | r'track|album', link)
210 | if match:
211 | return render_template('isrc.html', tracks_data=sp.get_isrc(link))
212 |
213 | else:
214 | # the link is an isrc code
215 | if len(link) == 12:
216 | # search by isrc
217 | return render_template('isrc.html', tracks_data=sp.search_by_isrc(link))
218 | return render_template('isrc.html', tracks_data=["Wrong Spotify Link"])
219 | else:
220 | return render_template('isrc.html')
221 |
222 |
223 | @app.route('/api', methods=['GET'])
224 | async def setAPI():
225 | key = request.args.get('key')
226 | delete = request.args.get("delete_key")
227 |
228 | # Get the existing token from the cookie
229 | token = request.cookies.get('api_token')
230 | if token:
231 | payload = verify_token(token)
232 | if payload:
233 | key = payload.get("mxm-key")
234 | censored_key = '*' * len(key) if key else None
235 |
236 | # refresh the token each time the user enter the "/api"
237 | resp = make_response(render_template(
238 | "api.html", key=censored_key))
239 | resp = jwt_ref(resp,payload)
240 | return resp
241 |
242 |
243 | if key:
244 | # check the key
245 | client.start_session()
246 | mxm = MXM(key, session=client.get_session())
247 | sp_data = [{"isrc": "DGA072332812", "image": None}]
248 |
249 | # Call the Tracks_Data method with the appropriate parameters
250 | mxmLinks = await mxm.Tracks_Data(sp_data)
251 | print(mxmLinks)
252 | await client.close_session()
253 |
254 | if isinstance(mxmLinks[0], str):
255 | return render_template("api.html", error="Please Enter A Valid Key")
256 |
257 | payload = {"mxm-key": key, "exp": int(
258 | (datetime.datetime.now() + datetime.timedelta(hours=1)).timestamp())}
259 | token = generate_token(payload)
260 |
261 | resp = make_response(render_template(
262 | "api.html", key="Token Generated"))
263 | expire_date = datetime.datetime.now() + datetime.timedelta(hours=1)
264 | resp.set_cookie("api_token", token.decode('utf-8'), expires=expire_date)
265 | return resp
266 |
267 | elif delete:
268 | resp = make_response(render_template("api.html"))
269 | resp.delete_cookie("api_token")
270 | return resp
271 |
272 | else:
273 | return render_template("api.html", key=None)
274 |
275 |
276 | @app.route('/mxm', methods=['GET'])
277 | async def mxm_to_sp():
278 | link = request.args.get('link')
279 | key = None
280 | if link:
281 | token = request.cookies.get('api_token')
282 | if token:
283 | payload = verify_token(token)
284 | if payload:
285 | key = payload.get("mxm-key")
286 |
287 | client.start_session()
288 | mxm = MXM(key, session=client.get_session())
289 | album = await mxm.album_sp_id(link)
290 | await client.close_session()
291 | return render_template("mxm.html", album=album.get("album"), error=album.get("error"))
292 | else:
293 | return render_template("mxm.html")
294 |
295 | @app.route('/abstrack', methods=['GET'])
296 | async def abstrack() -> str:
297 | """ Get the track data from the abstract track """
298 | id = request.args.get('id')
299 | key = None
300 | if id:
301 | token = request.cookies.get('api_token')
302 | if token:
303 | payload = verify_token(token)
304 | if payload:
305 | key = payload.get("mxm-key")
306 | if not re.match("^[0-9]+$",id):
307 | return render_template("abstrack.html", error = "Invalid input!")
308 | client.start_session()
309 | mxm = MXM(key, session=client.get_session())
310 | track, album = await mxm.abstrack(id)
311 | await client.close_session()
312 | return render_template("abstrack.html", track=track, album= album, error=track.get("error"))
313 | else:
314 | return render_template("abstrack.html")
315 |
316 |
317 | asgi_app = WsgiToAsgi(app)
318 | if __name__ == '__main__':
319 | import asyncio
320 | from hypercorn.config import Config
321 | from hypercorn.asyncio import serve
322 | asyncio.run(serve(app, Config()))
323 | # app.run(debug=True)
324 |
--------------------------------------------------------------------------------
/Asyncmxm/client.py:
--------------------------------------------------------------------------------
1 | """ A simple Async Python library for the Musixmatch Web API """
2 |
3 |
4 |
5 | import asyncio
6 | import aiohttp
7 | import json
8 |
9 | from Asyncmxm.exceptions import MXMException
10 |
11 | class Musixmatch(object):
12 | """"""
13 |
14 | max_retries = 3
15 | default_retry_codes = (429, 500, 502, 503, 504)
16 |
17 | def __init__(
18 | self,
19 | API_key,
20 | limit = 4,
21 | requests_session=None,
22 | retries=max_retries,
23 | requests_timeout=5,
24 | backoff_factor=0.3,
25 | ):
26 | """
27 | Create a Musixmatch Client.
28 | :param api_key: The API key, Get one at https://developer.musixmatch.com/signup
29 | :param requests_session: A Requests session object or a truthy value to create one.
30 | :param retries: Total number of retries to allow
31 | :param requests_timeout: Stop waiting for a response after a given number of seconds
32 | :param backoff: Factor to apply between attempts after the second try
33 | """
34 |
35 | self._url = "https://api.musixmatch.com/ws/1.1/"
36 | self._key = API_key
37 | self.requests_timeout = requests_timeout
38 | self.backoff_factor = backoff_factor
39 | self.retries = retries
40 | self.limit = limit
41 |
42 | if isinstance(requests_session, aiohttp.ClientSession):
43 | self._session = requests_session
44 | else:
45 | self._build_session()
46 |
47 | def _build_session(self):
48 | connector = aiohttp.TCPConnector(limit=self.limit, limit_per_host=self.limit)
49 | self._session = aiohttp.ClientSession(connector=connector,loop=asyncio.get_event_loop())
50 | '''
51 | async def __aexit__(self, exc_type, exc_value, exc_tb):
52 | """Make sure the connection gets closed"""
53 | await self._session.close()
54 | '''
55 |
56 | async def _api_call(self, method, api_method, params = None):
57 | url = self._url + api_method
58 | if params:
59 | params["apikey"] = self._key
60 | else:
61 | params = {"apikey": self._key}
62 |
63 | retries = 0
64 |
65 | while retries < self.max_retries:
66 | try:
67 | #print(params)
68 | async with self._session.request(method=method, url=str(url), params = params) as response:
69 |
70 | response.raise_for_status()
71 | res = await response.text()
72 | print(res)
73 | res = json.loads(res)
74 | status_code = res["message"]["header"]["status_code"]
75 | if status_code == 200:
76 | return res
77 | else:
78 | retries = self.max_retries
79 | hint = res["message"]["header"].get("hint") or None
80 | raise MXMException(status_code,hint)
81 | except (aiohttp.ClientError, asyncio.TimeoutError) as e:
82 | retries +=1
83 | await asyncio.sleep(self.backoff_factor * retries)
84 | continue
85 | raise Exception("API request failed after retries")
86 |
87 |
88 |
89 | async def track_get(
90 | self,
91 | commontrack_id=None,
92 | track_id=None,
93 | track_isrc=None,
94 | commontrack_vanity_id=None,
95 | track_spotify_id=None,
96 | track_itunes_id=None,
97 | ):
98 | """
99 | Get a track info from their database by objects
100 | Just one Parameter is required
101 | :param commontrack_id: Musixmatch commontrack id
102 | :param track_id: Musixmatch track id
103 | :param track_isrc: A valid ISRC identifier
104 | :param commontrack_vanity_id: Musixmatch vanity id ex "Imagine-Dragons/Demons"
105 | :param track_spotify_id: Spotify Track ID
106 | :param track_itunes_id: Apple track ID
107 | """
108 |
109 | params = {k: v for k, v in locals().items() if v is not None and k !='self'}
110 | return await self._api_call("get", "track.get", params)
111 |
112 | async def matcher_track_get(
113 | self,
114 | q_track=None,
115 | q_artist=None,
116 | q_album=None,
117 | commontrack_id=None,
118 | track_id=None,
119 | track_isrc=None,
120 | commontrack_vanity_id=None,
121 | track_spotify_id=None,
122 | track_itunes_id=None,
123 | **filters,
124 | ):
125 | """
126 | Match your song against Musixmatch database.
127 |
128 | QUERYING: (At least one required)
129 | :param q_track: search for a text string among song titles
130 | :param q_artist: search for a text string among artist names
131 | :param q_album: The song album
132 |
133 | Objects: (optional)
134 | :param commontrack_id: Musixmatch commontrack id
135 | :param track_id: Musixmatch track id
136 | :param track_isrc: A valid ISRC identifier
137 | :param commontrack_vanity_id: Musixmatch vanity id ex "Imagine-Dragons/Demons"
138 | :param track_spotify_id: Spotify Track ID
139 | :param track_itunes_id: Apple track ID
140 |
141 | FILTERING: (optional)
142 | :param f_has_lyrics: Filter by objects with available lyrics
143 | :param f_is_instrumental: Filter instrumental songs
144 | :param f_has_subtitle: Filter by objects with available subtitles (1 or 0)
145 | :param f_music_genre_id: Filter by objects with a specific music category
146 | :param f_subtitle_length: Filter subtitles by a given duration in seconds
147 | :param f_subtitle_length_max_deviation: Apply a deviation to a given subtitle duration (in seconds)
148 | :param f_lyrics_language: Filter the tracks by lyrics language
149 | :param f_artist_id: Filter by objects with a given Musixmatch artist_id
150 | :param f_artist_mbid: Filter by objects with a given musicbrainz artist id
151 |
152 | """
153 |
154 | params = {k: v for k, v in locals().items() if v is not None and k !='self' and k != "filters"}
155 | params = {**params, **filters}
156 | return await self._api_call("get", "matcher.track.get", params)
157 |
158 | async def chart_artists_get(self, page, page_size, country="US"):
159 | """
160 | This api provides you the list of the top artists of a given country.
161 |
162 | :param page: Define the page number for paginated results
163 | :param page_size: Define the page size for paginated results. Range is 1 to 100.
164 | :param country: A valid country code (default US)
165 | """
166 | params = {k: v for k, v in locals().items() if v is not None and k !='self'}
167 | return await self._api_call("get", "chart.artists.get", params)
168 |
169 | async def chart_tracks_get(
170 | self, chart_name,page = 1, page_size = 100, f_has_lyrics = 1, country="US"
171 | ):
172 | """
173 | This api provides you the list of the top artists of a given country.
174 |
175 | :param page: Define the page number for paginated results
176 | :param page_size: Define the page size for paginated results. Range is 1 to 100.
177 | :param chart_name: Select among available charts:
178 | top : editorial chart
179 | hot : Most viewed lyrics in the last 2 hours
180 | mxmweekly : Most viewed lyrics in the last 7 days
181 | mxmweekly_new : Most viewed lyrics in the last 7 days limited to new releases only
182 | :param f_has_lyrics: When set, filter only contents with lyrics, Takes (0 or 1)
183 | :param country: A valid country code (default US)
184 | """
185 | params = {k: v for k, v in locals().items() if v is not None and k !='self'}
186 | return await self._api_call("get", "chart.tracks.get", params)
187 |
188 | async def track_search(self, page = 1, page_size = 100, **params):
189 | """
190 | Search for track in Musixmatch database.
191 |
192 | :param q_track: The song title
193 | :param q_artist: The song artist
194 | :param q_lyrics: Any word in the lyrics
195 | :param q_track_artist: Any word in the song title or artist name
196 | :param q_writer: Search among writers
197 | :param q: Any word in the song title or artist name or lyrics
198 | :param f_artist_id: When set, filter by this artist id
199 | :param f_music_genre_id: When set, filter by this music category id
200 | :param f_lyrics_language: Filter by the lyrics language (en,it,..)
201 | :param f_has_lyrics: When set, filter only contents with lyrics
202 | :param f_track_release_group_first_release_date_min:
203 | When set, filter the tracks with release date newer than value, format is YYYYMMDD
204 | :param f_track_release_group_first_release_date_max:
205 | When set, filter the tracks with release date older than value, format is YYYYMMDD
206 | :param s_artist_rating: Sort by our popularity index for artists (asc|desc)
207 | :param s_track_rating: Sort by our popularity index for tracks (asc|desc)
208 | :param quorum_factor: Search only a part of the given query string.Allowed range is (0.1 - 0.9)
209 | :param page: Define the page number for paginated results
210 | :param page_size: Define the page size for paginated results. Range is 1 to 100.
211 | """
212 | locs = locals().copy()
213 | locs.pop("params")
214 | params = {**params, **locs}
215 | return await self._api_call("get", "track.search", params)
216 |
217 | async def track_lyrics_get(self, commontrack_id=None, track_id=None, track_spotify_id=None):
218 | """
219 | Get the lyrics of a track.
220 |
221 | :param commontrack_id: Musixmatch commontrack id
222 | :param track_id: Musixmatch track id
223 | :param track_spotify_id: Spotify Track ID
224 | """
225 | params = {k: v for k, v in locals().items() if v is not None and k !='self'}
226 | return await self._api_call("get", "track.lyrics.get", params)
227 |
228 | async def track_lyrics_post(self, lyrics:str, commontrack_id=None, track_isrc=None):
229 | """
230 | Submit a lyrics to Musixmatch database.
231 |
232 | :param lyrics: The lyrics to be submitted
233 | :param commontrack_id: The track commontrack
234 | :param track_isrc: A valid ISRC identifier
235 | """
236 | params = {k: v for k, v in locals().items() if v is not None and k !='self'}
237 | return await self._api_call("post", "track.lyrics.post", params)
238 |
239 | async def track_lyrics_mood_get(self,commontrack_id=None, track_isrc=None):
240 | """
241 | Get the mood list (and raw value that generated it) of a lyrics
242 |
243 | :note: Not available for the free plan
244 |
245 | :param commontrack_id: The track commontrack
246 | :param track_isrc: A valid ISRC identifier
247 | """
248 | params = {k: v for k, v in locals().items() if v is not None and k !='self'}
249 | return await self._api_call("get", "track.lyrics.mood.get", params)
250 |
251 | async def track_snippet_get(self,commontrack_id=None,
252 | track_id=None,
253 | track_isrc=None,
254 | track_spotify_id=None
255 | ):
256 | """
257 | Get the snippet for a given track.
258 | A lyrics snippet is a very short representation of a song lyrics.
259 | It's usually twenty to a hundred characters long
260 |
261 | :param commontrack_id: The track commontrack
262 | :param track_id: Musixmatch track id
263 | :param track_isrc: A valid ISRC identifier
264 | :param track_spotify_id: Spotify Track ID
265 | """
266 |
267 | params = {k: v for k, v in locals().items() if v is not None and k !='self'}
268 | return await self._api_call("get", "track.snippet.get", params)
269 |
270 | async def track_subtitle_get(self,commontrack_id=None,
271 | track_id=None,
272 | subtitle_format = None,
273 | track_isrc=None,
274 | f_subtitle_length = None,
275 | f_subtitle_length_max_deviation = None
276 | ):
277 | """
278 | Retreive the subtitle of a track.
279 | Return the subtitle of a track in LRC or DFXP format.
280 |
281 | :param commontrack_id: The track commontrack
282 | :param track_id: Musixmatch track id
283 | :param track_isrc: A valid ISRC identifier
284 | :param subtitle_format: The format of the subtitle (lrc,dfxp,stledu). Default to lrc
285 | :param f_subtitle_length: The desired length of the subtitle (seconds)
286 | :param f_subtitle_length_max_deviation: The maximum deviation allowed from the f_subtitle_length (seconds)
287 | """
288 |
289 | params = {k: v for k, v in locals().items() if v is not None and k !='self'}
290 | return await self._api_call("get", "track.subtitle.get", params)
291 |
292 | async def track_richsync_get(self,commontrack_id=None,
293 | track_id=None,
294 | track_isrc=None,
295 | track_spotify_id=None,
296 | f_richsync_length = None,
297 | f_richsync_length_max_deviation = None
298 | ):
299 | """
300 | A rich sync is an enhanced version of the standard sync.
301 |
302 | :param commontrack_id: The track commontrack
303 | :param track_id: Musixmatch track id
304 | :param track_isrc: A valid ISRC identifier
305 | :param track_spotify_id: Spotify Track ID
306 | :param f_richsync_length: The desired length of the sync (seconds)
307 | :param f_richsync_length_max_deviation: The maximum deviation allowed from the f_sync_length (seconds)
308 | """
309 |
310 | params = {k: v for k, v in locals().items() if v is not None and k !='self'}
311 | return await self._api_call("get", "track.richsync.get", params)
312 |
313 | async def track_lyrics_translation_get(self,commontrack_id=None,
314 | track_id=None,
315 | track_isrc=None,
316 | track_spotify_id=None,
317 | selected_language = None,
318 | min_completed = None
319 | ):
320 | """
321 | Get a translated lyrics for a given language
322 |
323 | :param commontrack_id: The track commontrack
324 | :param track_id: Musixmatch track id
325 | :param track_isrc: A valid ISRC identifier
326 | :param track_spotify_id: Spotify Track ID
327 | :param selected_language: he language of the translated lyrics (ISO 639-1)
328 | :param min_completed: Teal from 0 to 1. If present,
329 | only the tracks with a translation ratio over this specific value,
330 | for a given language, are returned Set it to 1 for completed translation only, to 0.7 for a mimimum of 70% complete translation.
331 | :param f_subtitle_length: The desired length of the subtitle (seconds)
332 | :param f_subtitle_length_max_deviation: The maximum deviation allowed from the f_subtitle_length (seconds)
333 | """
334 |
335 | params = {k: v for k, v in locals().items() if v is not None and k !='self'}
336 | return await self._api_call("get", "track.lyrics.translation.get", params)
337 |
338 | async def track_subtitle_translation_get(self,commontrack_id=None,
339 | track_id=None,
340 | track_isrc=None,
341 | track_spotify_id=None,
342 | selected_language = None,
343 | min_completed = None,
344 | f_subtitle_length = None,
345 | f_subtitle_length_max_deviation = None
346 | ):
347 | """
348 | Get a translated subtitle for a given language
349 |
350 | :param commontrack_id: The track commontrack
351 | :param track_id: Musixmatch track id
352 | :param track_isrc: A valid ISRC identifier
353 | :param track_spotify_id: Spotify Track ID
354 | :param selected_language: he language of the translated lyrics (ISO 639-1)
355 | :param min_completed: Teal from 0 to 1. If present,
356 | only the tracks with a translation ratio over this specific value,
357 | for a given language, are returned Set it to 1 for completed translation only, to 0.7 for a mimimum of 70% complete translation.
358 | """
359 |
360 | params = {k: v for k, v in locals().items() if v is not None and k !='self'}
361 | return await self._api_call("get", "track.subtitle.translation.get", params)
362 |
363 | async def music_genres_get(self):
364 | """
365 | Get the list of the music genres of our catalogue.
366 | """
367 | return await self._api_call("get", "music.genres.get")
368 |
369 | async def matcher_lyrics_get(
370 | self,
371 | q_track=None,
372 | q_artist=None,
373 | q_album=None,
374 | commontrack_id=None,
375 | track_id=None,
376 | track_isrc=None,
377 | commontrack_vanity_id=None,
378 | track_spotify_id=None,
379 | track_itunes_id=None,
380 | **filters,
381 | ):
382 | """
383 | Get the lyrics for track based on title and artist
384 |
385 | QUERYING: (At least one required)
386 | :param q_track: search for a text string among song titles
387 | :param q_artist: search for a text string among artist names
388 | :param q_album: The song album
389 |
390 | Objects: (optional)
391 | :param commontrack_id: Musixmatch commontrack id
392 | :param track_id: Musixmatch track id
393 | :param track_isrc: A valid ISRC identifier
394 | :param commontrack_vanity_id: Musixmatch vanity id ex "Imagine-Dragons/Demons"
395 | :param track_spotify_id: Spotify Track ID
396 | :param track_itunes_id: Apple track ID
397 |
398 | FILTERING: (optional)
399 | :param f_subtitle_length: The desired length of the subtitle (seconds)
400 | :param f_subtitle_length_max_deviation: The maximum deviation allowed from the f_subtitle_length (seconds)
401 | :param f_has_lyrics: Filter by objects with available lyrics
402 | :param f_is_instrumental: Filter instrumental songs
403 | :param f_has_subtitle: Filter by objects with available subtitles (1 or 0)
404 | :param f_music_genre_id: Filter by objects with a specific music category
405 | :param f_lyrics_language: Filter the tracks by lyrics language
406 | :param f_artist_id: Filter by objects with a given Musixmatch artist_id
407 | :param f_artist_mbid: Filter by objects with a given musicbrainz artist id
408 |
409 | """
410 |
411 | params = {k: v for k, v in locals().items() if v is not None and k !='self' and k != "filters"}
412 | params = {**params, **filters}
413 | return await self._api_call("get", "matcher.lyrics.get", params)
414 |
415 | async def matcher_subtitle_get(
416 | self,
417 | q_track=None,
418 | q_artist=None,
419 | q_album=None,
420 | commontrack_id=None,
421 | track_id=None,
422 | track_isrc=None,
423 | commontrack_vanity_id=None,
424 | track_spotify_id=None,
425 | track_itunes_id=None,
426 | **filters,
427 | ):
428 | """
429 | Get the subtitles for a song given his title,artist and duration.
430 | You can use the f_subtitle_length_max_deviation to fetch subtitles within a given duration range.
431 |
432 |
433 | QUERYING: (At least one required)
434 | :param q_track: search for a text string among song titles
435 | :param q_artist: search for a text string among artist names
436 | :param q_album: The song album
437 |
438 | Objects: (optional)
439 | :param commontrack_id: Musixmatch commontrack id
440 | :param track_id: Musixmatch track id
441 | :param track_isrc: A valid ISRC identifier
442 | :param commontrack_vanity_id: Musixmatch vanity id ex "Imagine-Dragons/Demons"
443 | :param track_spotify_id: Spotify Track ID
444 | :param track_itunes_id: Apple track ID
445 |
446 | FILTERING: (optional)
447 | :param f_subtitle_length: The desired length of the subtitle (seconds)
448 | :param f_subtitle_length_max_deviation: The maximum deviation allowed from the f_subtitle_length (seconds)
449 | :param f_has_lyrics: Filter by objects with available lyrics
450 | :param f_is_instrumental: Filter instrumental songs
451 | :param f_has_subtitle: Filter by objects with available subtitles (1 or 0)
452 | :param f_music_genre_id: Filter by objects with a specific music category
453 | :param f_lyrics_language: Filter the tracks by lyrics language
454 | :param f_artist_id: Filter by objects with a given Musixmatch artist_id
455 | :param f_artist_mbid: Filter by objects with a given musicbrainz artist id
456 |
457 | """
458 |
459 | params = {k: v for k, v in locals().items() if v is not None and k !='self' and k != "filters"}
460 | params = {**params, **filters}
461 | return await self._api_call("get", "matcher.subtitle.get", params)
462 |
463 | async def artist_get(self, artist_id):
464 | """
465 | Get the artist data.
466 |
467 | :param artist_id: Musixmatch artist id
468 |
469 | """
470 | return await self._api_call("get", "artist.get", locals())
471 |
472 | async def artist_search(self,
473 | q_artist,
474 | page = 1,
475 | page_size = 100,
476 | f_artist_id = None
477 | ):
478 | """
479 | Search for artists
480 |
481 | :param q_artist: The song artist
482 | :param page: Define the page number for paginated results
483 | :param page_size: Define the page size for paginated results. Range is 1 to 100.
484 | :param f_artist_id: When set, filter by this artist id
485 | """
486 | params = {k: v for k, v in locals().items() if v is not None and k !='self'}
487 | return await self._api_call("get", "artist.search", params)
488 |
489 | async def artist_albums_get(self,
490 | artist_id,
491 | page = 1,
492 | page_size = 100,
493 | g_album_name = 1,
494 | s_release_date = "desc"
495 | ):
496 | """
497 | Get the album discography of an artist
498 |
499 | :param q_artist: The song artist
500 | :param page: Define the page number for paginated results
501 | :param page_size: Define the page size for paginated results. Range is 1 to 100.
502 | :param g_album_name: Group by Album Name
503 | :param s_release_date: Sort by release date (asc|desc)
504 | """
505 | params = {k: v for k, v in locals().items() if v is not None and k !='self'}
506 | return await self._api_call("get", "artist.albums.get", params)
507 |
508 | async def artist_related_get(self,
509 | artist_id,
510 | page = 1,
511 | page_size = 100,
512 | ):
513 | """
514 | Get a list of artists somehow related to a given one.
515 |
516 | :param q_artist: The song artist
517 | :param page: Define the page number for paginated results
518 | :param page_size: Define the page size for paginated results. Range is 1 to 100.
519 | """
520 | params = {k: v for k, v in locals().items() if v is not None and k !='self'}
521 | return await self._api_call("get", "artist.related.get", params)
522 |
523 | async def album_get(self, album_id=None,album_vanity_id=None):
524 | """
525 | Get the album object using the musixmatch id.
526 |
527 | :param album_id: The musixmatch album id.
528 | """
529 | params = {k: v for k, v in locals().items() if v is not None and k !='self'}
530 | return await self._api_call("get", "album.get", params)
531 |
532 | async def album_tracks_get(self, album_id,
533 | f_has_lyrics = 0,
534 | page = 1,
535 | page_size = 100
536 | ):
537 | """
538 | This api provides you the list of the songs of an album.
539 |
540 | :param album_id: The musixmatch album id.
541 | :param f_has_lyrics: When set, filter only contents with lyrics.
542 | :param page: Define the page number for paginated results
543 | :param page_size: Define the page size for paginated results. Range is 1 to 100.
544 | """
545 | return await self._api_call("get", "album.tracks.get", locals())
--------------------------------------------------------------------------------