Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cf0cb1ab53 | |||
| bb72623446 |
+5
-6
@@ -6,10 +6,9 @@ dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
.*
|
||||
!.gitignore
|
||||
!.python-version
|
||||
|
||||
.env
|
||||
.claude
|
||||
.vscode
|
||||
.ruff_cache
|
||||
*.md
|
||||
!README.md
|
||||
|
||||
+150
-109
@@ -4,67 +4,86 @@ Date: 2026-03-26 02:04:39
|
||||
Description: CLI interface
|
||||
"""
|
||||
|
||||
import typer
|
||||
import sys
|
||||
import time
|
||||
from typing import Optional
|
||||
from loguru import logger
|
||||
import os
|
||||
from typing import Annotated
|
||||
import cyclopts
|
||||
from loguru import logger
|
||||
|
||||
from .config import enable_debug
|
||||
from .models import TrackMeta, CacheStatus
|
||||
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.",
|
||||
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()
|
||||
|
||||
# Global state set by the app callback
|
||||
_player: Optional[str] = None
|
||||
# Global state set by the meta launcher
|
||||
_player: str | None = None
|
||||
|
||||
|
||||
@app.callback()
|
||||
def main(
|
||||
debug: bool = typer.Option(False, "--debug", "-d", help="Enable debug logging."),
|
||||
player: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--player",
|
||||
"-p",
|
||||
@app.meta.default
|
||||
def launcher(
|
||||
*tokens: Annotated[str, cyclopts.Parameter(show=False, allow_leading_hyphen=True)],
|
||||
debug: Annotated[
|
||||
bool,
|
||||
cyclopts.Parameter(
|
||||
name=["--debug", "-d"], negative="", help="Enable debug logging."
|
||||
),
|
||||
] = 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
|
||||
if debug:
|
||||
enable_debug()
|
||||
_player = player
|
||||
app(tokens)
|
||||
|
||||
|
||||
# fetch
|
||||
|
||||
|
||||
@app.command()
|
||||
@app.command
|
||||
def fetch(
|
||||
method: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--method",
|
||||
help="Force a specific source (local, spotify, lrclib, lrclib-search, netease).",
|
||||
*,
|
||||
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."
|
||||
),
|
||||
no_cache: bool = typer.Option(
|
||||
False, "--no-cache", help="Bypass the cache for this request."
|
||||
),
|
||||
only_synced: bool = typer.Option(
|
||||
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."""
|
||||
track = get_current_track(_player)
|
||||
|
||||
if not track:
|
||||
logger.error("No active playing track found.")
|
||||
raise typer.Exit(1)
|
||||
sys.exit(1)
|
||||
|
||||
logger.info(f"Track: {track.display_name()}")
|
||||
|
||||
@@ -72,11 +91,11 @@ def fetch(
|
||||
|
||||
if not result or not result.lyrics:
|
||||
logger.error("No lyrics found.")
|
||||
raise typer.Exit(1)
|
||||
sys.exit(1)
|
||||
|
||||
if only_synced and result.status != CacheStatus.SUCCESS_SYNCED:
|
||||
logger.error("Only unsynced lyrics available (--only-synced requested).")
|
||||
raise typer.Exit(1)
|
||||
sys.exit(1)
|
||||
|
||||
print(result.lyrics)
|
||||
|
||||
@@ -84,27 +103,41 @@ def fetch(
|
||||
# search
|
||||
|
||||
|
||||
@app.command()
|
||||
@app.command
|
||||
def search(
|
||||
title: str = typer.Option(..., "--title", "-t", help="Track title."),
|
||||
artist: Optional[str] = typer.Option(None, "--artist", "-a", help="Artist name."),
|
||||
album: Optional[str] = typer.Option(None, "--album", help="Album name."),
|
||||
trackid: Optional[str] = typer.Option(None, "--trackid", help="Spotify track ID."),
|
||||
length: Optional[int] = typer.Option(
|
||||
None, "--length", "-l", help="Track duration in milliseconds."
|
||||
*,
|
||||
title: Annotated[
|
||||
str, cyclopts.Parameter(name=["--title", "-t"], help="Track title.")
|
||||
],
|
||||
artist: Annotated[
|
||||
str | None, cyclopts.Parameter(name=["--artist", "-a"], help="Artist name.")
|
||||
] = None,
|
||||
album: Annotated[str | None, cyclopts.Parameter(help="Album name.")] = None,
|
||||
trackid: Annotated[str | None, cyclopts.Parameter(help="Spotify track ID.")] = None,
|
||||
length: Annotated[
|
||||
int | None,
|
||||
cyclopts.Parameter(
|
||||
name=["--length", "-l"], help="Track duration in milliseconds."
|
||||
),
|
||||
url: Optional[str] = typer.Option(
|
||||
None, "--url", help="Local file URL (file:///...)."
|
||||
] = None,
|
||||
url: Annotated[
|
||||
str | None, cyclopts.Parameter(help="Local file URL (file:///...).")
|
||||
] = 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."
|
||||
),
|
||||
method: Optional[str] = typer.Option(
|
||||
None, "--method", help="Force a specific source."
|
||||
),
|
||||
no_cache: bool = typer.Option(
|
||||
False, "--no-cache", help="Bypass the cache for this request."
|
||||
),
|
||||
only_synced: bool = typer.Option(
|
||||
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,
|
||||
):
|
||||
"""Search for lyrics by metadata (bypasses MPRIS)."""
|
||||
track = TrackMeta(
|
||||
@@ -122,11 +155,11 @@ def search(
|
||||
|
||||
if not result or not result.lyrics:
|
||||
logger.error("No lyrics found.")
|
||||
raise typer.Exit(1)
|
||||
sys.exit(1)
|
||||
|
||||
if only_synced and result.status != CacheStatus.SUCCESS_SYNCED:
|
||||
logger.error("Only unsynced lyrics available (--only-synced requested).")
|
||||
raise typer.Exit(1)
|
||||
sys.exit(1)
|
||||
|
||||
print(result.lyrics)
|
||||
|
||||
@@ -134,32 +167,39 @@ def search(
|
||||
# export
|
||||
|
||||
|
||||
@app.command()
|
||||
@app.command
|
||||
def export(
|
||||
output: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--output",
|
||||
"-o",
|
||||
*,
|
||||
output: Annotated[
|
||||
str | None,
|
||||
cyclopts.Parameter(
|
||||
name=["--output", "-o"],
|
||||
help="Output file path (default: <Artist> - <Title>.lrc).",
|
||||
),
|
||||
method: Optional[str] = typer.Option(
|
||||
None, "--method", help="Force a specific source."
|
||||
),
|
||||
no_cache: bool = typer.Option(False, "--no-cache", help="Bypass cache."),
|
||||
overwrite: bool = typer.Option(
|
||||
False, "--overwrite", "-f", help="Overwrite existing file."
|
||||
] = 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 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."""
|
||||
track = get_current_track(_player)
|
||||
if not track:
|
||||
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)
|
||||
if not result or not result.lyrics:
|
||||
logger.error("No lyrics available to export.")
|
||||
raise typer.Exit(1)
|
||||
sys.exit(1)
|
||||
|
||||
# Build default output path
|
||||
if not output:
|
||||
@@ -176,7 +216,7 @@ def export(
|
||||
|
||||
if os.path.exists(output) and not overwrite:
|
||||
logger.error(f"File exists: {output} (use -f to overwrite)")
|
||||
raise typer.Exit(1)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
with open(output, "w", encoding="utf-8") as f:
|
||||
@@ -184,45 +224,67 @@ def export(
|
||||
logger.info(f"Exported lyrics to {output}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to write file: {e}")
|
||||
raise typer.Exit(1)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# cache
|
||||
# cache subcommands
|
||||
|
||||
|
||||
@app.command()
|
||||
def cache(
|
||||
clear: bool = typer.Option(False, "--clear", help="Clear the entire cache."),
|
||||
clear_current: bool = typer.Option(
|
||||
False, "--clear-current", help="Clear cache for the current track."
|
||||
),
|
||||
prune: bool = typer.Option(False, "--prune", help="Remove expired entries."),
|
||||
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."
|
||||
),
|
||||
@cache_app.command
|
||||
def query(
|
||||
*,
|
||||
all: Annotated[
|
||||
bool,
|
||||
cyclopts.Parameter(name="--all", negative="", help="Dump all cache entries."),
|
||||
] = False,
|
||||
):
|
||||
"""Manage the local SQLite cache."""
|
||||
if clear:
|
||||
manager.cache.clear_all()
|
||||
"""Show cached entries for the current track."""
|
||||
if all:
|
||||
rows = manager.cache.query_all()
|
||||
if not rows:
|
||||
print("Cache is empty.")
|
||||
return
|
||||
for row in rows:
|
||||
_print_cache_row(row)
|
||||
print()
|
||||
return
|
||||
|
||||
if clear_current:
|
||||
track = get_current_track(_player)
|
||||
if not track:
|
||||
logger.error("No active playing track found.")
|
||||
raise typer.Exit(1)
|
||||
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)
|
||||
return
|
||||
|
||||
if prune:
|
||||
|
||||
@cache_app.command
|
||||
def prune():
|
||||
"""Remove expired cache entries."""
|
||||
manager.cache.prune()
|
||||
return
|
||||
|
||||
if stats:
|
||||
|
||||
@cache_app.command
|
||||
def stats():
|
||||
"""Show cache statistics."""
|
||||
s = manager.cache.stats()
|
||||
print("=== Cache Statistics ===")
|
||||
print(f"Total entries : {s['total']}")
|
||||
@@ -236,30 +298,9 @@ def cache(
|
||||
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()
|
||||
if not rows:
|
||||
print("Cache is empty.")
|
||||
return
|
||||
for row in rows:
|
||||
_print_cache_row(row)
|
||||
print()
|
||||
return
|
||||
|
||||
logger.info(
|
||||
"No action specified. Try --stats, --query, --query-all, "
|
||||
"--prune, --clear, or --clear-current."
|
||||
)
|
||||
# helpers
|
||||
|
||||
|
||||
def _print_track_cache(track: TrackMeta) -> None:
|
||||
@@ -317,7 +358,7 @@ def _print_cache_row(row: dict, indent: str = "") -> None:
|
||||
|
||||
|
||||
def run():
|
||||
app()
|
||||
app.meta()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+12
-3
@@ -14,6 +14,7 @@ Fetch pipeline:
|
||||
|
||||
from typing import Optional
|
||||
from loguru import logger
|
||||
from typing import Literal
|
||||
|
||||
from .fetchers.netease import NeteaseFetcher
|
||||
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 .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:
|
||||
"""Normalize unsynced lyrics so every line has a [00:00.00] tag.
|
||||
@@ -65,7 +71,7 @@ class LrcManager:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.cache = CacheEngine()
|
||||
self.fetchers: dict[str, BaseFetcher] = {
|
||||
self.fetchers: dict[FetcherMethodType, BaseFetcher] = {
|
||||
"local": LocalFetcher(),
|
||||
"cache-search": CacheSearchFetcher(self.cache),
|
||||
"spotify": SpotifyFetcher(),
|
||||
@@ -73,9 +79,12 @@ class LrcManager:
|
||||
"lrclib-search": LrclibSearchFetcher(),
|
||||
"netease": NeteaseFetcher(),
|
||||
}
|
||||
assert set(self.fetchers) == set(METHODS), (
|
||||
f"METHODS and fetchers out of sync: {set(METHODS) ^ set(self.fetchers)}"
|
||||
)
|
||||
|
||||
def _build_sequence(
|
||||
self, track: TrackMeta, force_method: Optional[str] = None
|
||||
self, track: TrackMeta, force_method: Optional[FetcherMethodType] = None
|
||||
) -> list[BaseFetcher]:
|
||||
"""Determine the ordered list of fetchers to try."""
|
||||
if force_method:
|
||||
@@ -103,7 +112,7 @@ class LrcManager:
|
||||
def fetch_for_track(
|
||||
self,
|
||||
track: TrackMeta,
|
||||
force_method: Optional[str] = None,
|
||||
force_method: Optional[FetcherMethodType] = None,
|
||||
bypass_cache: bool = False,
|
||||
) -> Optional[LyricResult]:
|
||||
"""Fetch lyrics for *track* using the fallback pipeline.
|
||||
|
||||
+8
-2
@@ -4,11 +4,12 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "lrcfetch"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
description = "Fetch line-synced lyrics for your music player."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"cyclopts>=4.10.1",
|
||||
"dbus-next>=0.2.3",
|
||||
"httpx>=0.28.1",
|
||||
"loguru>=0.7.3",
|
||||
@@ -16,8 +17,13 @@ dependencies = [
|
||||
"platformdirs>=4.9.4",
|
||||
"pydantic>=2.12.5",
|
||||
"python-dotenv>=1.2.2",
|
||||
"typer>=0.24.1",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
lrcfetch = "lrcfetch.cli:run"
|
||||
|
||||
[tool.ruff.lint]
|
||||
ignore = ["E402"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["ruff>=0.15.8"]
|
||||
|
||||
@@ -2,15 +2,6 @@ version = 1
|
||||
revision = 3
|
||||
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]]
|
||||
name = "annotated-types"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "certifi"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "colorama"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "dbus-next"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
@@ -132,9 +153,10 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "lrcfetch"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "cyclopts" },
|
||||
{ name = "dbus-next" },
|
||||
{ name = "httpx" },
|
||||
{ name = "loguru" },
|
||||
@@ -142,11 +164,16 @@ dependencies = [
|
||||
{ name = "platformdirs" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "typer" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "cyclopts", specifier = ">=4.10.1" },
|
||||
{ name = "dbus-next", specifier = ">=0.2.3" },
|
||||
{ name = "httpx", specifier = ">=0.28.1" },
|
||||
{ name = "loguru", specifier = ">=0.7.3" },
|
||||
@@ -154,11 +181,10 @@ requires-dist = [
|
||||
{ name = "platformdirs", specifier = ">=4.9.4" },
|
||||
{ name = "pydantic", specifier = ">=2.12.5" },
|
||||
{ name = "python-dotenv", specifier = ">=1.2.2" },
|
||||
{ name = "typer", specifier = ">=0.24.1" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = []
|
||||
dev = [{ name = "ruff", specifier = ">=0.15.8" }]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
@@ -299,27 +325,41 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellingham"
|
||||
version = "1.5.4"
|
||||
name = "rich-rst"
|
||||
version = "1.3.2"
|
||||
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 = [
|
||||
{ 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]]
|
||||
name = "typer"
|
||||
version = "0.24.1"
|
||||
name = "ruff"
|
||||
version = "0.15.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ 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" }
|
||||
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" }
|
||||
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]]
|
||||
|
||||
Reference in New Issue
Block a user