187 lines
5.6 KiB
Python
187 lines
5.6 KiB
Python
|
|
import json
|
||
|
|
import shutil
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from mutagen import File as MutagenFile
|
||
|
|
from mutagen.id3 import COMM, WXXX
|
||
|
|
|
||
|
|
from mopidy_webhooks import metadata
|
||
|
|
|
||
|
|
DATA_DIR = Path(__file__).parent / "data"
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def tagged_episode(tmp_path):
|
||
|
|
episode = tmp_path / "2026-09-22 Episode One.mp3"
|
||
|
|
shutil.copy(DATA_DIR / "silence.mp3", episode)
|
||
|
|
audio = MutagenFile(episode, easy=True)
|
||
|
|
audio["title"] = ["Episode One"]
|
||
|
|
audio["artist"] = ["Jane Host"]
|
||
|
|
audio["album"] = ["The Test Podcast"]
|
||
|
|
audio["tracknumber"] = ["42"]
|
||
|
|
audio["date"] = ["2026-09-22"]
|
||
|
|
audio["genre"] = ["Technology"]
|
||
|
|
audio.save()
|
||
|
|
|
||
|
|
raw = MutagenFile(episode)
|
||
|
|
raw.tags.add(
|
||
|
|
COMM(encoding=3, lang="eng", desc="", text="A tag description")
|
||
|
|
)
|
||
|
|
raw.tags.add(WXXX(encoding=3, desc="", url="https://example.com/episode"))
|
||
|
|
raw.save()
|
||
|
|
return episode
|
||
|
|
|
||
|
|
|
||
|
|
def test_resolve_media_path_for_file_uri(tagged_episode):
|
||
|
|
path = metadata.resolve_media_path(tagged_episode.as_uri(), [])
|
||
|
|
|
||
|
|
assert path == str(tagged_episode)
|
||
|
|
|
||
|
|
|
||
|
|
def test_resolve_media_path_for_local_uri(tagged_episode, tmp_path):
|
||
|
|
uri = f"local:podcast:{tagged_episode.name}"
|
||
|
|
|
||
|
|
path = metadata.resolve_media_path(uri, [str(tmp_path)])
|
||
|
|
|
||
|
|
assert path == str(tagged_episode)
|
||
|
|
|
||
|
|
|
||
|
|
def test_resolve_media_path_for_local_track_uri(tagged_episode, tmp_path):
|
||
|
|
uri = f"local:track:{tagged_episode.name}"
|
||
|
|
|
||
|
|
path = metadata.resolve_media_path(uri, [str(tmp_path)])
|
||
|
|
|
||
|
|
assert path == str(tagged_episode)
|
||
|
|
|
||
|
|
|
||
|
|
def test_resolve_media_path_percent_decodes_uri(tagged_episode, tmp_path):
|
||
|
|
uri = "local:podcast:2026-09-22%20Episode%20One.mp3"
|
||
|
|
|
||
|
|
path = metadata.resolve_media_path(uri, [str(tmp_path)])
|
||
|
|
|
||
|
|
assert path == str(tagged_episode)
|
||
|
|
|
||
|
|
|
||
|
|
def test_resolve_media_path_returns_none_for_remote_uri():
|
||
|
|
assert metadata.resolve_media_path("spotify:track:123", []) is None
|
||
|
|
assert (
|
||
|
|
metadata.resolve_media_path("podcast+https://example.com/feed#1", [])
|
||
|
|
is None
|
||
|
|
)
|
||
|
|
assert metadata.resolve_media_path("", []) is None
|
||
|
|
|
||
|
|
|
||
|
|
def test_read_tags_returns_normalized_metadata(tagged_episode):
|
||
|
|
tags = metadata.read_tags(str(tagged_episode))
|
||
|
|
|
||
|
|
assert tags["title"] == "Episode One"
|
||
|
|
assert tags["artist"] == "Jane Host"
|
||
|
|
assert tags["album"] == "The Test Podcast"
|
||
|
|
assert tags["track_number"] == "42"
|
||
|
|
assert tags["date"] == "2026-09-22"
|
||
|
|
assert tags["genre"] == "Technology"
|
||
|
|
assert tags["comment"] == "A tag description"
|
||
|
|
assert tags["website"] == "https://example.com/episode"
|
||
|
|
|
||
|
|
|
||
|
|
def test_read_tags_handles_missing_file():
|
||
|
|
assert metadata.read_tags("/does/not/exist.mp3") == {}
|
||
|
|
assert metadata.read_tags(None) == {}
|
||
|
|
|
||
|
|
|
||
|
|
def test_read_sidecar_json_prefers_same_directory(tagged_episode):
|
||
|
|
sidecar = tagged_episode.with_suffix(".json")
|
||
|
|
sidecar.write_text(json.dumps({"feed_url": "https://example.com/feed"}))
|
||
|
|
|
||
|
|
data = metadata.read_sidecar_json(str(tagged_episode))
|
||
|
|
|
||
|
|
assert data["feed_url"] == "https://example.com/feed"
|
||
|
|
|
||
|
|
|
||
|
|
def test_read_metadata_merges_sidecar_and_tags(tagged_episode, tmp_path):
|
||
|
|
metadata_dir = tmp_path / "metadata"
|
||
|
|
metadata_dir.mkdir()
|
||
|
|
sidecar = metadata_dir / f"{tagged_episode.stem}.json"
|
||
|
|
sidecar.write_text(
|
||
|
|
json.dumps(
|
||
|
|
{
|
||
|
|
"title": "Sidecar Title",
|
||
|
|
"date": "2026-09-21T10:00:00",
|
||
|
|
"feed_url": "https://example.com/feed",
|
||
|
|
"guid": "episode-guid",
|
||
|
|
"episode_url": "https://example.com/episode.mp3",
|
||
|
|
"description": "A sidecar description",
|
||
|
|
"duration": 1234,
|
||
|
|
}
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
data = metadata.read_metadata(str(tagged_episode))
|
||
|
|
|
||
|
|
assert data["title"] == "Episode One"
|
||
|
|
assert data["artist"] == "Jane Host"
|
||
|
|
assert data["album"] == "The Test Podcast"
|
||
|
|
assert data["feed_url"] == "https://example.com/feed"
|
||
|
|
assert data["guid"] == "episode-guid"
|
||
|
|
assert data["episode_url"] == "https://example.com/episode.mp3"
|
||
|
|
assert data["description"] == "A sidecar description"
|
||
|
|
assert data["duration"] == 1234
|
||
|
|
assert data["date"] == "2026-09-21T10:00:00"
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize(
|
||
|
|
"value,expected",
|
||
|
|
[
|
||
|
|
("2026-09-22", "2026-09-22"),
|
||
|
|
("2026-09-22T10:00:00", "2026-09-22"),
|
||
|
|
("2026-09", "2026-09-01"),
|
||
|
|
("2026", ""),
|
||
|
|
("", ""),
|
||
|
|
("not a date", ""),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
def test_normalize_date(value, expected):
|
||
|
|
assert metadata.normalize_date(value) == expected
|
||
|
|
|
||
|
|
|
||
|
|
def test_normalize_date_handles_date_objects():
|
||
|
|
import datetime
|
||
|
|
|
||
|
|
assert metadata.normalize_date(datetime.date(2026, 9, 22)) == "2026-09-22"
|
||
|
|
|
||
|
|
|
||
|
|
def test_split_podcast_uri():
|
||
|
|
feed, guid = metadata.split_podcast_uri(
|
||
|
|
"podcast+https://feed.example/rss#abc"
|
||
|
|
)
|
||
|
|
|
||
|
|
assert feed == "https://feed.example/rss"
|
||
|
|
assert guid == "abc"
|
||
|
|
assert metadata.split_podcast_uri("local:track:foo.mp3") == (None, None)
|
||
|
|
|
||
|
|
|
||
|
|
def test_parse_dirs():
|
||
|
|
assert metadata.parse_dirs("/a, /b ,, ") == ["/a", "/b"]
|
||
|
|
assert metadata.parse_dirs("") == []
|
||
|
|
assert metadata.parse_dirs(None) == []
|
||
|
|
assert metadata.parse_dirs(["/a", "/b"]) == ["/a", "/b"]
|
||
|
|
|
||
|
|
|
||
|
|
def test_is_podcast_detection(tagged_episode, tmp_path):
|
||
|
|
assert metadata.is_podcast("local:podcast:foo.mp3")
|
||
|
|
assert metadata.is_podcast("podcast+https://feed.example/rss#abc")
|
||
|
|
assert metadata.is_podcast("file:///media/podcasts/foo.mp3")
|
||
|
|
assert metadata.is_podcast(
|
||
|
|
"file:///tmp/foo.mp3",
|
||
|
|
path=str(tagged_episode),
|
||
|
|
podcast_dirs=[str(tmp_path)],
|
||
|
|
)
|
||
|
|
assert metadata.is_podcast(
|
||
|
|
"file:///tmp/foo.mp3", path="/tmp/foo.mp3", metadata={"feed_url": "x"}
|
||
|
|
)
|
||
|
|
assert not metadata.is_podcast(
|
||
|
|
"local:track:Artist/Album/01%20Song.mp3",
|
||
|
|
path="/tmp/song.mp3",
|
||
|
|
)
|