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
|
│ │ ├── fetch_lyrics.py # fetch missing lyrics from the
|
||||||
│ │ │ # public APIs into the lyrics dir
|
│ │ │ # public APIs into the lyrics dir
|
||||||
│ │ ├── models.py # SQLAlchemy ORM 資料模型
|
│ │ ├── models.py # SQLAlchemy ORM 資料模型
|
||||||
│ │ └── run_llm.py # API runner:2+1 協定、Batch API、
|
│ │ ├── run_llm.py # API runner:2+1 協定、Batch API、
|
||||||
│ │ # 寫入引數指定的 runs 目錄;執行
|
│ │ │ # 寫入引數指定的 runs 目錄;執行
|
||||||
│ │ # 方式 pop-fem-audit-tools run-llm
|
│ │ │ # 方式 pop-fem-audit-tools run-llm
|
||||||
|
│ │ └── utils.py # 共用工具(format_duration)
|
||||||
│ └── tests/ # 單元測試(unittest)
|
│ └── tests/ # 單元測試(unittest)
|
||||||
├── runs/ # 每次執行的完整稽核紀錄(進 git)
|
├── runs/ # 每次執行的完整稽核紀錄(進 git)
|
||||||
│ └── <階段>/<日期>-<定義檔版本>/
|
│ └── <階段>/<日期>-<定義檔版本>/
|
||||||
|
|||||||
@@ -60,6 +60,14 @@ pop\_fem\_audit\_tools.run\_llm module
|
|||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
|
||||||
|
pop\_fem\_audit\_tools.utils module
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
.. automodule:: pop_fem_audit_tools.utils
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
:undoc-members:
|
||||||
|
|
||||||
Module contents
|
Module contents
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ from sqlalchemy.orm import Session
|
|||||||
from . import VERSION
|
from . import VERSION
|
||||||
from .database import ds
|
from .database import ds
|
||||||
from .models import Artist, Song, SongArtist
|
from .models import Artist, Song, SongArtist
|
||||||
|
from .utils import format_duration
|
||||||
|
|
||||||
API_URL: str = "https://www.wikidata.org/w/api.php"
|
API_URL: str = "https://www.wikidata.org/w/api.php"
|
||||||
"""The URL of the Wikidata API endpoint."""
|
"""The URL of the Wikidata API endpoint."""
|
||||||
@@ -779,25 +780,6 @@ def write_snapshot(file: TextIO) -> None:
|
|||||||
writer.writerows(ordered)
|
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:
|
def main(argv: list[str] | None = None) -> int:
|
||||||
"""Fetch the artist metadata from Wikidata.
|
"""Fetch the artist metadata from Wikidata.
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ from .models import (
|
|||||||
Song,
|
Song,
|
||||||
SongArtist,
|
SongArtist,
|
||||||
)
|
)
|
||||||
|
from .utils import format_duration
|
||||||
|
|
||||||
PROVENANCE_FIELDS: Sequence[str] = (
|
PROVENANCE_FIELDS: Sequence[str] = (
|
||||||
"song_id", "source", "method", "acquired_at", "note")
|
"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,
|
:return: The exit status: 0 on success, misses included,
|
||||||
non-zero on a setup error.
|
non-zero on a setup error.
|
||||||
"""
|
"""
|
||||||
|
started: float = time.monotonic()
|
||||||
args: argparse.Namespace = parse_args(argv)
|
args: argparse.Namespace = parse_args(argv)
|
||||||
fetcher: LyricsFetcher = LyricsFetcher()
|
fetcher: LyricsFetcher = LyricsFetcher()
|
||||||
fetched: int = 0
|
fetched: int = 0
|
||||||
@@ -264,6 +266,9 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
return 1
|
return 1
|
||||||
finally:
|
finally:
|
||||||
session.close()
|
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)
|
file=sys.stderr)
|
||||||
return 0
|
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()
|
urlopen.assert_not_called()
|
||||||
self.assertIn("error:", stderr)
|
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:
|
def test_summary_line_exact_shape(self) -> None:
|
||||||
"""Test the exact wording and timing of the summary
|
"""Test the exact wording and timing of the summary
|
||||||
line."""
|
line."""
|
||||||
|
|||||||
@@ -170,7 +170,8 @@ class TestFetchLyrics(unittest.TestCase):
|
|||||||
["1", "lyrics.ovh", "api-fetch"])
|
["1", "lyrics.ovh", "api-fetch"])
|
||||||
self.assertNotEqual(rows[1][3], "")
|
self.assertNotEqual(rows[1][3], "")
|
||||||
self.assertEqual(rows[1][4], "")
|
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:
|
def test_lrclib_fallback(self) -> None:
|
||||||
"""Test that an ovh miss falls back to an LRCLIB hit."""
|
"""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.__lyrics / "1.txt").exists())
|
||||||
self.assertFalse(self.__provenance.exists())
|
self.assertFalse(self.__provenance.exists())
|
||||||
self.assertIn("song 1 \"Hello\": miss", stderr)
|
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:
|
def test_cached_song_skipped(self) -> None:
|
||||||
"""Test that a cached song triggers no HTTP request."""
|
"""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