diff options
| author | TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> | 2026-06-25 17:42:40 +0900 |
|---|---|---|
| committer | TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> | 2026-06-25 17:42:40 +0900 |
| commit | acff8d35ad55ccae03fe9c73ab6c53aeb316324e (patch) | |
| tree | 1caa02e8670303a4073fc3616362db37d0c88c26 | |
| parent | 65836f3fbe366f486673f3c42043d3c07e53e046 (diff) | |
modified ncmpcpp/config, modified Music/.music.txt, modified Music/.music_titles.txt, created ncmpcpp/genius-lyrics.pyHEADmaster
| -rw-r--r-- | ar/.config/ncmpcpp/config | 8 | ||||
| -rwxr-xr-x | ar/.config/ncmpcpp/genius-lyrics.py | 175 | ||||
| -rw-r--r-- | global/Music/.music.txt | 2 | ||||
| -rw-r--r-- | global/Music/.music_titles.txt | 56 |
4 files changed, 209 insertions, 32 deletions
diff --git a/ar/.config/ncmpcpp/config b/ar/.config/ncmpcpp/config index 6cea647..5e90e43 100644 --- a/ar/.config/ncmpcpp/config +++ b/ar/.config/ncmpcpp/config @@ -335,7 +335,7 @@ song_columns_list_format = (20)[cyan]{a} (40)[yellow]{t} (30)[blue]{b} (10)[mage ## Note: Custom command that will be executed each time song changes. Useful for ## notifications etc. ## -execute_on_song_change="pkill -RTMIN+2 dwmblocks" +execute_on_song_change="pkill -RTMIN+2 dwmblocks; (~/.config/ncmpcpp/genius-lyrics.py >/dev/null 2>&1 &)" # ## ## Note: Custom command that will be executed each time player state @@ -421,7 +421,11 @@ media_library_albums_split_by_date = no # #cyclic_scrolling = no # -#lyrics_fetchers = tags, genius, tekstowo, plyrics, justsomelyrics, jahlyrics, zeneszoveg, internet +## Only `tags` (embedded lyrics) is kept here. The broken built-in `genius` +## scraper is removed on purpose; lyrics downloading is handled by +## ~/.config/ncmpcpp/genius-lyrics.py (run from execute_on_song_change). +## To restore ncmpcpp's own fetchers, re-add: genius, tekstowo, plyrics, ... +lyrics_fetchers = tags # #follow_now_playing_lyrics = no # diff --git a/ar/.config/ncmpcpp/genius-lyrics.py b/ar/.config/ncmpcpp/genius-lyrics.py new file mode 100755 index 0000000..98a0292 --- /dev/null +++ b/ar/.config/ncmpcpp/genius-lyrics.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +"""Fetch lyrics from Genius for the current (or given) song and write them +in the flat `<artist> - <title>.txt` form that ncmpcpp reads. + +Why this exists: ncmpcpp 0.10.1's built-in Genius scraper stops at the first +</div> inside the lyrics container. Genius now nests a LyricsHeader div there, +so ncmpcpp only captures the header ("N Contributors<title> Lyrics") and drops +the actual lyrics. This script parses the current Genius structure correctly. + +Usage: + genius-lyrics.py # uses `mpc current` for artist/title + genius-lyrics.py "Artist" "Title" + genius-lyrics.py --force ... # refetch even if a good file already exists + +Designed to be safe to call from ncmpcpp's execute_on_song_change: it never +raises to the caller, runs quietly, and skips songs that already have lyrics. +""" +from __future__ import annotations + +import html +import json +import os +import re +import subprocess +import sys +import urllib.parse +import urllib.request + +LYRICS_DIR = os.path.expanduser( + os.environ.get("NCMPCPP_LYRICS_DIR", "~/.local/share/lyrics") +) +UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36" +TIMEOUT = 12 + +# Content that means a previous (broken) scrape, so we should refetch: +# e.g. "2 Contributorsgestalt Lyrics" +BROKEN_RE = re.compile(r"^\s*\d+\s+Contributors?.*Lyrics\s*$", re.IGNORECASE | re.DOTALL) +MIN_VALID_LEN = 60 # real lyrics are always longer than this + + +def get(url: str) -> str: + req = urllib.request.Request(url, headers={"User-Agent": UA}) + with urllib.request.urlopen(req, timeout=TIMEOUT) as r: + return r.read().decode("utf-8", "replace") + + +def current_song(): + try: + out = subprocess.run( + ["mpc", "current", "-f", "%artist%\t%title%"], + capture_output=True, text=True, timeout=5, + ).stdout.strip() + except Exception: + return None + if not out or "\t" not in out: + return None + artist, title = out.split("\t", 1) + if not artist or not title: + return None + return artist, title + + +def lyrics_path(artist: str, title: str) -> str: + # ncmpcpp stores lyrics flat as "<artist> - <title>.txt"; '/' is unsafe. + name = f"{artist} - {title}.txt".replace("/", "_") + return os.path.join(LYRICS_DIR, name) + + +def needs_fetch(path: str, force: bool) -> bool: + if force or not os.path.exists(path): + return True + try: + with open(path, encoding="utf-8") as f: + content = f.read() + except OSError: + return True + return len(content.strip()) < MIN_VALID_LEN or bool(BROKEN_RE.match(content)) + + +def _norm(s: str) -> str: + # Keep only alphanumerics (Unicode-aware: Korean stays), casefolded. + return "".join(ch for ch in s.casefold() if ch.isalnum()) + + +def _artist_matches(query_artist: str, hit_artist: str) -> bool: + a, b = _norm(query_artist), _norm(hit_artist) + if not a or not b: + return False + return a in b or b in a + + +def genius_url(artist: str, title: str): + q = urllib.parse.quote(f"{artist} {title}") + try: + data = json.loads(get(f"https://genius.com/api/search/multi?q={q}")) + except Exception: + return None + # Only accept a hit whose artist matches the song's artist. Genius search + # happily returns unrelated songs when it lacks the track; writing those + # would be worse than writing nothing. + for section in data.get("response", {}).get("sections", []): + for hit in section.get("hits", []): + if hit.get("type") != "song": + continue + result = hit.get("result", {}) + hit_artist = result.get("primary_artist", {}).get("name", "") + if _artist_matches(artist, hit_artist): + return result.get("url") + return None + + +def extract_lyrics(page: str) -> str: + # Every verse lives in a data-lyrics-container; concatenate them all. + parts = re.findall( + r'data-lyrics-container="true"[^>]*>(.*?)</div>' + r'(?=\s*<div data-lyrics-container|\s*<div[^>]*class="[^"]*RightSidebar' + r'|\s*</div>\s*<div[^>]*StyledLink|$)', + page, re.S, + ) + if not parts: + parts = re.findall(r'data-lyrics-container="true"[^>]*>(.*)', page, re.S) + text = "".join(parts) + # Drop the nested header block (Contributors / "<song> Lyrics") that broke ncmpcpp. + text = re.sub( + r'<div[^>]*data-exclude-from-selection="true".*?</div>\s*</div>\s*</div>', + "", text, flags=re.S, + ) + text = re.sub(r"<br\s*/?>", "\n", text) # line breaks + text = re.sub(r"<[^>]+>", "", text) # strip remaining tags + return html.unescape(text).strip() + + +def write_atomic(path: str, content: str) -> None: + os.makedirs(os.path.dirname(path), exist_ok=True) + tmp = path + ".tmp" + with open(tmp, "w", encoding="utf-8") as f: + f.write(content + "\n") + os.replace(tmp, path) + + +def main(argv) -> int: + force = "--force" in argv + argv = [a for a in argv if a != "--force"] + + if len(argv) >= 2: + artist, title = argv[0], argv[1] + else: + song = current_song() + if not song: + return 0 # nothing playing; nothing to do + artist, title = song + + path = lyrics_path(artist, title) + if not needs_fetch(path, force): + return 0 + + url = genius_url(artist, title) + if not url: + return 0 + try: + lyrics = extract_lyrics(get(url)) + except Exception: + return 0 + if len(lyrics) < MIN_VALID_LEN or BROKEN_RE.match(lyrics): + return 0 # don't overwrite with garbage + + write_atomic(path, lyrics) + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main(sys.argv[1:])) + except Exception: + sys.exit(0) # never disrupt ncmpcpp's song-change command diff --git a/global/Music/.music.txt b/global/Music/.music.txt index ed47e01..61cc35e 100644 --- a/global/Music/.music.txt +++ b/global/Music/.music.txt @@ -222,7 +222,6 @@ youtube hxLXdCvLHfs youtube jJzw1h5CR-I youtube pOnXgF4wTmc youtube b_bjtIeqIR4 -youtube uMeR2W19wT0 youtube vGoMNejaSy0 youtube dGZqpVCJP3k youtube 64ftDYJXcTo @@ -373,7 +372,6 @@ youtube G1ej5up7JG0 youtube kPlSyYtE63M youtube IMWT6937uUs youtube -kcZNuFYnns -youtube Kdl3erX8kA4 youtube q-I62ICVEeE youtube 1kHNU5KsH5g youtube Yc4gp6oeN7k diff --git a/global/Music/.music_titles.txt b/global/Music/.music_titles.txt index 191ec90..d528ed3 100644 --- a/global/Music/.music_titles.txt +++ b/global/Music/.music_titles.txt @@ -155,7 +155,7 @@ kcx0a2OAhN0 IU - Love poem ybiINVA_gLc IU - Ending Scene (이런 엔딩) 1MnzUWjBQog IU - The shower (푸르던) EQzqq0LNlto IU - Rain Drop -5CXTwUrotf4 라따뚜이 - 레오 Leo - 콧시 CozySea cover (원곡:優里 Yuuri - Leo) +5CXTwUrotf4 콧시 CozySea - Leo (cover) cvQQEs4TDGM BIBI - Bam Yang Gang (밤양갱) 9TiGuWyGlXc 10CM - Tell Me It's Not a Dream zFpoaELOrTo Crush - Love You With All My Heart @@ -173,23 +173,23 @@ yuTesDwv6uA DAOKO & Kenshi Yonezu - 打上花火 TQ8WlA2GXbk OFFICIAL HIGE DANDISM - Pretender m9SMT5ipbxk YOASOBI - アイドル _PNFMuT6e4o King Gnu - Hakujitsu -iuJDhFRDx9M Master ZX - Tokyo Drift - Teriyaki Boyz [ MUSIC VIDEO ] HD +iuJDhFRDx9M Teriyaki Boyz - Tokyo Drift 1tk1pqwrOys Eve - Kaikai Kitan 4r6L3FUhbIo YOASOBI - Monster --EKxzId_Sj4 Kenshi Yonezu - 米津玄師 - アイネクライネ , Kenshi Yonezu - Eine Kleine +-EKxzId_Sj4 Kenshi Yonezu - アイネクライネ 0xSiBpUdW4E あいみょん - Marigold -Dx_fKPBPYUI Kenshi Yonezu - 米津玄師 - LOSER , Kenshi Yonezu +Dx_fKPBPYUI Kenshi Yonezu - LOSER GtPLYvYeZ_4 Ado - Usseewa diH9qVVUSNo NiziU - Make you happy NinLIDs9qmU LiSA - homura qp0AktOIAag RADWIMPS - Zenzenzense - movie ver. zqKrpw36xnU KANA-BOON - Silhouette -9aJVr5tTTWk Kenshi Yonezu - 米津玄師 - ピースサイン , Kenshi Yonezu - Peace Sign +9aJVr5tTTWk Kenshi Yonezu - ピースサイン -Jget2NBi30 紗倉ひびき(CV:ファイルーズあい)&街雄鳴造(CV:石川界人) - お願いマッスル x8VYWazR5mE YOASOBI - Yoru ni Kakeru wNjvuRZtQeI Mrs. GREEN APPLE - Inferno RjDEFe8dw0A DA PUMP - U.S.A. -gJX2iy6nhHc Masaki Suda & Kenshi Yonezu - 米津玄師 MV「 灰色と青( +菅田将暉 )」 +gJX2iy6nhHc Kenshi Yonezu - 灰色と青 (+菅田将暉) by4SYYWlhEs YOASOBI - 夜に駆ける KMdTrqzEI0I Gen Hoshino - Koi b9dBi-Sd4SI Kenshi Yonezu - Paprika @@ -208,16 +208,16 @@ gxp3R7l1iSk minami - Kawakiwoameku pdvnR6cmI5U Masaki Suda - Machigaisagashi 0t0HtFYjcU8 imase - NIGHT DANCER C7YQq-nup3A Ado - Odo -ptnYBctoexk Kenshi Yonezu - 米津玄師 - 馬と鹿 Kenshi Yonezu - Uma to Shika -bt8wNQJaKAk OFFICIAL HIGE DANDISM - Official髭男dism - I LOVE...[Official Video] +ptnYBctoexk Kenshi Yonezu - 馬と鹿 +bt8wNQJaKAk Official髭男dism - I LOVE... yzC4hFK5P3g Kyary Pamyu Pamyu - Pon Pon Pon anOu7GvEKpI BABYMETAL - Gimme Chocolate!! -J9FuvPmMoI RADWIMPS - Sparkle - movie ver. -DuMqFknYHBs Official髭男dism - Official髭男dism - イエスタデイ[Official Video] +DuMqFknYHBs Official髭男dism - イエスタデイ CVe_AuaoQJk Kenshi Yonezu - Flamingo 3wu_p2s3h1g Columbia Music Box - Orion (Kenshi Yonezu) 3zh9Wb1KuW8 Fujii Kaze - Shinunoga E-Wa -J5Z7tIq7bco スキマスイッチ - 「奏(かなで)」Music Video : SUKIMASWITCH / KANADE Music Video +J5Z7tIq7bco スキマスイッチ - 奏(かなで) hxLXdCvLHfs AKB48 - Heavy Rotation jJzw1h5CR-I Eve - Dramaturgy pOnXgF4wTmc J SOUL BROTHERS III from EXILE TRIBE - R.Y.U.S.E.I. @@ -228,7 +228,7 @@ dGZqpVCJP3k YOASOBI - 群青 qT2ZeIWhcno Masaki Suda - Sayonara Elegy yOAwvRmVIyo あいみょん - Naked Heart IHaUGQVrBa8 THE ORAL CIGARETTES - Kyouran Hey Kids!! -CID-sYQNCew animelab - Attack on Titan Season 2 - Official Opening Song - Shinzou wo Sasageyo by Linked Horizon +CID-sYQNCew Linked Horizon - Shinzou wo Sasageyo 96IPIQjohZ4 WANIMA - Tomoni 1jaK9CT_omc Eito - Kousui Dt0CYNAYfNY Ado - Gira Gira @@ -236,7 +236,7 @@ VJDbgtc692k Ado - Show qhmrt3ZNgcs Ado - Dried Flowers QnAUQvrjuP0 King & Prince - Tsukiyomi ViOzYSYWCMM Ado - New Genesis (UTA from ONE PIECE FILM RED) -zkNzxsaCunU Kenshi Yonezu - 米津玄師 - 春雷 Kenshi Yonezu - Shunrai +zkNzxsaCunU Kenshi Yonezu - 春雷 wJuef4IAt4k WagakkiBand - Senbonzakura mMlFlUZDZVQ Superfly - Aiwo Komete Hanatabawo (From THE FIRST TAKE) F8xpmTmdf6o MONOGATARI Series - Renai Circulation @@ -246,25 +246,25 @@ WHdL2vstvL8 ヨルシカ - 言って。 YL9_wPbbvQo Masaki Suda - Niji 6gMBmZ28Xg4 Creepy Nuts - Bling-Bang-Bang-Born -QxMzUEJH4Q Mrs. GREEN APPLE - Ao To Natsu --kgOFJG881I OFFICIAL HIGE DANDISM - Official髭男dism - 宿命[Official Video] +-kgOFJG881I Official髭男dism - 宿命 mp2-w15SXms Tani Yuuki - W / X / Y EwpKUV0ECvQ yama - Haru wo Tsugeru -O1bhZgkC4Gw OFFICIAL HIGE DANDISM - Official髭男dism - Cry Baby[Official Video] +O1bhZgkC4Gw Official髭男dism - Cry Baby N2A2VZTl9X0 Iori Kanzaki - Hated by Life 9mWbCPJuoIo frederic - Oddloop -EHw005ZqCXk OFFICIAL HIGE DANDISM - Official髭男dism - ノーダウト[Official Video] +EHw005ZqCXk Official髭男dism - ノーダウト waYhTInn7GU 神山 羊 - YELLOW -s582L3gujnw Kenshi Yonezu - 米津玄師 - パプリカ Kenshi Yonezu - PAPRIKA +s582L3gujnw Kenshi Yonezu - パプリカ oGWXgMa5vdE HIKAKIN & SEIKIN - YouTube Theme Song 6f6J7bvnceU Taro Urashima - Umino Koe (Instrumental) xiuOSWPsIyM OFFICIAL HIGE DANDISM - Subtitle TqyfmFaZlqw LiSA - Gurenge VEe_yIbW64w Miki Matsubara - Mayonaka no Door 〜 Stay with Me -xOYe7PN9u5E Kenshi Yonezu - KICK BACK (Tomggg Remix) +xOYe7PN9u5E Kenshi Yonezu - KICK BACK (Tomggg Remix) 0ad7f0H5wmY ZUTOMAYO - Byoushinwo Kamu -nROvY9uiYYk Eve - お気に召すまま - Eve MV +nROvY9uiYYk Eve - お気に召すまま tkjALBLAF6o SixTONES - kokkara -Woorod1gJ_w Amatsuki - 【オリジナルPV】 小さな恋のうた / MONGOL800(cover) by天月 +Woorod1gJ_w 天月 - 小さな恋のうた (cover) vfLfonEeoyM Chinozo - Good-bye Declaration bWVYjgsJUUc ONE OK ROCK - Clock Strikes M_Uz5SKGxs4 BUMP OF CHICKEN - Tentaikansoku @@ -292,9 +292,9 @@ Hx_7LvLjNmc AAA - KoiotoToAmazora tgNIeWvXKNM Yoko Oginome - Dancing Hero(Eat You Up) 7lcxt2tRyGQ AAA - さよならの前に 6ZyZRFChIPU Uru - Anataga Iru Kotode -clU8c2fpk2s kobasolo - 【女性が歌う】Lemon/米津玄師(Full Covered by コバソロ & 春茶) +clU8c2fpk2s kobasolo - Lemon (cover) uDsEVMjwNb0 back number - Takaneno Hanakosan -1s84rIhPuhk Kenshi Yonezu - 米津玄師 - 海の幽霊 Kenshi Yonezu - Spirits of the Sea +1s84rIhPuhk Kenshi Yonezu - 海の幽霊 e7u2aPzWmU4 back number - Christmas Song ENQANfv2ANQ Vaundy - Kaiju no Hanauta Po2Ujyn8SXE aimyon - Harunohi @@ -308,7 +308,7 @@ oMeFtedQS0E King Gnu - Ichizu (ALBUM ver.) 8GfDq1osBLM YOASOBI - ハルジオン vJ2AhTaQO10 Saucy Dog - Cinderella Boy kMzcG30g8OQ なにわ男子 - 初心LOVE(うぶらぶ)YouTube ver. -jOegTv3a2h4 high_note Music Lounge - 糸 - 中島みゆき(フル) +jOegTv3a2h4 中島みゆき - 糸 csVeFCLhmAM FLOW - Sign FVaAp4dhK0Q gesunokiwamiotome - watashi igai watashi ja naino IGldkA5dG8o Hikaru Utada - First Love @@ -349,7 +349,7 @@ MYZXUJ-BG-Q Paul Blanco - Summer kp-gVJ7XJV4 KyoungSeo - 첫 키스에 내 심장은 120BPM 8kQH_bBMrMQ 최유리 - Forest 7eiQHEJ-Sbo 안예은 - Magic Lily -EK18vAp9VVc CHiCORY - 비몽 - 작야 Live COVER (240201) │ 원곡 조선블루스 +EK18vAp9VVc 비몽 - 작야 (cover) 2El3ZfWbehw OOHYO - Youth (DAY) yPCi20P1QFs YEEUN AHN - Night Flower (야화) KBKzIDdAO1s 하시토 - [한글자막] 원피스 필름 레드 삽입곡 Full - 나는 최강(I'm invincible) │ Ado @@ -358,7 +358,7 @@ hza4uQGZexA 이예준 - miss you more, I'm sorry Bi87zvvexLo majiko - Kokoronashi (2017 Live Version) em0lGANmAUU GUMMY - Memory Loss RGPpBDFFqDg Lucia(심규선) - 달과 6펜스 -i6rXYiwuJwM Philopon Music - 리무카이쵸 - 너에게 마지막 입맞춤을(君に最後の口づけを) [자막/가사] +i6rXYiwuJwM 리무카이쵸 - 너에게 마지막 입맞춤을 R89tf2GbxTM 위도 Music Official - Gravity | 위도(W2RDO) Live Cover a1IuJLebHgM Adele - When We Were Young 8CNnNGgAHig Lucia - Narrow Road (소로 小路) @@ -505,7 +505,7 @@ Dmy0CkieM6s CAMO - Shawty CLNQ_qWdEeA Gyeong Je Hwan - a remote star and satellite h0ccMK0U9nY Coogie - Alone (Feat. LeeHi) (Alone 5TgTPH2b6nM Jehwwn - No Hope For Your Return -JvQhJTM9OJU JUSTHIS - Do Not Go Gentle Into That Good Night (Feat. KWAII, DON MALIK, GongGongGoo009) (Prod. Humbert) +JvQhJTM9OJU JUSTHIS - Do Not Go Gentle Into That Good Night pRosoqfxJY4 GIRIBOY - That's Why I Can't Talk About Love (Feat. Woo) (That's Why I Can't Talk About Love vMrI9LXohYo 2NE1 - 그리워해요(Missing You) iTPkkKRKGrQ 2NE1 - Go Away @@ -850,7 +850,7 @@ _0Yp2Wtvpeo Primary - Drama jXgc1FTjMd4 hard life - dead celebrities pwjj1eInTSU Zion.T - Click Me (2013) sobPzRxbmwE 스트레이 - Thing You Are (Live Demo) -OejyNpxR-DM The S6 - Vardy's on Fire - The S6 [Official Lyric Video] +OejyNpxR-DM The S6 - Vardy's on Fire vw3k_jWa-gI Yeah Yeah Yeahs - Heads Will Roll pRIZohFFOMo Lewis Capaldi - Someone You Loved NqLB6ub1BOw VIVIZ - MANIAC @@ -900,9 +900,9 @@ PiHCuVnvBkU Shin Giwon Piano - In Your Time IwQGnXpskoY Rokudenashi - One Voice dgZlLjfh6q4 Damons year - yours BctgR3JqGug Hebi - 늘 (EVER) -2ThVc_x9_y0 비몽 Beemong - 김필선 - 마마 live (cover by 비몽) +2ThVc_x9_y0 비몽 - 마마 (cover) 29lj9jFfJvY Woody - Sadder Than Yesterday (어제보다 슬픈 오늘) -iTF-zVkrtZk 비몽 Beemong - 비몽 - 야수 live cover [돌발서버 콘서트] +iTF-zVkrtZk 비몽 - 야수 (cover) eNiPuDEUQpQ AKMU - Joy, Sorrow, A Beautiful Heart 7kCDaxIjZFY Park Hyo Shin - AE QmyrqNY9RPU 하데스(HADES) - Planet B |
