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__)
 

Reply via email to