Align the fetch-lyrics summary with fetch-artists via shared utils
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,9 +44,10 @@ pop-fem-audit/
|
||||
│ │ ├── fetch_lyrics.py # fetch missing lyrics from the
|
||||
│ │ │ # public APIs into the lyrics dir
|
||||
│ │ ├── models.py # SQLAlchemy ORM 資料模型
|
||||
│ │ └── run_llm.py # API runner:2+1 協定、Batch API、
|
||||
│ │ # 寫入引數指定的 runs 目錄;執行
|
||||
│ │ # 方式 pop-fem-audit-tools run-llm
|
||||
│ │ ├── run_llm.py # API runner:2+1 協定、Batch API、
|
||||
│ │ │ # 寫入引數指定的 runs 目錄;執行
|
||||
│ │ │ # 方式 pop-fem-audit-tools run-llm
|
||||
│ │ └── utils.py # 共用工具(format_duration)
|
||||
│ └── tests/ # 單元測試(unittest)
|
||||
├── runs/ # 每次執行的完整稽核紀錄(進 git)
|
||||
│ └── <階段>/<日期>-<定義檔版本>/
|
||||
|
||||
@@ -60,6 +60,14 @@ pop\_fem\_audit\_tools.run\_llm module
|
||||
:show-inheritance:
|
||||
:undoc-members:
|
||||
|
||||
pop\_fem\_audit\_tools.utils module
|
||||
-----------------------------------
|
||||
|
||||
.. automodule:: pop_fem_audit_tools.utils
|
||||
:members:
|
||||
:show-inheritance:
|
||||
:undoc-members:
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ from sqlalchemy.orm import Session
|
||||
from . import VERSION
|
||||
from .database import ds
|
||||
from .models import Artist, Song, SongArtist
|
||||
from .utils import format_duration
|
||||
|
||||
API_URL: str = "https://www.wikidata.org/w/api.php"
|
||||
"""The URL of the Wikidata API endpoint."""
|
||||
@@ -779,25 +780,6 @@ def write_snapshot(file: TextIO) -> None:
|
||||
writer.writerows(ordered)
|
||||
|
||||
|
||||
def format_duration(seconds: float) -> str:
|
||||
"""Format an elapsed duration for the closing summary line.
|
||||
|
||||
:param seconds: The elapsed duration, in seconds.
|
||||
:return: The duration formatted ``mm:ss``, or ``h:mm:ss``
|
||||
once it reaches one hour.
|
||||
"""
|
||||
total: int = round(seconds)
|
||||
hours: int
|
||||
remainder: int
|
||||
hours, remainder = divmod(total, 3600)
|
||||
minutes: int
|
||||
secs: int
|
||||
minutes, secs = divmod(remainder, 60)
|
||||
if hours > 0:
|
||||
return f"{hours}:{minutes:02d}:{secs:02d}"
|
||||
return f"{minutes:02d}:{secs:02d}"
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
"""Fetch the artist metadata from Wikidata.
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ from .models import (
|
||||
Song,
|
||||
SongArtist,
|
||||
)
|
||||
from .utils import format_duration
|
||||
|
||||
PROVENANCE_FIELDS: Sequence[str] = (
|
||||
"song_id", "source", "method", "acquired_at", "note")
|
||||
@@ -228,6 +229,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
:return: The exit status: 0 on success, misses included,
|
||||
non-zero on a setup error.
|
||||
"""
|
||||
started: float = time.monotonic()
|
||||
args: argparse.Namespace = parse_args(argv)
|
||||
fetcher: LyricsFetcher = LyricsFetcher()
|
||||
fetched: int = 0
|
||||
@@ -264,6 +266,9 @@ def main(argv: list[str] | None = None) -> int:
|
||||
return 1
|
||||
finally:
|
||||
session.close()
|
||||
print(f"done: {fetched} fetched, {missed} missed",
|
||||
attempted: int = fetched + missed
|
||||
elapsed: str = format_duration(time.monotonic() - started)
|
||||
print(f"Done. Fetched lyrics for {fetched}/{attempted}"
|
||||
f" songs. {elapsed} elapsed.",
|
||||
file=sys.stderr)
|
||||
return 0
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Tools for A Feminist Audit of Pop Music.
|
||||
# Copyright 2026 imacat. All rights reserved.
|
||||
# Authors:
|
||||
# imacat@mail.imacat.idv.tw (imacat), 2026/8/4
|
||||
"""The shared utilities of the package."""
|
||||
|
||||
|
||||
def format_duration(seconds: float) -> str:
|
||||
"""Format an elapsed duration for the closing summary line.
|
||||
|
||||
:param seconds: The elapsed duration, in seconds.
|
||||
:return: The duration formatted ``mm:ss``, or ``h:mm:ss``
|
||||
once it reaches one hour.
|
||||
"""
|
||||
total: int = round(seconds)
|
||||
hours: int
|
||||
remainder: int
|
||||
hours, remainder = divmod(total, 3600)
|
||||
minutes: int
|
||||
secs: int
|
||||
minutes, secs = divmod(remainder, 60)
|
||||
if hours > 0:
|
||||
return f"{hours}:{minutes:02d}:{secs:02d}"
|
||||
return f"{minutes:02d}:{secs:02d}"
|
||||
@@ -587,17 +587,6 @@ class TestFetchArtists(unittest.TestCase):
|
||||
urlopen.assert_not_called()
|
||||
self.assertIn("error:", stderr)
|
||||
|
||||
def test_format_duration_under_hour(self) -> None:
|
||||
"""Test the mm:ss format for a duration under one hour."""
|
||||
self.assertEqual(
|
||||
fetch_artists.format_duration(205), "03:25")
|
||||
|
||||
def test_format_duration_over_hour(self) -> None:
|
||||
"""Test the h:mm:ss format once the duration reaches an
|
||||
hour."""
|
||||
self.assertEqual(
|
||||
fetch_artists.format_duration(6439), "1:47:19")
|
||||
|
||||
def test_summary_line_exact_shape(self) -> None:
|
||||
"""Test the exact wording and timing of the summary
|
||||
line."""
|
||||
|
||||
@@ -170,7 +170,8 @@ class TestFetchLyrics(unittest.TestCase):
|
||||
["1", "lyrics.ovh", "api-fetch"])
|
||||
self.assertNotEqual(rows[1][3], "")
|
||||
self.assertEqual(rows[1][4], "")
|
||||
self.assertIn("1 fetched, 0 missed", stderr)
|
||||
self.assertIn(
|
||||
"Done. Fetched lyrics for 1/1 songs.", stderr)
|
||||
|
||||
def test_lrclib_fallback(self) -> None:
|
||||
"""Test that an ovh miss falls back to an LRCLIB hit."""
|
||||
@@ -247,7 +248,8 @@ class TestFetchLyrics(unittest.TestCase):
|
||||
self.assertFalse((self.__lyrics / "1.txt").exists())
|
||||
self.assertFalse(self.__provenance.exists())
|
||||
self.assertIn("song 1 \"Hello\": miss", stderr)
|
||||
self.assertIn("0 fetched, 1 missed", stderr)
|
||||
self.assertIn(
|
||||
"Done. Fetched lyrics for 0/1 songs.", stderr)
|
||||
|
||||
def test_cached_song_skipped(self) -> None:
|
||||
"""Test that a cached song triggers no HTTP request."""
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Tools for A Feminist Audit of Pop Music.
|
||||
# Copyright 2026 imacat. All rights reserved.
|
||||
# Authors:
|
||||
# imacat@mail.imacat.idv.tw (imacat), 2026/8/4
|
||||
"""Unit tests for the shared utilities module."""
|
||||
import unittest
|
||||
|
||||
from pop_fem_audit_tools import utils
|
||||
|
||||
|
||||
class TestUtils(unittest.TestCase):
|
||||
"""Test cases for the shared utilities."""
|
||||
|
||||
def test_format_duration_under_hour(self) -> None:
|
||||
"""Test the mm:ss format for a duration under one hour."""
|
||||
self.assertEqual(utils.format_duration(205), "03:25")
|
||||
|
||||
def test_format_duration_over_hour(self) -> None:
|
||||
"""Test the h:mm:ss format once the duration reaches an
|
||||
hour."""
|
||||
self.assertEqual(utils.format_duration(6439), "1:47:19")
|
||||
Reference in New Issue
Block a user