Initial import: Music_Server, MusicFree, catalog-sync

This commit is contained in:
2026-05-23 16:51:14 +08:00
commit 069af30dba
847 changed files with 179878 additions and 0 deletions
+477
View File
@@ -0,0 +1,477 @@
import sqlite3
import tempfile
import unittest
from contextlib import closing
from pathlib import Path
from unittest.mock import ANY, patch
from click.testing import CliRunner
class CatalogCliTests(unittest.TestCase):
def test_init_db_command_creates_sqlite_file(self):
from musicdl.catalogsync.cli import cli
runner = CliRunner()
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
db_path = Path(tmpdir) / "catalogsync.db"
library_root = Path(tmpdir) / "library"
result = runner.invoke(
cli,
["init-db", "--db", str(db_path), "--library-root", str(library_root)],
)
self.assertEqual(0, result.exit_code, msg=result.output)
self.assertTrue(db_path.exists())
with closing(sqlite3.connect(db_path)) as conn:
table_names = {
row[0]
for row in conn.execute(
"SELECT name FROM sqlite_master WHERE type = 'table'"
).fetchall()
}
self.assertIn("songs", table_names)
def test_init_db_command_creates_resolver_stats_side_db(self):
from musicdl.catalogsync.cli import cli
from musicdl.catalogsync.resolver_stats import default_resolver_stats_db_path
runner = CliRunner()
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
db_path = Path(tmpdir) / "catalogsync.db"
library_root = Path(tmpdir) / "library"
result = runner.invoke(
cli,
["init-db", "--db", str(db_path), "--library-root", str(library_root)],
)
self.assertEqual(0, result.exit_code, msg=result.output)
resolver_stats_db_path = default_resolver_stats_db_path(db_path)
self.assertTrue(resolver_stats_db_path.exists())
def test_run_command_wires_collect_sync_and_download_steps(self):
from musicdl.catalogsync.cli import cli
runner = CliRunner()
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
db_path = Path(tmpdir) / "catalogsync.db"
with patch("musicdl.catalogsync.cli.CatalogSyncApplication") as app_cls:
app = app_cls.return_value
result = runner.invoke(
cli,
[
"run",
"--db",
str(db_path),
"--sources",
"netease,qq",
"--download-sources",
"qq,kuwo,migu",
"--library-root",
str(Path(tmpdir) / "library"),
"--workers",
"3",
],
)
self.assertEqual(0, result.exit_code, msg=result.output)
app.collect_playlists.assert_called_once()
app.sync_playlist_catalog.assert_called_once()
app.download_pending.assert_called_once_with(
["netease", "qq"],
limit=None,
workers=3,
download_sources=["qq", "kuwo", "migu"],
lyrics_enabled=True,
overwrite_lyrics=False,
)
def test_run_command_uses_playlist_file_branch_without_collect(self):
from musicdl.catalogsync.cli import cli
runner = CliRunner()
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
db_path = Path(tmpdir) / "catalogsync.db"
playlist_file = Path(tmpdir) / "playlists.txt"
playlist_file.write_text(
"https://music.163.com/#/playlist?id=17745989905\n",
encoding="utf-8",
)
with patch("musicdl.catalogsync.cli.CatalogSyncApplication") as app_cls:
app = app_cls.return_value
result = runner.invoke(
cli,
[
"run",
"--db",
str(db_path),
"--library-root",
str(Path(tmpdir) / "library"),
"--playlist-file",
str(playlist_file),
"--download-sources",
"qq,kuwo",
],
)
self.assertEqual(0, result.exit_code, msg=result.output)
app.collect_playlists.assert_not_called()
app.run_playlist_file.assert_called_once_with(
playlist_file=str(playlist_file),
limit=None,
workers=10,
download_sources=["qq", "kuwo"],
lyrics_enabled=True,
overwrite_lyrics=False,
)
def test_download_command_defaults_workers_to_ten(self):
from musicdl.catalogsync.cli import cli
runner = CliRunner()
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
db_path = Path(tmpdir) / "catalogsync.db"
with patch("musicdl.catalogsync.cli.CatalogSyncApplication") as app_cls:
app = app_cls.return_value
result = runner.invoke(
cli,
[
"download",
"--db",
str(db_path),
"--sources",
"netease,qq",
"--download-sources",
"qq,kuwo,migu",
"--library-root",
str(Path(tmpdir) / "library"),
],
)
self.assertEqual(0, result.exit_code, msg=result.output)
app.download_pending.assert_called_once_with(
["netease", "qq"],
limit=None,
workers=10,
download_sources=["qq", "kuwo", "migu"],
lyrics_enabled=True,
overwrite_lyrics=False,
)
def test_download_command_reads_workers_from_download_workers_env(self):
from musicdl.catalogsync.cli import cli
runner = CliRunner()
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
db_path = Path(tmpdir) / "catalogsync.db"
with patch("musicdl.catalogsync.cli.CatalogSyncApplication") as app_cls:
app = app_cls.return_value
result = runner.invoke(
cli,
[
"download",
"--db",
str(db_path),
"--sources",
"netease,qq",
"--download-sources",
"qq,kuwo,migu",
"--library-root",
str(Path(tmpdir) / "library"),
],
env={"DOWNLOAD_WORKERS": "8"},
)
self.assertEqual(0, result.exit_code, msg=result.output)
app.download_pending.assert_called_once_with(
["netease", "qq"],
limit=None,
workers=8,
download_sources=["qq", "kuwo", "migu"],
lyrics_enabled=True,
overwrite_lyrics=False,
)
def test_download_command_forwards_workers(self):
from musicdl.catalogsync.cli import cli
runner = CliRunner()
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
db_path = Path(tmpdir) / "catalogsync.db"
with patch("musicdl.catalogsync.cli.CatalogSyncApplication") as app_cls:
app = app_cls.return_value
result = runner.invoke(
cli,
[
"download",
"--db",
str(db_path),
"--sources",
"netease,qq",
"--download-sources",
"qq,kuwo,migu",
"--library-root",
str(Path(tmpdir) / "library"),
"--workers",
"5",
],
)
self.assertEqual(0, result.exit_code, msg=result.output)
app.download_pending.assert_called_once_with(
["netease", "qq"],
limit=None,
workers=5,
download_sources=["qq", "kuwo", "migu"],
lyrics_enabled=True,
overwrite_lyrics=False,
)
def test_download_command_forwards_lyrics_flags(self):
from musicdl.catalogsync.cli import cli
runner = CliRunner()
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
db_path = Path(tmpdir) / "catalogsync.db"
with patch("musicdl.catalogsync.cli.CatalogSyncApplication") as app_cls:
app = app_cls.return_value
result = runner.invoke(
cli,
[
"download",
"--db",
str(db_path),
"--sources",
"netease",
"--download-sources",
"qq",
"--library-root",
str(Path(tmpdir) / "library"),
"--no-lyrics",
"--overwrite-lyrics",
],
)
self.assertEqual(0, result.exit_code, msg=result.output)
app.download_pending.assert_called_once_with(
["netease"],
limit=None,
workers=10,
download_sources=["qq"],
lyrics_enabled=False,
overwrite_lyrics=True,
)
def test_lyrics_command_wires_application_method_and_filters(self):
from musicdl.catalogsync.cli import cli
runner = CliRunner()
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
db_path = Path(tmpdir) / "catalogsync.db"
with patch("musicdl.catalogsync.cli.CatalogSyncApplication") as app_cls:
app = app_cls.return_value
def sync_local_lyrics_side_effect(*args, **kwargs):
progress_callback = kwargs["progress_callback"]
progress_callback(
total=10,
processed=0,
saved=0,
skipped=0,
failed=0,
progress_percent=0,
)
progress_callback(
total=10,
processed=10,
saved=7,
skipped=2,
failed=1,
progress_percent=100,
)
return {"total": 10, "processed": 10, "saved": 7, "skipped": 2, "failed": 1}
app.sync_local_lyrics.side_effect = sync_local_lyrics_side_effect
result = runner.invoke(
cli,
[
"lyrics",
"--db",
str(db_path),
"--sources",
"netease,qq",
"--playlist-ids",
"12,15",
"--limit",
"200",
"--workers",
"8",
"--overwrite-lyrics",
],
)
self.assertEqual(0, result.exit_code, msg=result.output)
app.sync_local_lyrics.assert_called_once_with(
sources=["netease", "qq"],
playlist_ids=[12, 15],
limit=200,
workers=8,
progress_callback=ANY,
overwrite_lyrics=True,
)
self.assertIn("Lyrics progress: 0/10 (0%)", result.output)
self.assertIn("Lyrics progress: 10/10 (100%)", result.output)
def test_register_object_backend_command_wires_application_method(self):
from musicdl.catalogsync.cli import cli
runner = CliRunner()
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
db_path = Path(tmpdir) / "catalogsync.db"
with patch("musicdl.catalogsync.cli.CatalogSyncApplication") as app_cls:
app = app_cls.return_value
result = runner.invoke(
cli,
[
"register-object-backend",
"--db",
str(db_path),
"--backend",
"main-s3",
"--bucket",
"music-bucket",
"--endpoint",
"https://s3.example.com",
"--region",
"auto",
"--base-prefix",
"music",
"--credential-env-prefix",
"CATALOGSYNC_MAIN_S3",
],
)
self.assertEqual(0, result.exit_code, msg=result.output)
app.register_object_backend.assert_called_once_with(
backend_name="main-s3",
container_name="music-bucket",
endpoint="https://s3.example.com",
region="auto",
base_prefix="music",
credential_env_prefix="CATALOGSYNC_MAIN_S3",
addressing_style=None,
public_base_url=None,
)
def test_upload_command_wires_application_method_and_filters(self):
from musicdl.catalogsync.cli import cli
runner = CliRunner()
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
db_path = Path(tmpdir) / "catalogsync.db"
with patch("musicdl.catalogsync.cli.CatalogSyncApplication") as app_cls:
app = app_cls.return_value
result = runner.invoke(
cli,
[
"upload",
"--db",
str(db_path),
"--backend",
"main-s3",
"--sources",
"netease,qq",
"--playlist-ids",
"12,15",
"--limit",
"200",
"--workers",
"4",
],
)
self.assertEqual(0, result.exit_code, msg=result.output)
app.upload_files.assert_called_once_with(
backend_name="main-s3",
sources=["netease", "qq"],
playlist_ids=[12, 15],
limit=200,
workers=4,
)
def test_serve_command_wires_ops_web_app_and_uvicorn(self):
from musicdl.catalogsync.cli import cli
runner = CliRunner()
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
db_path = Path(tmpdir) / "catalogsync.db"
env_file = Path(tmpdir) / "catalogsync.env"
fake_app = object()
with patch(
"musicdl.catalogsync.cli.create_ops_web_app",
return_value=fake_app,
) as create_app_mock, patch("musicdl.catalogsync.cli.uvicorn.run") as uvicorn_run_mock:
result = runner.invoke(
cli,
[
"serve",
"--db",
str(db_path),
"--env-file",
str(env_file),
"--host",
"0.0.0.0",
"--port",
"19090",
],
)
self.assertEqual(0, result.exit_code, msg=result.output)
create_app_mock.assert_called_once_with(
db_path=str(db_path),
env_path=str(env_file),
)
uvicorn_run_mock.assert_called_once_with(
fake_app,
host="0.0.0.0",
port=19090,
)
def test_serve_command_rejects_out_of_range_port(self):
from musicdl.catalogsync.cli import cli
runner = CliRunner()
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
db_path = Path(tmpdir) / "catalogsync.db"
env_file = Path(tmpdir) / "catalogsync.env"
with patch("musicdl.catalogsync.cli.create_ops_web_app") as create_app_mock:
result = runner.invoke(
cli,
[
"serve",
"--db",
str(db_path),
"--env-file",
str(env_file),
"--port",
"70000",
],
)
self.assertNotEqual(0, result.exit_code)
self.assertIn("is not in the range", result.output)
create_app_mock.assert_not_called()
if __name__ == "__main__":
unittest.main()