diff --git a/python-pytaglib.changes b/python-pytaglib.changes index 9438a9d..0d85c6b 100644 --- a/python-pytaglib.changes +++ b/python-pytaglib.changes @@ -1,3 +1,20 @@ +------------------------------------------------------------------- +Fri Mar 1 14:49:51 UTC 2024 - Jaime Marquínez Ferrándiz + +- version update to 2.1.0 + * Fix #110 (broken test) in #113 + * CI: update cibuildwheel; ensure Python-3.12 builds in #116 + * Modernize tooling in #117 +- version update to 2.0.0 + * improve build_taglib.py helper script (now supports all platforms) + * add taglib_version() to the taglib module + * allow using File as a context manager, optionally saving on exit + * new property File.is_closed + * fix #94: Accept os.PathLike in constructor + * File.path is now a Path object +- Add upgrade_taglib_version.patch +- Skip building for python 3.6 on Leap since it now uses a pyproject.toml + ------------------------------------------------------------------- Thu Oct 6 22:10:20 UTC 2022 - Yogalakshmi Arunachalam diff --git a/python-pytaglib.spec b/python-pytaglib.spec index bbd9cf7..57fc29b 100644 --- a/python-pytaglib.spec +++ b/python-pytaglib.spec @@ -1,7 +1,7 @@ # # spec file for package python-pytaglib # -# Copyright (c) 2022 SUSE LLC +# Copyright (c) 2024 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -17,24 +17,29 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} +%{?sle15_python_module_pythons} Name: python-pytaglib -Version: 1.5.0 +Version: 2.1.0 Release: 0 Summary: Metadata "tagging" library based on TagLib License: GPL-3.0-only OR MIT URL: https://github.com/supermihi/pytaglib Source: https://github.com/supermihi/pytaglib/archive/v%{version}.tar.gz +# PATCH-FIX-UPSTREAM +Patch1: upgrade_taglib_version.patch BuildRequires: %{python_module Cython} BuildRequires: %{python_module devel} +BuildRequires: %{python_module pip} BuildRequires: %{python_module pytest} -BuildRequires: %{python_module setuptools} +BuildRequires: %{python_module setuptools >= 61.0.0} +BuildRequires: %{python_module wheel} BuildRequires: fdupes BuildRequires: gcc-c++ BuildRequires: libtag-devel BuildRequires: python-rpm-macros Requires: python-setuptools Requires(post): update-alternatives -Requires(postun):update-alternatives +Requires(postun): update-alternatives %python_subpackages %description @@ -42,18 +47,20 @@ pytaglib is an audio metadata (“tag”) library for Python. It relies on the TagLib C++ library. %prep -%setup -q -n "pytaglib-%{version}" +%autosetup -N -n "pytaglib-%{version}" +%if %{pkg_vcmp libtag-devel >= 2} +%autopatch -p1 1 +%endif # Remove pre-generated source rm -vf src/taglib.cpp sed -i -e "1d" src/pyprinttags.py %build -sed -i "s:\(script_name =\).*:\1 'pyprinttags':" setup.py export PYTAGLIB_CYTHONIZE=1 -%python_build +%pyproject_wheel %install -%python_install +%pyproject_install %python_expand %fdupes %{buildroot}%{$python_sitearch} %python_clone -a %{buildroot}%{_bindir}/pyprinttags @@ -68,11 +75,11 @@ export LANG=en_US.UTF-8 %python_uninstall_alternative pyprinttags %files %{python_files} -%license COPYING +%license LICENSE.txt %doc README.md %{python_sitearch}/taglib*.so %{python_sitearch}/pyprinttags.* -%{python_sitearch}/pytaglib-%{version}-py%{python_version}.egg-info/ +%{python_sitearch}/pytaglib-%{version}.dist-info/ %pycache_only %{python_sitearch}/__pycache__/pyprinttags.* %python_alternative %{_bindir}/pyprinttags diff --git a/upgrade_taglib_version.patch b/upgrade_taglib_version.patch new file mode 100644 index 0000000..b5a4967 --- /dev/null +++ b/upgrade_taglib_version.patch @@ -0,0 +1,410 @@ +Reference: https://github.com/supermihi/pytaglib/pull/123 +Index: pytaglib-2.1.0/.github/workflows/default.yml +=================================================================== +--- pytaglib-2.1.0.orig/.github/workflows/default.yml ++++ pytaglib-2.1.0/.github/workflows/default.yml +@@ -6,12 +6,14 @@ jobs: + build: + strategy: + matrix: +- os: [ubuntu-latest, macos-latest, windows-latest] ++ os: [ubuntu-latest, macos-latest] #, windows-latest] + runs-on: ${{ matrix.os }} + env: +- CIBW_SKIP: "*p36-* *p37-*" ++ #CIBW_SKIP: "*p36-* *p37-*" ++ CIBW_BUILD: "cp311-*" + CIBW_ARCHS: auto64 + CIBW_ARCHS_MACOS: "x86_64 arm64" ++ MACOSX_DEPLOYMENT_TARGET: "10.14" + steps: + - uses: actions/checkout@v3 + - name: Set up Python +@@ -24,9 +26,6 @@ jobs: + with: + path: build/taglib + key: taglib-windows-${{ hashFiles('build_taglib.py') }} +- - name: Install TagLib (Linux) +- if: ${{ runner.os == 'Linux' }} +- run: sudo apt-get install -y libtag1-dev + - name: install pip dependencies (Linux) + if: ${{ runner.os == 'Linux' }} + run: | +Index: pytaglib-2.1.0/build_taglib.py +=================================================================== +--- pytaglib-2.1.0.orig/build_taglib.py ++++ pytaglib-2.1.0/build_taglib.py +@@ -1,4 +1,5 @@ + import hashlib ++import os + import platform + import shutil + import subprocess +@@ -8,22 +9,24 @@ import urllib.request + from argparse import ArgumentParser + from pathlib import Path + +-is_x64 = sys.maxsize > 2**32 ++is_x64 = sys.maxsize > 2 ** 32 + arch = "x64" if is_x64 else "x32" + system = platform.system() + python_version = platform.python_version() + here = Path(__file__).resolve().parent +-default_taglib_path = here / "build" / "taglib" / f"{system}-{arch}-py{python_version}" + +-taglib_version = "1.13.1" ++taglib_version = "2.0" + taglib_release = f"https://github.com/taglib/taglib/archive/refs/tags/v{taglib_version}.tar.gz" +-taglib_sha256sum = "c8da2b10f1bfec2cd7dbfcd33f4a2338db0765d851a50583d410bacf055cfd0b" ++taglib_sha256sum = "e36ea877a6370810b97d84cf8f72b1e4ed205149ab3ac8232d44c850f38a2859" ++ ++utfcpp_version = "4.0.5" ++utfcpp_release = f"https://github.com/nemtrif/utfcpp/archive/refs/tags/v{utfcpp_version}.tar.gz" + + + class Configuration: + def __init__(self): +- self.tl_install_dir = default_taglib_path + self.build_path = here / "build" ++ self.tl_install_dir = self.build_path / "taglib" / f"{system}-{arch}-py{python_version}" + self.clean = False + + @property +@@ -31,31 +34,63 @@ class Configuration: + return self.build_path / f"taglib-{taglib_version}.tar.gz" + + @property ++ def utfcpp_download_dest(self): ++ return self.build_path / f"utfcpp-{utfcpp_version}.tar.gz" ++ ++ @property + def tl_extract_dir(self): + return self.build_path / f"taglib-{taglib_version}" + ++ @property ++ def utfcpp_extract_dir(self): ++ return self.build_path / f"utfcpp-{utfcpp_version}" + +-def download(config: Configuration): +- target = config.tl_download_dest ++ @property ++ def utfcpp_include_dir(self): ++ return self.utfcpp_extract_dir / "source" ++ ++ ++def _download_file(url: str, target: Path, sha256sum: str = None): + if target.exists(): + print("skipping download, file exists") +- else: +- print(f"downloading taglib {taglib_version} ...") +- response = urllib.request.urlopen(taglib_release) +- data = response.read() +- target.parent.mkdir(exist_ok=True, parents=True) +- target.write_bytes(data) ++ return ++ print(f"downloading {url} ...") ++ response = urllib.request.urlopen(url) ++ data = response.read() ++ target.parent.mkdir(exist_ok=True, parents=True) ++ target.write_bytes(data) ++ if sha256sum is None: ++ return + the_hash = hashlib.sha256(target.read_bytes()).hexdigest() +- assert the_hash == taglib_sha256sum ++ if the_hash != taglib_sha256sum: ++ error = f'checksum of downloaded file ({the_hash}) does not match expected hash ({taglib_sha256sum})' ++ raise RuntimeError(error) ++ ++ ++def download(config: Configuration): ++ _download_file(taglib_release, config.tl_download_dest, taglib_sha256sum) ++ _download_file(utfcpp_release, config.utfcpp_download_dest) ++ ++ ++def _extract_tar(archive: Path, target: Path): ++ if target.exists(): ++ print(f"extracted directory {target} found; skipping tar") ++ return ++ print(f"extracting {archive} ...") ++ tar = tarfile.open(archive) ++ tar.extractall(target.parent) + + + def extract(config: Configuration): +- if config.tl_extract_dir.exists(): +- print("extracted taglib found. Skipping tar") +- else: +- print("extracting tarball") +- tar = tarfile.open(config.tl_download_dest) +- tar.extractall(config.tl_extract_dir.parent) ++ _extract_tar(config.tl_download_dest, config.tl_extract_dir) ++ _extract_tar(config.utfcpp_download_dest, config.utfcpp_extract_dir) ++ ++ ++def copy_utfcpp(config: Configuration): ++ target = config.tl_extract_dir / "3rdparty" / "utfcpp" ++ if target.exists(): ++ shutil.rmtree(target) ++ shutil.copytree(config.utfcpp_extract_dir, target) + + + def cmake_clean(config: Configuration): +@@ -85,6 +120,7 @@ def cmake_config(config: Configuration): + elif system == "Linux": + args.append("-DCMAKE_POSITION_INDEPENDENT_CODE=ON") + args.append(f"-DCMAKE_INSTALL_PREFIX={config.tl_install_dir}") ++ args.append(f"-DCMAKE_CXX_FLAGS=-I{config.tl_extract_dir / '3rdparty' / 'utfcpp' / 'source'}") + args.append(".") + config.tl_install_dir.mkdir(exist_ok=True, parents=True) + call_cmake(config, *args) +@@ -132,15 +168,16 @@ def run(): + print(f"building taglib on {system}, arch {arch}, for python {python_version} ...") + config = parse_args() + tag_lib = ( +- config.tl_install_dir +- / "lib" +- / ("tag.lib" if system == "Windows" else "libtag.a") ++ config.tl_install_dir ++ / "lib" ++ / ("tag.lib" if system == "Windows" else "libtag.a") + ) + if tag_lib.exists() and not config.clean: + print("installed TagLib found, exiting") + return + download(config) + extract(config) ++ copy_utfcpp(config) + cmake_clean(config) + cmake_config(config) + cmake_build(config) +Index: pytaglib-2.1.0/pyproject.toml +=================================================================== +--- pytaglib-2.1.0.orig/pyproject.toml ++++ pytaglib-2.1.0/pyproject.toml +@@ -40,4 +40,5 @@ package-dir = { "" = "src" } + [tool.cibuildwheel] + test-extras = ["tests"] + test-command = "pytest {project}/tests" +-before-build = "python build_taglib.py --clean" +\ No newline at end of file ++before-build = "python build_taglib.py --clean" ++skip = "cp36-* cp37-*" +\ No newline at end of file +Index: pytaglib-2.1.0/src/ctypes.pxd +=================================================================== +--- pytaglib-2.1.0.orig/src/ctypes.pxd ++++ pytaglib-2.1.0/src/ctypes.pxd +@@ -1,5 +1,5 @@ + # -*- coding: utf-8 -*- +-# Copyright 2011-2018 Michael Helmling, michaelhelmling@posteo.de ++# Copyright 2011-2024 Michael Helmling, michaelhelmling@posteo.de + # + # This program is free software; you can redistribute it and/or modify + # it under the terms of the GNU General Public License version 3 as +@@ -7,12 +7,9 @@ + + """This file contains the external C/C++ definitions used by taglib.pyx.""" + +-from libc.stddef cimport wchar_t + from libcpp.list cimport list + from libcpp.map cimport map + from libcpp.string cimport string +-from cpython.mem cimport PyMem_Free +-from cpython.object cimport PyObject + + + cdef extern from 'taglib/tstring.h' namespace 'TagLib::String': +@@ -42,14 +39,19 @@ cdef extern from 'taglib/tpropertymap.h' + StringList& unsupportedData() + int size() + +- ++ + cdef extern from 'taglib/audioproperties.h' namespace 'TagLib': + cdef cppclass AudioProperties: +- int length() ++ int lengthInMilliseconds() + int bitrate() + int sampleRate() + int channels() + ++cdef extern from 'taglib/audioproperties.h' namespace 'TagLib::AudioProperties': ++ cdef enum ReadStyle: ++ Fast = 0 ++ Average = 1 ++ Accurate = 2 + + cdef extern from 'taglib/tfile.h' namespace 'TagLib': + cdef cppclass File: +@@ -62,21 +64,30 @@ cdef extern from 'taglib/tfile.h' namesp + void removeUnsupportedProperties(StringList&) + + +-IF UNAME_SYSNAME == "Windows": +- cdef extern from 'taglib/fileref.h' namespace 'TagLib::FileRef': +- cdef File * create(const wchar_t *) except + +- cdef extern from "Python.h": +- cdef wchar_t *PyUnicode_AsWideCharString(PyObject *path, Py_ssize_t *size) +- cdef inline File* create_wrapper(unicode path): +- cdef wchar_t *wchar_path = PyUnicode_AsWideCharString(path, NULL) +- cdef File * file = create(wchar_path) +- PyMem_Free(wchar_path) +- return file +-ELSE: +- cdef extern from 'taglib/fileref.h' namespace 'TagLib::FileRef': +- cdef File* create(const char*) except + +- cdef inline File* create_wrapper(unicode path): +- return create(path.encode('utf-8')) ++cdef extern from 'taglib/tiostream.h' namespace 'TagLib': ++ IF UNAME_SYSNAME != "Windows": ++ ctypedef char* FileName ++ ELSE: ++ cdef cppclass FileName: ++ FileName(const char*) ++ ++cdef extern from 'taglib/fileref.h' namespace 'TagLib': ++ cdef cppclass FileRef: ++ FileRef(FileName, boolean, ReadStyle) except + ++ File* file() ++ ++ AudioProperties *audioProperties() ++ bint save() except + ++ PropertyMap properties() ++ PropertyMap setProperties(PropertyMap&) ++ void removeUnsupportedProperties(StringList&) ++ ++cdef inline FileRef* create_wrapper(char* path) except +: ++ IF UNAME_SYSNAME != "Windows": ++ return new FileRef(path, True, ReadStyle.Average) ++ ELSE: ++ cdef FileName fn = FileName(path) ++ return new FileRef(fn, True, ReadStyle.Average) + + cdef extern from 'taglib/taglib.h': + int TAGLIB_MAJOR_VERSION +Index: pytaglib-2.1.0/src/taglib.pyx +=================================================================== +--- pytaglib-2.1.0.orig/src/taglib.pyx ++++ pytaglib-2.1.0/src/taglib.pyx +@@ -43,12 +43,12 @@ cdef dict propertyMapToDict(ctypes.Prope + + cdef class File: + """Class representing an audio file with metadata ("tags"). +- ++ + To read tags from an audio file, create a *File* object, passing the file's path to the + constructor (should be a unicode string): +- ++ + >>> f = taglib.File('/path/to/file.ogg') +- ++ + The tags are stored in the attribute *tags* as a *dict* mapping strings (tag names) + to lists of strings (tag values). + +@@ -59,30 +59,30 @@ cdef class File: + as strings (e.g. cover art, proprietary data written by some programs, ...), according + identifiers will be placed into the *unsupported* attribute of the File object. Using the + method *removeUnsupportedProperties*, some or all of those can be removed. +- ++ + Additionally, the readonly attributes *length*, *bitrate*, *sampleRate*, and *channels* are + available with their obvious meanings. + + >>> print('File length: {}'.format(f.length)) +- ++ + Changes to the *tags* attribute are stored using the *save* method. + + >>> f.save() + """ +- cdef ctypes.File *cFile ++ cdef ctypes.FileRef *cFile + cdef public dict tags + cdef readonly object path + cdef readonly list unsupported + cdef readonly object save_on_exit + + def __cinit__(self, path, save_on_exit: bool = False): +- if not isinstance(path, os.PathLike): +- if not isinstance(path, unicode): +- path = path.decode('utf8') ++ if not isinstance(path, Path): ++ if isinstance(path, bytes): ++ path = path.decode('utf-8') + path = Path(path) + self.path = path +- self.cFile = ctypes.create_wrapper(str(self.path)) +- if not self.cFile or not self.cFile.isValid(): ++ self.cFile = ctypes.create_wrapper(str(path).encode('utf-8')) ++ if self.cFile is NULL or self.cFile.file() is NULL or not self.cFile.file().isValid(): + raise OSError(f'Could not read file {path}') + + def __init__(self, path, save_on_exit: bool = False): +@@ -97,7 +97,7 @@ cdef class File: + This method is not accessible from Python, and is called only once, immediately after + object creation. + """ +- ++ + cdef: + ctypes.PropertyMap cTags = self.cFile.properties() + ctypes.String cString +@@ -109,7 +109,7 @@ cdef class File: + + def save(self): + """Store the tags currently hold in the `tags` attribute into the file. +- ++ + If some tags cannot be stored because the underlying metadata format does not support them, + the unsuccesful tags are returned as a "sub-dictionary" of `self.tags` which will be empty + if everything is ok. +@@ -143,7 +143,7 @@ cdef class File: + if not success: + raise OSError('Unable to save tags: Unknown OS error') + return propertyMapToDict(cRemaining) +- ++ + def removeUnsupportedProperties(self, properties): + """This is a direct binding for the corresponding TagLib method.""" + if not self.cFile: +@@ -173,32 +173,32 @@ cdef class File: + property length: + def __get__(self): + self.check_closed() +- return self.cFile.audioProperties().length() +- ++ return self.cFile.audioProperties().lengthInMilliseconds() / 1_000 ++ + property bitrate: + def __get__(self): + self.check_closed() + return self.cFile.audioProperties().bitrate() +- ++ + property sampleRate: + def __get__(self): + self.check_closed() + return self.cFile.audioProperties().sampleRate() +- ++ + property channels: + def __get__(self): + self.check_closed() + return self.cFile.audioProperties().channels() +- ++ + property readOnly: + def __get__(self): + self.check_closed() +- return self.cFile.readOnly() ++ return self.cFile.file().readOnly() + + cdef check_closed(self): + if self.is_closed: + raise ValueError('I/O operation on closed file.') +- ++ + def __enter__(self): + return self + diff --git a/v1.5.0.tar.gz b/v1.5.0.tar.gz deleted file mode 100644 index a786df9..0000000 --- a/v1.5.0.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3dc3dba61beb7accf6eef3a515c122a817ed2668b3ae03d85598c8d9861bb933 -size 469483 diff --git a/v2.1.0.tar.gz b/v2.1.0.tar.gz new file mode 100644 index 0000000..82ecbd0 --- /dev/null +++ b/v2.1.0.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbf42eaa0677d9f1691bc1045d7e5c9d97f6d48defcb0e8f1b9451a0db5a75c8 +size 428497