From 3cb665344970986290f170efca3f86ef51b4ba3c Mon Sep 17 00:00:00 2001 From: Puqns67 Date: Thu, 17 Oct 2024 22:28:30 +0800 Subject: [PATCH] feature: add private url scheme * Also reformat code * Rename symbol LinkType.Song -> LinkType.Track --- ncmlyrics/__init__.py | 2 +- ncmlyrics/__main__.py | 2 +- ncmlyrics/enum.py | 6 +-- ncmlyrics/util.py | 98 +++++++++++++++++++++++++++++-------------- tests/test_utils.py | 61 ++++++++++++++++++++++++--- 5 files changed, 126 insertions(+), 43 deletions(-) diff --git a/ncmlyrics/__init__.py b/ncmlyrics/__init__.py index f90ed5f..bfea9f2 100644 --- a/ncmlyrics/__init__.py +++ b/ncmlyrics/__init__.py @@ -1,4 +1,4 @@ -from .__version__ import __title__, __description__, __version__ from .__main__ import main +from .__version__ import __description__, __title__, __version__ __all__ = ["__title__", "__description__", "__version__", "main"] diff --git a/ncmlyrics/__main__.py b/ncmlyrics/__main__.py index 5ea74f2..fe96052 100644 --- a/ncmlyrics/__main__.py +++ b/ncmlyrics/__main__.py @@ -93,7 +93,7 @@ def main(outputs: list[Path], exist: bool, overwrite: bool, quiet: bool, links: continue match parsed.type: - case LinkType.Song: + case LinkType.Track: newTrack = api.getDetailsForTrack(parsed.id) savePath = pickOutput(newTrack, outputs, exist) diff --git a/ncmlyrics/enum.py b/ncmlyrics/enum.py index dec0b44..3cca4f4 100644 --- a/ncmlyrics/enum.py +++ b/ncmlyrics/enum.py @@ -29,6 +29,6 @@ class LrcMetaType(Enum): class LinkType(Enum): - Song = auto() - Album = auto() - Playlist = auto() + Track = "track" + Album = "album" + Playlist = "playlist" diff --git a/ncmlyrics/util.py b/ncmlyrics/util.py index 09693eb..a2949c2 100644 --- a/ncmlyrics/util.py +++ b/ncmlyrics/util.py @@ -12,7 +12,8 @@ from .object import NCMTrack __all__ = ["Link", "parseLink", "testExistTrackSource", "pickOutput"] -RE_ANDROID_ALBUM_SHARE_LINK_PATH = reCompile(r"^/album/(?P\d*)/?$") +RE_SHARE_LINK_ID_BY_PATH = reCompile(r"^/?(?P\d+)$") +RE_SHARE_LINK_ANDROID_ALBUM_PATH = reCompile(r"^/album/(?P\d+)/?$") RE_SAFE_FILENAME = reCompile(r"\*{2,}") TRANSLATER_SAFE_FILENAME = str.maketrans({i: 0x2A for i in ("<", ">", ":", '"', "/", "\\", "|", "?")}) @@ -28,39 +29,72 @@ def parseLink(url: str) -> Link: contentType: LinkType | None = None contentId: int | None = None - match parsedUrl.netloc: - case "music.163.com": - match parsedUrl.path: - case "/playlist" | "/#/playlist": - contentType = LinkType.Playlist - case "/album" | "/#/album": - contentType = LinkType.Album - case "/song" | "/#/song": - contentType = LinkType.Song - case _: - # Hack for android client shared album link - matchedPath = RE_ANDROID_ALBUM_SHARE_LINK_PATH.match(parsedUrl.path) - if matchedPath is not None: - contentType = LinkType.Album - contentId = int(matchedPath["id"]) - else: - raise UnsupportedLinkError(parsedUrl) - case "y.music.163.com": - match parsedUrl.path: - case "/m/playlist": - contentType = LinkType.Playlist - case "/m/song": - contentType = LinkType.Song + match parsedUrl.scheme: + case "http" | "https": + match parsedUrl.netloc: + case "music.163.com": + match parsedUrl.path: + case "/playlist" | "/#/playlist": + contentType = LinkType.Playlist + case "/album" | "/#/album": + contentType = LinkType.Album + case "/song" | "/#/song": + contentType = LinkType.Track + case _: + # Hack for android client shared album link + matchedPath = RE_SHARE_LINK_ANDROID_ALBUM_PATH.match(parsedUrl.path) + if matchedPath is not None: + contentType = LinkType.Album + contentId = int(matchedPath["id"]) + else: + raise UnsupportedLinkError(parsedUrl) + case "y.music.163.com": + match parsedUrl.path: + case "/m/playlist": + contentType = LinkType.Playlist + case "/m/song": + contentType = LinkType.Track + case _: + raise UnsupportedLinkError(parsedUrl) + case "163cn.tv": + response = httpGet(url) + if response.status_code != 302: + raise ParseLinkError(f"未知的 Api 响应: {response.status_code}") + newUrl = response.headers.get("Location") + if newUrl is None: + raise ParseLinkError("Api 未返回重定向结果") + return parseLink(newUrl) case _: raise UnsupportedLinkError(parsedUrl) - case "163cn.tv": - response = httpGet(url) - if response.status_code != 302: - raise ParseLinkError(f"未知的 Api 响应: {response.status_code}") - newUrl = response.headers.get("Location") - if newUrl is None: - raise ParseLinkError("Api 未返回重定向结果") - return parseLink(newUrl) + case "ncmlyrics": # eg: ncmlyrics://playlist/123456, ncmlyrics://album/12456, ncmlyrics://track/123456 + try: + contentType = LinkType(parsedUrl.netloc) + except ValueError: + raise UnsupportedLinkError(parsedUrl) + + if parsedUrl.path: + matched = RE_SHARE_LINK_ID_BY_PATH.match(parsedUrl.path) + if matched is not None: + contentId = int(matched.group("id")) + else: + raise ParseLinkError + case "playlist" | "album" | "track": # eg: playlist:123456, album:/12456, track://123456 + try: + contentType = LinkType(parsedUrl.scheme) + except ValueError: + raise UnsupportedLinkError(parsedUrl) + + try: + if parsedUrl.netloc: + contentId = int(parsedUrl.netloc) + elif parsedUrl.path: + matched = RE_SHARE_LINK_ID_BY_PATH.match(parsedUrl.path) + if matched is not None: + contentId = int(matched.group("id")) + else: + raise ParseLinkError + except ValueError: + raise ParseLinkError case _: raise UnsupportedLinkError(parsedUrl) diff --git a/tests/test_utils.py b/tests/test_utils.py index 6946fda..2f97b97 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -23,7 +23,7 @@ class TestUtils(TestCase): self.assertEqual( parseLink("https://music.163.com/song?id=2621105420"), - Link(LinkType.Song, 2621105420), + Link(LinkType.Track, 2621105420), msg="Shared song from NCM Windows Client", ) @@ -42,7 +42,7 @@ class TestUtils(TestCase): self.assertEqual( parseLink("https://music.163.com/#/song?id=2621105420"), - Link(LinkType.Song, 2621105420), + Link(LinkType.Track, 2621105420), msg="Song from NCM Website", ) @@ -61,18 +61,55 @@ class TestUtils(TestCase): self.assertEqual( parseLink("https://y.music.163.com/m/song?id=2604307454"), - Link(LinkType.Song, 2604307454), + Link(LinkType.Track, 2604307454), msg="Shared song from NCM Android Client", ) def test_parseLink_302(self): self.assertEqual( parseLink("http://163cn.tv/xpaQwii"), - Link(LinkType.Song, 413077069), + Link(LinkType.Track, 413077069), msg="Shared song from NCM Android Client player", ) - def test_parseLink_UnsupportShareLinkError(self): + def test_parseLink_special(self): + self.assertEqual( + parseLink("ncmlyrics://playlist/123456"), + Link(LinkType.Playlist, 123456), + ) + + self.assertEqual( + parseLink("ncmlyrics://album/123456"), + Link(LinkType.Album, 123456), + ) + + self.assertEqual( + parseLink("ncmlyrics://track/123456"), + Link(LinkType.Track, 123456), + ) + + self.assertEqual( + parseLink("playlist:123456"), + Link(LinkType.Playlist, 123456), + ) + + self.assertEqual( + parseLink("album:/123456"), + Link(LinkType.Album, 123456), + ) + + self.assertEqual( + parseLink("track://123456"), + Link(LinkType.Track, 123456), + ) + + def test_parseLink_UnsupportedLinkError(self): + self.assertRaises( + UnsupportedLinkError, + parseLink, + "ftp://ftpserver.com/", + ) + self.assertRaises( UnsupportedLinkError, parseLink, @@ -91,13 +128,25 @@ class TestUtils(TestCase): "https://music.163.com/album/123a", ) - def test_parseLink_ParseShareLinkError(self): + self.assertRaises( + UnsupportedLinkError, + parseLink, + "ncmlyrics://unsupport/123456", + ) + + def test_parseLink_ParseLinkError(self): self.assertRaises( ParseLinkError, parseLink, "https://music.163.com/playlist?id=123a", ) + self.assertRaises( + ParseLinkError, + parseLink, + "playlist://123456a", + ) + def test_testExistTrackSource(self): resources = Path("tests/resource/util/testExistTrackSource")