Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-mediafile for
openSUSE:Factory checked in at 2026-04-25 21:38:04
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-mediafile (Old)
and /work/SRC/openSUSE:Factory/.python-mediafile.new.11940 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-mediafile"
Sat Apr 25 21:38:04 2026 rev:9 rq:1349216 version:0.17.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-mediafile/python-mediafile.changes
2026-04-20 16:11:52.122564528 +0200
+++
/work/SRC/openSUSE:Factory/.python-mediafile.new.11940/python-mediafile.changes
2026-04-25 21:42:58.955538825 +0200
@@ -1,0 +2,9 @@
+Sat Apr 25 10:22:25 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 0.17.0:
+ * Added synced_lyrics field backed by the ID3v2 SYLT
+ (synchronized lyrics) frame. Reads and writes a list of
+ (text, milliseconds) tuples for MP3, AIFF, DSF, and WAVE
+ files. Non-ID3 formats return None.
+
+-------------------------------------------------------------------
Old:
----
mediafile-0.16.2.tar.gz
New:
----
mediafile-0.17.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-mediafile.spec ++++++
--- /var/tmp/diff_new_pack.KXNnhV/_old 2026-04-25 21:42:59.491560693 +0200
+++ /var/tmp/diff_new_pack.KXNnhV/_new 2026-04-25 21:42:59.491560693 +0200
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-mediafile
-Version: 0.16.2
+Version: 0.17.0
Release: 0
Summary: Read and write audio files tags in Python
License: MIT
@@ -29,7 +29,7 @@
BuildRequires: %{python_module mutagen >= 1.46}
BuildRequires: %{python_module pip}
BuildRequires: %{python_module poetry-core}
-BuildRequires: %{python_module pytest}
+BuildRequires: %{python_module pytest >= 8}
BuildRequires: %{python_module wheel}
BuildRequires: fdupes
BuildRequires: python-rpm-macros
++++++ mediafile-0.16.2.tar.gz -> mediafile-0.17.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/mediafile-0.16.2/docs/changelog.rst
new/mediafile-0.17.0/docs/changelog.rst
--- old/mediafile-0.16.2/docs/changelog.rst 2026-04-18 00:35:44.000000000
+0200
+++ new/mediafile-0.17.0/docs/changelog.rst 2026-04-24 21:05:44.000000000
+0200
@@ -4,6 +4,13 @@
Upcoming
--------
+v0.17.0
+-------
+
+- Added ``synced_lyrics`` field backed by the ID3v2 ``SYLT`` (synchronized
+ lyrics) frame. Reads and writes a list of ``(text, milliseconds)`` tuples for
+ MP3, AIFF, DSF, and WAVE files. Non-ID3 formats return ``None``.
+
v0.16.2
-------
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/mediafile-0.16.2/mediafile/__init__.py
new/mediafile-0.17.0/mediafile/__init__.py
--- old/mediafile-0.16.2/mediafile/__init__.py 2026-04-18 00:35:44.000000000
+0200
+++ new/mediafile-0.17.0/mediafile/__init__.py 2026-04-24 21:05:44.000000000
+0200
@@ -66,6 +66,7 @@
MP3SlashPackStorageStyle,
MP3SoundCheckStorageStyle,
MP3StorageStyle,
+ MP3SYLTStorageStyle,
MP3UFIDStorageStyle,
MP4BoolStorageStyle,
MP4ImageStorageStyle,
@@ -503,6 +504,10 @@
StorageStyle("LYRICS"),
ASFStorageStyle("WM/Lyrics"),
)
+ synced_lyrics = MediaField(
+ MP3SYLTStorageStyle(),
+ out_type=list,
+ )
comments = MediaField(
MP3DescStorageStyle(key="COMM"),
MP4StorageStyle("\xa9cmt"),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/mediafile-0.16.2/mediafile/storage/__init__.py
new/mediafile-0.17.0/mediafile/storage/__init__.py
--- old/mediafile-0.16.2/mediafile/storage/__init__.py 2026-04-18
00:35:44.000000000 +0200
+++ new/mediafile-0.17.0/mediafile/storage/__init__.py 2026-04-24
21:05:44.000000000 +0200
@@ -12,6 +12,7 @@
MP3SlashPackStorageStyle,
MP3SoundCheckStorageStyle,
MP3StorageStyle,
+ MP3SYLTStorageStyle,
MP3UFIDStorageStyle,
)
from .mp4 import (
@@ -42,6 +43,7 @@
"MP4TupleStorageStyle",
"MP3ListStorageStyle",
"MP3UFIDStorageStyle",
+ "MP3SYLTStorageStyle",
"MP3ListDescStorageStyle",
"MP4StorageStyle",
"MP4BoolStorageStyle",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/mediafile-0.16.2/mediafile/storage/mp3.py
new/mediafile-0.17.0/mediafile/storage/mp3.py
--- old/mediafile-0.16.2/mediafile/storage/mp3.py 2026-04-18
00:35:44.000000000 +0200
+++ new/mediafile-0.17.0/mediafile/storage/mp3.py 2026-04-24
21:05:44.000000000 +0200
@@ -304,3 +304,43 @@
def __init__(self, index=0, **kwargs):
super().__init__(**kwargs)
self.index = index
+
+
+class MP3SYLTStorageStyle(MP3StorageStyle):
+ """Storage for SYLT (synchronized lyrics) ID3 frames.
+
+ Reads and writes a list of ``(text, milliseconds)`` tuples, which is the
+ native structure of the ID3v2 SYLT frame (format=2, milliseconds).
+ Returns ``None`` when no SYLT frame is present.
+ """
+
+ def __init__(self):
+ super().__init__(key="SYLT")
+
+ def fetch(self, mutagen_file):
+ frames = mutagen_file.tags.getall("SYLT")
+ if frames:
+ return list(frames[0].text)
+ return None
+
+ def store(self, mutagen_file, value):
+ import mutagen.id3
+
+ if not value:
+ mutagen_file.tags.delall("SYLT")
+ return
+
+ frame = mutagen.id3.SYLT(
+ encoding=3,
+ lang="XXX",
+ format=2,
+ type=1,
+ text=value,
+ )
+ mutagen_file.tags.setall("SYLT", [frame])
+
+ def serialize(self, value):
+ return value
+
+ def deserialize(self, value):
+ return value
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/mediafile-0.16.2/pyproject.toml
new/mediafile-0.17.0/pyproject.toml
--- old/mediafile-0.16.2/pyproject.toml 2026-04-18 00:35:44.000000000 +0200
+++ new/mediafile-0.17.0/pyproject.toml 2026-04-24 21:05:44.000000000 +0200
@@ -1,6 +1,6 @@
[project]
name = "mediafile"
-version = "0.16.2"
+version = "0.17.0"
description = "A simple, cross-format library for reading and writing media
file metadata."
authors = [{ name = "Adrian Sampson", email = "[email protected]" }]
readme = "README.rst"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/mediafile-0.16.2/test/test_mediafile.py
new/mediafile-0.17.0/test/test_mediafile.py
--- old/mediafile-0.16.2/test/test_mediafile.py 2026-04-18 00:35:44.000000000
+0200
+++ new/mediafile-0.17.0/test/test_mediafile.py 2026-04-24 21:05:44.000000000
+0200
@@ -1145,6 +1145,7 @@
"albumartists_sort",
"subtitle",
"remixers",
+ "synced_lyrics",
)
)
self.assertCountEqual(MediaFile.fields(), fields)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/mediafile-0.16.2/test/test_mediafile_edge.py
new/mediafile-0.17.0/test/test_mediafile_edge.py
--- old/mediafile-0.16.2/test/test_mediafile_edge.py 2026-04-18
00:35:44.000000000 +0200
+++ new/mediafile-0.17.0/test/test_mediafile_edge.py 2026-04-24
21:05:44.000000000 +0200
@@ -432,6 +432,78 @@
pass
+SYNCED_LYRICS = [("hello", 1000), ("world", 2500)]
+
+
+def _copy_fixture(tmp_path, name):
+ src = os.path.join(_common.RSRC, name.encode("utf8"))
+ dst = os.path.join(os.fsencode(str(tmp_path)), name.encode("utf8"))
+ shutil.copy(src, dst)
+ return dst
+
+
+class TestSyncedLyrics:
+ """Tests for the ``synced_lyrics`` field backed by the SYLT ID3 frame."""
+
+ @pytest.mark.parametrize("ext", ["mp3", "aiff"])
+ def test_write_and_read(self, tmp_path, ext):
+ path = _copy_fixture(tmp_path, f"empty.{ext}")
+ mf = mediafile.MediaFile(path)
+ mf.synced_lyrics = SYNCED_LYRICS
+ mf.save()
+
+ mf2 = mediafile.MediaFile(path)
+ assert mf2.synced_lyrics == SYNCED_LYRICS
+
+ @pytest.mark.parametrize("clear_value", [None, []])
+ def test_clear(self, tmp_path, clear_value):
+ path = _copy_fixture(tmp_path, "empty.mp3")
+ mf = mediafile.MediaFile(path)
+ mf.synced_lyrics = SYNCED_LYRICS
+ mf.save()
+
+ mf2 = mediafile.MediaFile(path)
+ mf2.synced_lyrics = clear_value
+ mf2.save()
+
+ mf3 = mediafile.MediaFile(path)
+ assert not mf3.synced_lyrics
+
+ def test_no_sylt_returns_falsy(self, tmp_path):
+ path = _copy_fixture(tmp_path, "empty.mp3")
+ mf = mediafile.MediaFile(path)
+ assert not mf.synced_lyrics
+
+ def test_synced_and_plain_lyrics_are_independent(self, tmp_path):
+ path = _copy_fixture(tmp_path, "empty.mp3")
+ mf = mediafile.MediaFile(path)
+ mf.lyrics = "plain text"
+ mf.synced_lyrics = SYNCED_LYRICS
+ mf.save()
+
+ mf2 = mediafile.MediaFile(path)
+ assert mf2.lyrics == "plain text"
+ assert mf2.synced_lyrics == SYNCED_LYRICS
+
+ @pytest.mark.parametrize("ext", ["flac", "ogg", "m4a", "wma", "wv", "ape"])
+ def test_non_id3_returns_falsy(self, tmp_path, ext):
+ path = _copy_fixture(tmp_path, f"empty.{ext}")
+ mf = mediafile.MediaFile(path)
+ assert not mf.synced_lyrics
+
+ def test_sylt_lang_is_xxx(self, tmp_path):
+ """The SYLT frame should use 'XXX' (undetermined) as language code."""
+ path = _copy_fixture(tmp_path, "empty.mp3")
+ mf = mediafile.MediaFile(path)
+ mf.synced_lyrics = SYNCED_LYRICS
+ mf.save()
+
+ tags = mutagen.id3.ID3(path)
+ frames = tags.getall("SYLT")
+ assert len(frames) == 1
+ assert frames[0].lang == "XXX"
+
+
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)