Skip to content

Commit

Permalink
Add enhancedLRC support (#24)
Browse files Browse the repository at this point in the history
Adds support for enhanced (word-level) synced lyrics using Musixmatch API. Resolved #24
  • Loading branch information
moehmeni committed Mar 15, 2024
1 parent 52b93e2 commit 2d0c523
Show file tree
Hide file tree
Showing 6 changed files with 57 additions and 5 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ syncedlyrics "SEARCH_TERM"
| `-l` | Language code of the translation ([ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) format) |
| `-v` | Use this flag to show the logs |
| `--allow-plain` | Return a plain text (not synced) lyrics if no LRC format was found |
| `--enhanced` | Return an [Enhanced](https://en.wikipedia.org/wiki/LRC_(file_format)#A2_extension:_word_time_tag) (word-level karaoke) format

### Python
```py
Expand Down
13 changes: 12 additions & 1 deletion syncedlyrics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def search(
save_path: Optional[str] = None,
providers: Optional[List[str]] = None,
lang: Optional[str] = None,
enhanced: bool = False,
) -> Optional[str]:
"""
Returns the synced lyrics of the song in [LRC](https://en.wikipedia.org/wiki/LRC_(file_format)) format if found.
Expand All @@ -31,8 +32,15 @@ def search(
- `save_path`: Path to save `.lrc` lyrics. No saving if `None`
- `providers`: A list of provider names to include in searching; loops over all the providers as soon as an LRC is found
- `lang`: Language of the translation along with the lyrics. **Only supported by Musixmatch**
- `enhanced`: Returns word by word synced lyrics if available. **Only supported by Musixmatch**
"""
_providers = [Musixmatch(lang=lang), Lrclib(), Deezer(), NetEase(), Megalobiz()]
_providers = [
Musixmatch(lang=lang, enhanced=enhanced),
Lrclib(),
Deezer(),
NetEase(),
Megalobiz(),
]
if providers and any(providers):
# Filtering the providers
_providers = [
Expand All @@ -49,6 +57,9 @@ def search(
for provider in _providers:
logger.debug(f"Looking for an LRC on {provider.__class__.__name__}")
lrc = provider.get_lrc(search_term)
if enhanced and not lrc:
# Since enhanced is only supported by Musixmatch, break if no LRC is found
break
if is_lrc_valid(lrc, allow_plain_format):
logger.info(
f'synced-lyrics found for "{search_term}" on {provider.__class__.__name__}'
Expand Down
12 changes: 11 additions & 1 deletion syncedlyrics/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,21 @@ def cli_handler():
help="Return a plain text (not synced) lyrics if not LRC was found",
action="store_true",
)
parser.add_argument(
"--enhanced",
help="Returns word by word synced lyrics (if available)",
action="store_true",
)
args = parser.parse_args()
if args.verbose:
logging.basicConfig(level=logging.DEBUG)
lrc = search(
args.search_term, args.allow_plain, args.output, args.p, lang=args.lang
args.search_term,
args.allow_plain,
args.output,
args.p,
lang=args.lang,
enhanced=args.enhanced,
)
if lrc:
print(lrc)
24 changes: 21 additions & 3 deletions syncedlyrics/providers/musixmatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import json
import os
from .base import LRCProvider
from ..utils import get_best_match
from ..utils import get_best_match, format_time

# Inspired from https://github.com/Marekkon5/onetagger/blob/0654131188c4df2b4b171ded7cdb927a4369746e/crates/onetagger-platforms/src/musixmatch.rs
# Huge part converted from Rust to Py by ChatGPT :)
Expand All @@ -16,9 +16,10 @@ class Musixmatch(LRCProvider):

ROOT_URL = "https://apic-desktop.musixmatch.com/ws/1.1/"

def __init__(self, lang: Optional[str] = None) -> None:
def __init__(self, lang: Optional[str] = None, enhanced: bool = False) -> None:
super().__init__()
self.lang = lang
self.enhanced = enhanced
self.token = None

def _get(self, action: str, query: List[tuple]):
Expand Down Expand Up @@ -90,6 +91,20 @@ def get_lrc_by_id(self, track_id: str) -> Optional[str]:
lrc = lrc.replace(org, org + "\n" + f"({tr})")
return lrc

def get_lrc_word_by_word(self, track_id: str) -> Optional[str]:
r = self._get("track.richsync.get", [("track_id", track_id)])
if r.ok and r.json()["message"]["header"]["status_code"] == 200:
lrc_raw = r.json()["message"]["body"]["richsync"]["richsync_body"]
lrc_raw = json.loads(lrc_raw)
lrc = ""
for i in lrc_raw:
lrc += f"[{format_time(i['ts'])}] "
for l in i["l"]:
t = format_time(float(i["ts"]) + float(l["o"]))
lrc += f"<{t}> {l['c']} "
lrc += "\n"
return lrc

def get_lrc(self, search_term: str) -> Optional[str]:
r = self._get(
"track.search",
Expand All @@ -107,4 +122,7 @@ def get_lrc(self, search_term: str) -> Optional[str]:
track = get_best_match(tracks, search_term, cmp_key)
if not track:
return None
return self.get_lrc_by_id(track["track"]["track_id"])
track_id = track["track"]["track_id"]
if self.enhanced:
return self.get_lrc_word_by_word(track_id)
return self.get_lrc_by_id(track_id)
8 changes: 8 additions & 0 deletions syncedlyrics/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from bs4 import BeautifulSoup, FeatureNotFound
import rapidfuzz
from typing import Union, Callable, Optional
import datetime
import re

R_FEAT = re.compile(r"\((feat.+)\)", re.IGNORECASE)
Expand Down Expand Up @@ -37,6 +38,13 @@ def generate_bs4_soup(session, url: str, **kwargs):
return soup


def format_time(time_in_seconds: float):
"""Returns a [mm:ss.xx] formatted string from the given time in seconds."""
time = datetime.timedelta(seconds=time_in_seconds)
minutes, seconds = divmod(time.seconds, 60)
return f"{minutes:02}:{seconds:02}.{time.microseconds//10000:02}"


def str_score(a: str, b: str) -> float:
"""Returns the similarity score of the two strings"""
# if user does not specify any "feat" in the search term,
Expand Down
4 changes: 4 additions & 0 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ def test_musixmatch_translation():
_test_provider("Musixmatch", lang="es")


def test_musixmatch_enhanced():
_test_provider("Musixmatch", enhanced=True)


def test_lrclib():
_test_provider("Lrclib")

Expand Down

0 comments on commit 2d0c523

Please sign in to comment.