Compare commits

..

2 Commits

Author SHA1 Message Date
Uyanide cf0cb1ab53 feat: replace typer with cycplots & improve cli 2026-03-30 18:48:42 +02:00
Uyanide bb72623446 update .gitignore 2026-03-30 01:06:54 +02:00
5 changed files with 279 additions and 184 deletions
+5 -6
View File
@@ -6,10 +6,9 @@ dist/
wheels/ wheels/
*.egg-info *.egg-info
# Virtual environments .*
.venv !.gitignore
!.python-version
.env *.md
.claude !README.md
.vscode
.ruff_cache
+175 -134
View File
@@ -4,67 +4,86 @@ Date: 2026-03-26 02:04:39
Description: CLI interface Description: CLI interface
""" """
import typer import sys
import time import time
from typing import Optional
from loguru import logger
import os import os
from typing import Annotated
import cyclopts
from loguru import logger
from .config import enable_debug from .config import enable_debug
from .models import TrackMeta, CacheStatus from .models import TrackMeta, CacheStatus
from .mpris import get_current_track from .mpris import get_current_track
from .core import LrcManager from .core import LrcManager, FetcherMethodType
app = typer.Typer(
app = cyclopts.App(
help="LRCFetch — Fetch line-synced lyrics for your music player.", help="LRCFetch — Fetch line-synced lyrics for your music player.",
add_completion=True,
) )
app.register_install_completion_command()
cache_app = cyclopts.App(name="cache", help="Manage the local SQLite cache.")
app.command(cache_app)
manager = LrcManager() manager = LrcManager()
# Global state set by the app callback # Global state set by the meta launcher
_player: Optional[str] = None _player: str | None = None
@app.callback() @app.meta.default
def main( def launcher(
debug: bool = typer.Option(False, "--debug", "-d", help="Enable debug logging."), *tokens: Annotated[str, cyclopts.Parameter(show=False, allow_leading_hyphen=True)],
player: Optional[str] = typer.Option( debug: Annotated[
None, bool,
"--player", cyclopts.Parameter(
"-p", name=["--debug", "-d"], negative="", help="Enable debug logging."
help="Target a specific MPRIS player using its DBus name or a portion thereof.", ),
), ] = False,
player: Annotated[
str | None,
cyclopts.Parameter(
name=["--player", "-p"],
help="Target a specific MPRIS player using its DBus name or a portion thereof.",
),
] = None,
): ):
global _player global _player
if debug: if debug:
enable_debug() enable_debug()
_player = player _player = player
app(tokens)
# fetch # fetch
@app.command() @app.command
def fetch( def fetch(
method: Optional[str] = typer.Option( *,
None, method: Annotated[
"--method", FetcherMethodType | None,
help="Force a specific source (local, spotify, lrclib, lrclib-search, netease).", cyclopts.Parameter(help="Force a specific source."),
), ] = None,
no_cache: bool = typer.Option( no_cache: Annotated[
False, "--no-cache", help="Bypass the cache for this request." bool,
), cyclopts.Parameter(
only_synced: bool = typer.Option( name="--no-cache", negative="", help="Bypass the cache for this request."
False, "--only-synced", help="Only accept synced (timed) lyrics." ),
), ] = False,
only_synced: Annotated[
bool,
cyclopts.Parameter(
name="--only-synced", negative="", help="Only accept synced (timed) lyrics."
),
] = False,
): ):
"""Fetch and print lyrics for the currently playing track.""" """Fetch and print lyrics for the currently playing track."""
track = get_current_track(_player) track = get_current_track(_player)
if not track: if not track:
logger.error("No active playing track found.") logger.error("No active playing track found.")
raise typer.Exit(1) sys.exit(1)
logger.info(f"Track: {track.display_name()}") logger.info(f"Track: {track.display_name()}")
@@ -72,11 +91,11 @@ def fetch(
if not result or not result.lyrics: if not result or not result.lyrics:
logger.error("No lyrics found.") logger.error("No lyrics found.")
raise typer.Exit(1) sys.exit(1)
if only_synced and result.status != CacheStatus.SUCCESS_SYNCED: if only_synced and result.status != CacheStatus.SUCCESS_SYNCED:
logger.error("Only unsynced lyrics available (--only-synced requested).") logger.error("Only unsynced lyrics available (--only-synced requested).")
raise typer.Exit(1) sys.exit(1)
print(result.lyrics) print(result.lyrics)
@@ -84,27 +103,41 @@ def fetch(
# search # search
@app.command() @app.command
def search( def search(
title: str = typer.Option(..., "--title", "-t", help="Track title."), *,
artist: Optional[str] = typer.Option(None, "--artist", "-a", help="Artist name."), title: Annotated[
album: Optional[str] = typer.Option(None, "--album", help="Album name."), str, cyclopts.Parameter(name=["--title", "-t"], help="Track title.")
trackid: Optional[str] = typer.Option(None, "--trackid", help="Spotify track ID."), ],
length: Optional[int] = typer.Option( artist: Annotated[
None, "--length", "-l", help="Track duration in milliseconds." str | None, cyclopts.Parameter(name=["--artist", "-a"], help="Artist name.")
), ] = None,
url: Optional[str] = typer.Option( album: Annotated[str | None, cyclopts.Parameter(help="Album name.")] = None,
None, "--url", help="Local file URL (file:///...)." trackid: Annotated[str | None, cyclopts.Parameter(help="Spotify track ID.")] = None,
), length: Annotated[
method: Optional[str] = typer.Option( int | None,
None, "--method", help="Force a specific source." cyclopts.Parameter(
), name=["--length", "-l"], help="Track duration in milliseconds."
no_cache: bool = typer.Option( ),
False, "--no-cache", help="Bypass the cache for this request." ] = None,
), url: Annotated[
only_synced: bool = typer.Option( str | None, cyclopts.Parameter(help="Local file URL (file:///...).")
False, "--only-synced", help="Only accept synced (timed) lyrics." ] = None,
), method: Annotated[
FetcherMethodType | None, cyclopts.Parameter(help="Force a specific source.")
] = None,
no_cache: Annotated[
bool,
cyclopts.Parameter(
name="--no-cache", negative="", help="Bypass the cache for this request."
),
] = False,
only_synced: Annotated[
bool,
cyclopts.Parameter(
name="--only-synced", negative="", help="Only accept synced (timed) lyrics."
),
] = False,
): ):
"""Search for lyrics by metadata (bypasses MPRIS).""" """Search for lyrics by metadata (bypasses MPRIS)."""
track = TrackMeta( track = TrackMeta(
@@ -122,11 +155,11 @@ def search(
if not result or not result.lyrics: if not result or not result.lyrics:
logger.error("No lyrics found.") logger.error("No lyrics found.")
raise typer.Exit(1) sys.exit(1)
if only_synced and result.status != CacheStatus.SUCCESS_SYNCED: if only_synced and result.status != CacheStatus.SUCCESS_SYNCED:
logger.error("Only unsynced lyrics available (--only-synced requested).") logger.error("Only unsynced lyrics available (--only-synced requested).")
raise typer.Exit(1) sys.exit(1)
print(result.lyrics) print(result.lyrics)
@@ -134,32 +167,39 @@ def search(
# export # export
@app.command() @app.command
def export( def export(
output: Optional[str] = typer.Option( *,
None, output: Annotated[
"--output", str | None,
"-o", cyclopts.Parameter(
help="Output file path (default: <Artist> - <Title>.lrc).", name=["--output", "-o"],
), help="Output file path (default: <Artist> - <Title>.lrc).",
method: Optional[str] = typer.Option( ),
None, "--method", help="Force a specific source." ] = None,
), method: Annotated[
no_cache: bool = typer.Option(False, "--no-cache", help="Bypass cache."), FetcherMethodType | None, cyclopts.Parameter(help="Force a specific source.")
overwrite: bool = typer.Option( ] = None,
False, "--overwrite", "-f", help="Overwrite existing file." no_cache: Annotated[
), bool, cyclopts.Parameter(name="--no-cache", negative="", help="Bypass cache.")
] = False,
overwrite: Annotated[
bool,
cyclopts.Parameter(
name=["--overwrite", "-f"], negative="", help="Overwrite existing file."
),
] = False,
): ):
"""Export lyrics of the current track to a .lrc file.""" """Export lyrics of the current track to a .lrc file."""
track = get_current_track(_player) track = get_current_track(_player)
if not track: if not track:
logger.error("No active playing track found.") logger.error("No active playing track found.")
raise typer.Exit(1) sys.exit(1)
result = manager.fetch_for_track(track, force_method=method, bypass_cache=no_cache) result = manager.fetch_for_track(track, force_method=method, bypass_cache=no_cache)
if not result or not result.lyrics: if not result or not result.lyrics:
logger.error("No lyrics available to export.") logger.error("No lyrics available to export.")
raise typer.Exit(1) sys.exit(1)
# Build default output path # Build default output path
if not output: if not output:
@@ -176,7 +216,7 @@ def export(
if os.path.exists(output) and not overwrite: if os.path.exists(output) and not overwrite:
logger.error(f"File exists: {output} (use -f to overwrite)") logger.error(f"File exists: {output} (use -f to overwrite)")
raise typer.Exit(1) sys.exit(1)
try: try:
with open(output, "w", encoding="utf-8") as f: with open(output, "w", encoding="utf-8") as f:
@@ -184,69 +224,22 @@ def export(
logger.info(f"Exported lyrics to {output}") logger.info(f"Exported lyrics to {output}")
except Exception as e: except Exception as e:
logger.error(f"Failed to write file: {e}") logger.error(f"Failed to write file: {e}")
raise typer.Exit(1) sys.exit(1)
# cache # cache subcommands
@app.command() @cache_app.command
def cache( def query(
clear: bool = typer.Option(False, "--clear", help="Clear the entire cache."), *,
clear_current: bool = typer.Option( all: Annotated[
False, "--clear-current", help="Clear cache for the current track." bool,
), cyclopts.Parameter(name="--all", negative="", help="Dump all cache entries."),
prune: bool = typer.Option(False, "--prune", help="Remove expired entries."), ] = False,
stats: bool = typer.Option(False, "--stats", help="Show cache statistics."),
query: bool = typer.Option(
False, "--query", "-q", help="Show detailed cache info for the current track."
),
query_all: bool = typer.Option(
False, "--query-all", help="Dump all cache entries."
),
): ):
"""Manage the local SQLite cache.""" """Show cached entries for the current track."""
if clear: if all:
manager.cache.clear_all()
return
if clear_current:
track = get_current_track(_player)
if not track:
logger.error("No active playing track found.")
raise typer.Exit(1)
manager.cache.clear_track(track)
return
if prune:
manager.cache.prune()
return
if stats:
s = manager.cache.stats()
print("=== Cache Statistics ===")
print(f"Total entries : {s['total']}")
print(f"Active : {s['active']}")
print(f"Expired : {s['expired']}")
if s["by_status"]:
print("\nBy status:")
for status, count in s["by_status"].items():
print(f" {status}: {count}")
if s["by_source"]:
print("\nBy source:")
for source, count in s["by_source"].items():
print(f" {source}: {count}")
return
if query:
track = get_current_track(_player)
if not track:
logger.error("No active playing track found.")
raise typer.Exit(1)
_print_track_cache(track)
return
if query_all:
rows = manager.cache.query_all() rows = manager.cache.query_all()
if not rows: if not rows:
print("Cache is empty.") print("Cache is empty.")
@@ -256,10 +249,58 @@ def cache(
print() print()
return return
logger.info( track = get_current_track(_player)
"No action specified. Try --stats, --query, --query-all, " if not track:
"--prune, --clear, or --clear-current." logger.error("No active playing track found.")
) sys.exit(1)
_print_track_cache(track)
@cache_app.command
def clear(
*,
all: Annotated[
bool,
cyclopts.Parameter(name="--all", negative="", help="Clear the entire cache."),
] = False,
):
"""Clear cached entries for the current track."""
if all:
manager.cache.clear_all()
return
track = get_current_track(_player)
if not track:
logger.error("No active playing track found.")
sys.exit(1)
manager.cache.clear_track(track)
@cache_app.command
def prune():
"""Remove expired cache entries."""
manager.cache.prune()
@cache_app.command
def stats():
"""Show cache statistics."""
s = manager.cache.stats()
print("=== Cache Statistics ===")
print(f"Total entries : {s['total']}")
print(f"Active : {s['active']}")
print(f"Expired : {s['expired']}")
if s["by_status"]:
print("\nBy status:")
for status, count in s["by_status"].items():
print(f" {status}: {count}")
if s["by_source"]:
print("\nBy source:")
for source, count in s["by_source"].items():
print(f" {source}: {count}")
# helpers
def _print_track_cache(track: TrackMeta) -> None: def _print_track_cache(track: TrackMeta) -> None:
@@ -317,7 +358,7 @@ def _print_cache_row(row: dict, indent: str = "") -> None:
def run(): def run():
app() app.meta()
if __name__ == "__main__": if __name__ == "__main__":
+12 -3
View File
@@ -14,6 +14,7 @@ Fetch pipeline:
from typing import Optional from typing import Optional
from loguru import logger from loguru import logger
from typing import Literal
from .fetchers.netease import NeteaseFetcher from .fetchers.netease import NeteaseFetcher
from .fetchers.lrclib_search import LrclibSearchFetcher from .fetchers.lrclib_search import LrclibSearchFetcher
@@ -27,6 +28,11 @@ from .lrc import LRC_LINE_RE, normalize_tags
from .config import TTL_SYNCED, TTL_UNSYNCED, TTL_NOT_FOUND, TTL_NETWORK_ERROR from .config import TTL_SYNCED, TTL_UNSYNCED, TTL_NOT_FOUND, TTL_NETWORK_ERROR
from .models import TrackMeta, LyricResult, CacheStatus from .models import TrackMeta, LyricResult, CacheStatus
METHODS = ("local", "cache-search", "spotify", "lrclib", "lrclib-search", "netease")
FetcherMethodType = Literal[
"local", "cache-search", "spotify", "lrclib", "lrclib-search", "netease"
]
def _normalize_unsynced(lyrics: str) -> str: def _normalize_unsynced(lyrics: str) -> str:
"""Normalize unsynced lyrics so every line has a [00:00.00] tag. """Normalize unsynced lyrics so every line has a [00:00.00] tag.
@@ -65,7 +71,7 @@ class LrcManager:
def __init__(self) -> None: def __init__(self) -> None:
self.cache = CacheEngine() self.cache = CacheEngine()
self.fetchers: dict[str, BaseFetcher] = { self.fetchers: dict[FetcherMethodType, BaseFetcher] = {
"local": LocalFetcher(), "local": LocalFetcher(),
"cache-search": CacheSearchFetcher(self.cache), "cache-search": CacheSearchFetcher(self.cache),
"spotify": SpotifyFetcher(), "spotify": SpotifyFetcher(),
@@ -73,9 +79,12 @@ class LrcManager:
"lrclib-search": LrclibSearchFetcher(), "lrclib-search": LrclibSearchFetcher(),
"netease": NeteaseFetcher(), "netease": NeteaseFetcher(),
} }
assert set(self.fetchers) == set(METHODS), (
f"METHODS and fetchers out of sync: {set(METHODS) ^ set(self.fetchers)}"
)
def _build_sequence( def _build_sequence(
self, track: TrackMeta, force_method: Optional[str] = None self, track: TrackMeta, force_method: Optional[FetcherMethodType] = None
) -> list[BaseFetcher]: ) -> list[BaseFetcher]:
"""Determine the ordered list of fetchers to try.""" """Determine the ordered list of fetchers to try."""
if force_method: if force_method:
@@ -103,7 +112,7 @@ class LrcManager:
def fetch_for_track( def fetch_for_track(
self, self,
track: TrackMeta, track: TrackMeta,
force_method: Optional[str] = None, force_method: Optional[FetcherMethodType] = None,
bypass_cache: bool = False, bypass_cache: bool = False,
) -> Optional[LyricResult]: ) -> Optional[LyricResult]:
"""Fetch lyrics for *track* using the fallback pipeline. """Fetch lyrics for *track* using the fallback pipeline.
+8 -2
View File
@@ -4,11 +4,12 @@ build-backend = "hatchling.build"
[project] [project]
name = "lrcfetch" name = "lrcfetch"
version = "0.1.0" version = "0.1.2"
description = "Fetch line-synced lyrics for your music player." description = "Fetch line-synced lyrics for your music player."
readme = "README.md" readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [ dependencies = [
"cyclopts>=4.10.1",
"dbus-next>=0.2.3", "dbus-next>=0.2.3",
"httpx>=0.28.1", "httpx>=0.28.1",
"loguru>=0.7.3", "loguru>=0.7.3",
@@ -16,8 +17,13 @@ dependencies = [
"platformdirs>=4.9.4", "platformdirs>=4.9.4",
"pydantic>=2.12.5", "pydantic>=2.12.5",
"python-dotenv>=1.2.2", "python-dotenv>=1.2.2",
"typer>=0.24.1",
] ]
[project.scripts] [project.scripts]
lrcfetch = "lrcfetch.cli:run" lrcfetch = "lrcfetch.cli:run"
[tool.ruff.lint]
ignore = ["E402"]
[dependency-groups]
dev = ["ruff>=0.15.8"]
Generated
+79 -39
View File
@@ -2,15 +2,6 @@ version = 1
revision = 3 revision = 3
requires-python = ">=3.13" requires-python = ">=3.13"
[[package]]
name = "annotated-doc"
version = "0.0.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
]
[[package]] [[package]]
name = "annotated-types" name = "annotated-types"
version = "0.7.0" version = "0.7.0"
@@ -32,6 +23,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
] ]
[[package]]
name = "attrs"
version = "26.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2026.2.25" version = "2026.2.25"
@@ -41,18 +41,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
] ]
[[package]]
name = "click"
version = "8.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
]
[[package]] [[package]]
name = "colorama" name = "colorama"
version = "0.4.6" version = "0.4.6"
@@ -62,6 +50,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
] ]
[[package]]
name = "cyclopts"
version = "4.10.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "docstring-parser" },
{ name = "rich" },
{ name = "rich-rst" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6c/c4/2ce2ca1451487dc7d59f09334c3fa1182c46cfcf0a2d5f19f9b26d53ac74/cyclopts-4.10.1.tar.gz", hash = "sha256:ad4e4bb90576412d32276b14a76f55d43353753d16217f2c3cd5bdceba7f15a0", size = 166623, upload-time = "2026-03-23T14:43:01.098Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0b/2261922126b2e50c601fe22d7ff5194e0a4d50e654836260c0665e24d862/cyclopts-4.10.1-py3-none-any.whl", hash = "sha256:35f37257139380a386d9fe4475e1e7c87ca7795765ef4f31abba579fcfcb6ecd", size = 204331, upload-time = "2026-03-23T14:43:02.625Z" },
]
[[package]] [[package]]
name = "dbus-next" name = "dbus-next"
version = "0.2.3" version = "0.2.3"
@@ -71,6 +74,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/fc/c0a3f4c4eaa5a22fbef91713474666e13d0ea2a69c84532579490a9f2cc8/dbus_next-0.2.3-py3-none-any.whl", hash = "sha256:58948f9aff9db08316734c0be2a120f6dc502124d9642f55e90ac82ffb16a18b", size = 57885, upload-time = "2021-07-25T22:11:25.466Z" }, { url = "https://files.pythonhosted.org/packages/d2/fc/c0a3f4c4eaa5a22fbef91713474666e13d0ea2a69c84532579490a9f2cc8/dbus_next-0.2.3-py3-none-any.whl", hash = "sha256:58948f9aff9db08316734c0be2a120f6dc502124d9642f55e90ac82ffb16a18b", size = 57885, upload-time = "2021-07-25T22:11:25.466Z" },
] ]
[[package]]
name = "docstring-parser"
version = "0.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" },
]
[[package]]
name = "docutils"
version = "0.22.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" },
]
[[package]] [[package]]
name = "h11" name = "h11"
version = "0.16.0" version = "0.16.0"
@@ -132,9 +153,10 @@ wheels = [
[[package]] [[package]]
name = "lrcfetch" name = "lrcfetch"
version = "0.1.0" version = "0.1.2"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "cyclopts" },
{ name = "dbus-next" }, { name = "dbus-next" },
{ name = "httpx" }, { name = "httpx" },
{ name = "loguru" }, { name = "loguru" },
@@ -142,11 +164,16 @@ dependencies = [
{ name = "platformdirs" }, { name = "platformdirs" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "typer" }, ]
[package.dev-dependencies]
dev = [
{ name = "ruff" },
] ]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "cyclopts", specifier = ">=4.10.1" },
{ name = "dbus-next", specifier = ">=0.2.3" }, { name = "dbus-next", specifier = ">=0.2.3" },
{ name = "httpx", specifier = ">=0.28.1" }, { name = "httpx", specifier = ">=0.28.1" },
{ name = "loguru", specifier = ">=0.7.3" }, { name = "loguru", specifier = ">=0.7.3" },
@@ -154,11 +181,10 @@ requires-dist = [
{ name = "platformdirs", specifier = ">=4.9.4" }, { name = "platformdirs", specifier = ">=4.9.4" },
{ name = "pydantic", specifier = ">=2.12.5" }, { name = "pydantic", specifier = ">=2.12.5" },
{ name = "python-dotenv", specifier = ">=1.2.2" }, { name = "python-dotenv", specifier = ">=1.2.2" },
{ name = "typer", specifier = ">=0.24.1" },
] ]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [] dev = [{ name = "ruff", specifier = ">=0.15.8" }]
[[package]] [[package]]
name = "markdown-it-py" name = "markdown-it-py"
@@ -299,27 +325,41 @@ wheels = [
] ]
[[package]] [[package]]
name = "shellingham" name = "rich-rst"
version = "1.5.4" version = "1.3.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } dependencies = [
{ name = "docutils" },
{ name = "rich" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, { url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" },
] ]
[[package]] [[package]]
name = "typer" name = "ruff"
version = "0.24.1" version = "0.15.8"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ sdist = { url = "https://files.pythonhosted.org/packages/14/b0/73cf7550861e2b4824950b8b52eebdcc5adc792a00c514406556c5b80817/ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e", size = 4610921, upload-time = "2026-03-26T18:39:38.675Z" }
{ name = "annotated-doc" },
{ name = "click" },
{ name = "rich" },
{ name = "shellingham" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, { url = "https://files.pythonhosted.org/packages/4a/92/c445b0cd6da6e7ae51e954939cb69f97e008dbe750cfca89b8cedc081be7/ruff-0.15.8-py3-none-linux_armv6l.whl", hash = "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7", size = 10527394, upload-time = "2026-03-26T18:39:41.566Z" },
{ url = "https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570", size = 10905693, upload-time = "2026-03-26T18:39:30.364Z" },
{ url = "https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3", size = 10323044, upload-time = "2026-03-26T18:39:33.37Z" },
{ url = "https://files.pythonhosted.org/packages/67/18/1bf38e20914a05e72ef3b9569b1d5c70a7ef26cd188d69e9ca8ef588d5bf/ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94", size = 10629135, upload-time = "2026-03-26T18:39:44.142Z" },
{ url = "https://files.pythonhosted.org/packages/d2/e9/138c150ff9af60556121623d41aba18b7b57d95ac032e177b6a53789d279/ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3", size = 10348041, upload-time = "2026-03-26T18:39:52.178Z" },
{ url = "https://files.pythonhosted.org/packages/02/f1/5bfb9298d9c323f842c5ddeb85f1f10ef51516ac7a34ba446c9347d898df/ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762", size = 11121987, upload-time = "2026-03-26T18:39:55.195Z" },
{ url = "https://files.pythonhosted.org/packages/10/11/6da2e538704e753c04e8d86b1fc55712fdbdcc266af1a1ece7a51fff0d10/ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a", size = 11951057, upload-time = "2026-03-26T18:39:19.18Z" },
{ url = "https://files.pythonhosted.org/packages/83/f0/c9208c5fd5101bf87002fed774ff25a96eea313d305f1e5d5744698dc314/ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8", size = 11464613, upload-time = "2026-03-26T18:40:06.301Z" },
{ url = "https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1", size = 11257557, upload-time = "2026-03-26T18:39:57.972Z" },
{ url = "https://files.pythonhosted.org/packages/71/8c/382a9620038cf6906446b23ce8632ab8c0811b8f9d3e764f58bedd0c9a6f/ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec", size = 11169440, upload-time = "2026-03-26T18:39:22.205Z" },
{ url = "https://files.pythonhosted.org/packages/4d/0d/0994c802a7eaaf99380085e4e40c845f8e32a562e20a38ec06174b52ef24/ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6", size = 10605963, upload-time = "2026-03-26T18:39:46.682Z" },
{ url = "https://files.pythonhosted.org/packages/19/aa/d624b86f5b0aad7cef6bbf9cd47a6a02dfdc4f72c92a337d724e39c9d14b/ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb", size = 10357484, upload-time = "2026-03-26T18:39:49.176Z" },
{ url = "https://files.pythonhosted.org/packages/35/c3/e0b7835d23001f7d999f3895c6b569927c4d39912286897f625736e1fd04/ruff-0.15.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8", size = 10830426, upload-time = "2026-03-26T18:40:03.702Z" },
{ url = "https://files.pythonhosted.org/packages/f0/51/ab20b322f637b369383adc341d761eaaa0f0203d6b9a7421cd6e783d81b9/ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49", size = 11345125, upload-time = "2026-03-26T18:39:27.799Z" },
{ url = "https://files.pythonhosted.org/packages/37/e6/90b2b33419f59d0f2c4c8a48a4b74b460709a557e8e0064cf33ad894f983/ruff-0.15.8-py3-none-win32.whl", hash = "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34", size = 10571959, upload-time = "2026-03-26T18:39:36.117Z" },
{ url = "https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl", hash = "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89", size = 11743893, upload-time = "2026-03-26T18:39:25.01Z" },
{ url = "https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size = 11048175, upload-time = "2026-03-26T18:40:01.06Z" },
] ]
[[package]] [[package]]