From fd9990ce2b408f51cce67a7f2fd953d645c000aa Mon Sep 17 00:00:00 2001 From: Arnab Chakraborty Date: Sun, 24 Nov 2024 19:48:21 +0530 Subject: [PATCH 01/14] Add Path to metadatabox --- picard/ui/metadatabox.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/picard/ui/metadatabox.py b/picard/ui/metadatabox.py index 6f79dbc109..826d1c83d2 100644 --- a/picard/ui/metadatabox.py +++ b/picard/ui/metadatabox.py @@ -624,6 +624,9 @@ def _update_tags(self, new_selection=True, drop_album_caches=False): tag_diff.add('~length', str(orig_metadata.length), str(new_metadata.length), removable=False, readonly=True) + if (len(files) == 1): + if settings['rename_files'] or settings['move_files']: + tag_diff.add('Path', [file.filename], [file.make_filename(file.filename, orig_metadata)], removable=False, readonly=True) for track in tracks: if track.num_linked_files == 0: From a1cd703b83ecc0004de14c734ba82893e0cb22b7 Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Wed, 13 Nov 2024 22:18:10 +0100 Subject: [PATCH 02/14] Uppercase dict names that are meant to be constant --- picard/util/textencoding.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/picard/util/textencoding.py b/picard/util/textencoding.py index 72a0d3b647..bdde436937 100644 --- a/picard/util/textencoding.py +++ b/picard/util/textencoding.py @@ -81,7 +81,7 @@ # Various bugs and mistakes in this have been ironed out during testing. -_additional_compatibility = { +_ADDITIONAL_COMPATIBILITY = { "\u0276": "Œ", # LATIN LETTER SMALL CAPITAL OE "\u1D00": "A", # LATIN LETTER SMALL CAPITAL A "\u1D01": "Æ", # LATIN LETTER SMALL CAPITAL AE @@ -107,11 +107,11 @@ def unicode_simplify_compatibility(string, pathsave=False, win_compat=False): - interim = ''.join(_replace_char(_additional_compatibility, ch, pathsave, win_compat) for ch in string) + interim = ''.join(_replace_char(_ADDITIONAL_COMPATIBILITY, ch, pathsave, win_compat) for ch in string) return unicodedata.normalize("NFKC", interim) -_simplify_punctuation = { +_SIMPLIFY_PUNCTUATION = { "\u013F": "L", # LATIN CAPITAL LETTER L WITH MIDDLE DOT (compat) "\u0140": "l", # LATIN SMALL LETTER L WITH MIDDLE DOT (compat) "\u2018": "'", # LEFT SINGLE QUOTATION MARK (from ‹character-fallback›) @@ -185,10 +185,10 @@ def unicode_simplify_compatibility(string, pathsave=False, win_compat=False): def unicode_simplify_punctuation(string, pathsave=False, win_compat=False): - return ''.join(_replace_char(_simplify_punctuation, ch, pathsave, win_compat) for ch in string) + return ''.join(_replace_char(_SIMPLIFY_PUNCTUATION, ch, pathsave, win_compat) for ch in string) -_simplify_combinations = { +_SIMPLIFY_COMBINATIONS = { "\u00C6": "AE", # LATIN CAPITAL LETTER AE (from ‹character-fallback›) "\u00D0": "D", # LATIN CAPITAL LETTER ETH "\u00D8": "OE", # LATIN CAPITAL LETTER O WITH STROKE (see https://en.wikipedia.org/wiki/%C3%98) @@ -416,7 +416,7 @@ def unicode_simplify_punctuation(string, pathsave=False, win_compat=False): def _replace_unicode_simplify_combinations(char, pathsave, win_compat): - result = _simplify_combinations.get(char) + result = _SIMPLIFY_COMBINATIONS.get(char) if result is None: return char elif not pathsave: From 5c674804c55c501f1c2958f624252a5a3cea1acf Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Wed, 13 Nov 2024 22:40:23 +0100 Subject: [PATCH 03/14] Wrap few long lines --- picard/util/textencoding.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/picard/util/textencoding.py b/picard/util/textencoding.py index bdde436937..b571dc5b25 100644 --- a/picard/util/textencoding.py +++ b/picard/util/textencoding.py @@ -107,7 +107,10 @@ def unicode_simplify_compatibility(string, pathsave=False, win_compat=False): - interim = ''.join(_replace_char(_ADDITIONAL_COMPATIBILITY, ch, pathsave, win_compat) for ch in string) + interim = ''.join( + _replace_char(_ADDITIONAL_COMPATIBILITY, ch, pathsave, win_compat) + for ch in string + ) return unicodedata.normalize("NFKC", interim) @@ -185,7 +188,10 @@ def unicode_simplify_compatibility(string, pathsave=False, win_compat=False): def unicode_simplify_punctuation(string, pathsave=False, win_compat=False): - return ''.join(_replace_char(_SIMPLIFY_PUNCTUATION, ch, pathsave, win_compat) for ch in string) + return ''.join( + _replace_char(_SIMPLIFY_PUNCTUATION, ch, pathsave, win_compat) + for ch in string + ) _SIMPLIFY_COMBINATIONS = { @@ -427,12 +433,17 @@ def _replace_unicode_simplify_combinations(char, pathsave, win_compat): def unicode_simplify_combinations(string, pathsave=False, win_compat=False): return ''.join( - _replace_unicode_simplify_combinations(c, pathsave, win_compat) for c in string) + _replace_unicode_simplify_combinations(c, pathsave, win_compat) + for c in string + ) def unicode_simplify_accents(string): - result = ''.join(c for c in unicodedata.normalize('NFKD', string) if not unicodedata.combining(c)) - return result + return ''.join( + c + for c in unicodedata.normalize('NFKD', string) + if not unicodedata.combining(c) + ) def asciipunct(string): From 07b3e63d5326884d82e79e09c2b75675106ee21a Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Wed, 13 Nov 2024 22:40:48 +0100 Subject: [PATCH 04/14] map -> mapping as map is a reserved word --- picard/util/textencoding.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/picard/util/textencoding.py b/picard/util/textencoding.py index b571dc5b25..0e18160c65 100644 --- a/picard/util/textencoding.py +++ b/picard/util/textencoding.py @@ -470,9 +470,9 @@ def error_repl(e, repl="_"): return interim.encode('ascii', 'repl').decode('ascii') -def _replace_char(map, ch, pathsave=False, win_compat=False): +def _replace_char(mapping, ch, pathsave=False, win_compat=False): try: - result = map[ch] + result = mapping[ch] if ch != result and pathsave: result = sanitize_filename(result, win_compat=win_compat) return result From 9d63803a2b08bc80a4c3d2b982fa746182a862d6 Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Wed, 13 Nov 2024 22:43:53 +0100 Subject: [PATCH 05/14] Add a blank line after function definition --- picard/util/textencoding.py | 1 + 1 file changed, 1 insertion(+) diff --git a/picard/util/textencoding.py b/picard/util/textencoding.py index 0e18160c65..2f12e1d25d 100644 --- a/picard/util/textencoding.py +++ b/picard/util/textencoding.py @@ -465,6 +465,7 @@ def replace_non_ascii(string, repl="_", pathsave=False, win_compat=False): def error_repl(e, repl="_"): return (repl, e.start + 1) + codecs.register_error('repl', partial(error_repl, repl=repl)) # Decoding and encoding to allow replacements return interim.encode('ascii', 'repl').decode('ascii') From dab23c52cd13b7f07603a4c4b1b7b788c7c969e0 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 1 Dec 2024 12:34:29 +0100 Subject: [PATCH 06/14] CI: Replace deprecated macos-12 image with macos-13 --- .github/workflows/package-macos.yml | 2 +- .github/workflows/package-pypi.yml | 2 +- .github/workflows/run-tests.yml | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/package-macos.yml b/.github/workflows/package-macos.yml index eeae426237..41a4061cff 100644 --- a/.github/workflows/package-macos.yml +++ b/.github/workflows/package-macos.yml @@ -6,7 +6,7 @@ permissions: {} jobs: package-macos: - runs-on: macos-12 + runs-on: macos-13 strategy: matrix: setup: diff --git a/.github/workflows/package-pypi.yml b/.github/workflows/package-pypi.yml index caf26d457a..7e50e98d2b 100644 --- a/.github/workflows/package-pypi.yml +++ b/.github/workflows/package-pypi.yml @@ -76,7 +76,7 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-12, windows-2019] + os: [macos-13, windows-2019] python-version: ['3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index cac8c802f6..2de7727790 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -8,10 +8,10 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [macos-12, ubuntu-latest, windows-2019] + os: [macos-13, ubuntu-latest, windows-2019] python-version: ['3.9', '3.10', '3.11', '3.12'] include: - - os: macos-12 + - os: macos-13 python-version: '3.9' env: CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} @@ -102,7 +102,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [macos-12, ubuntu-latest, windows-latest] + os: [macos-13, ubuntu-latest, windows-latest] python-version: ['3.9', '3.12'] steps: From 9d1c148be16923408b9b55e9b409668e2b83734b Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 1 Dec 2024 11:49:32 +0100 Subject: [PATCH 07/14] PICARD-2991: Handle statvfs errors on file rename --- picard/util/filenaming.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/picard/util/filenaming.py b/picard/util/filenaming.py index 10ea27d50f..cc29e490e4 100644 --- a/picard/util/filenaming.py +++ b/picard/util/filenaming.py @@ -29,6 +29,7 @@ from enum import IntEnum import math import os +from pathlib import Path import re import shutil import struct @@ -326,14 +327,21 @@ def _get_filename_limit(target): limit = limits[target] except KeyError: # we need to call statvfs on an existing target - d = target - while not os.path.exists(d): - d = os.path.dirname(d) + p = Path(target) + while not p.exists(): + p = p.parent # XXX http://bugs.python.org/issue18695 - try: - limit = os.statvfs(d).f_namemax - except UnicodeEncodeError: - limit = os.statvfs(d.encode(_io_encoding)).f_namemax + limit = 0 + while not limit: + try: + try: + limit = os.statvfs(p).f_namemax + except UnicodeEncodeError: + limit = os.statvfs(str(p).encode(_io_encoding)).f_namemax + except (FileNotFoundError, PermissionError): + if p == p.parent: # we reached the root + raise + p = p.parent limits[target] = limit return limit From a07c590f64dc0095503e75d5888243f18bc7f191 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 1 Dec 2024 15:45:48 +0100 Subject: [PATCH 08/14] PICARD-2991: Do not fail to load renaming options page on make_filename errors --- picard/ui/scripteditor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/ui/scripteditor.py b/picard/ui/scripteditor.py index df0054cb9c..330c01a9a2 100644 --- a/picard/ui/scripteditor.py +++ b/picard/ui/scripteditor.py @@ -173,7 +173,7 @@ def _example_to_filename(self, file): if not self.settings['move_files']: return os.path.basename(filename_before), os.path.basename(filename_after) return filename_before, filename_after - except (ScriptError, TypeError, WinPathTooLong): + except (FileNotFoundError, PermissionError, ScriptError, TypeError, WinPathTooLong): return "", "" def update_example_listboxes(self, before_listbox, after_listbox): From c8b94deb2818cf5b3a8d4a750b2fa3961278b4c2 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 2 Dec 2024 17:21:56 +0100 Subject: [PATCH 09/14] PICARD-3008: Handle OverflowError in extract_year_from_date --- picard/util/__init__.py | 2 +- test/test_utils.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/picard/util/__init__.py b/picard/util/__init__.py index 59968c7beb..453f647bff 100644 --- a/picard/util/__init__.py +++ b/picard/util/__init__.py @@ -946,7 +946,7 @@ def extract_year_from_date(dt): return int(dt.get('year')) else: return parse(dt).year - except (TypeError, ValueError): + except (OverflowError, TypeError, ValueError): return None diff --git a/test/test_utils.py b/test/test_utils.py index d131d9689b..2b5068282c 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -172,6 +172,7 @@ def test_string(self): self.assertEqual(extract_year_from_date('2020-02-28'), 2020) self.assertEqual(extract_year_from_date('2015.02'), 2015) self.assertEqual(extract_year_from_date('2015; 2015'), None) + self.assertEqual(extract_year_from_date('20190303201903032019030320190303'), None) # test for the format as supported by ID3 (https://id3.org/id3v2.4.0-structure): yyyy-MM-ddTHH:mm:ss self.assertEqual(extract_year_from_date('2020-07-21T13:00:00'), 2020) From d0da2fa15943f8ba40a41e53c4897ec8c3f4aeba Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 5 Dec 2024 07:58:14 +0100 Subject: [PATCH 10/14] PICARD-3002: Use Azure Trusted Signing for code signing --- .github/workflows/package-windows.yml | 127 ++++++++++------------ scripts/package/win-build-fixes.ps1 | 19 ++++ scripts/package/win-common.ps1 | 49 --------- scripts/package/win-package-appx.ps1 | 60 +--------- scripts/package/win-package-installer.ps1 | 43 -------- scripts/package/win-package-portable.ps1 | 39 ------- 6 files changed, 84 insertions(+), 253 deletions(-) create mode 100644 scripts/package/win-build-fixes.ps1 delete mode 100644 scripts/package/win-package-installer.ps1 delete mode 100644 scripts/package/win-package-portable.ps1 diff --git a/.github/workflows/package-windows.yml b/.github/workflows/package-windows.yml index de841fb37d..2766fccef9 100644 --- a/.github/workflows/package-windows.yml +++ b/.github/workflows/package-windows.yml @@ -9,14 +9,15 @@ jobs: runs-on: windows-2019 strategy: matrix: - type: - - store-app - - signed-app - - installer - - portable + setup: + - type: installer + - type: portable + build-portable: 1 + - type: store-app + disable-autoupdate: 1 fail-fast: false env: - CODESIGN: 0 + CODESIGN: ${{ !!secrets.AZURE_CERT_PROFILE_NAME }} steps: - uses: actions/checkout@v4 with: @@ -32,9 +33,6 @@ jobs: -DiscidVersion $Env:DISCID_VERSION -DiscidSha256Sum $Env:DISCID_SHA256SUM ` -FpcalcVersion $Env:FPCALC_VERSION -FpcalcSha256Sum $Env:FPCALC_SHA256SUM Add-Content $env:GITHUB_PATH "C:\Program Files (x86)\Windows Kits\10\bin\10.0.18362.0\x64" - $ReleaseTag = $(git describe --match "release-*" --abbrev=0 --always HEAD) - $BuildNumber = $(git rev-list --count "$ReleaseTag..HEAD") - Add-Content $env:GITHUB_ENV "BUILD_NUMBER=$BuildNumber" New-Item -Name .\artifacts -ItemType Directory env: DISCID_VERSION: 0.6.4 @@ -57,80 +55,73 @@ jobs: - name: Patch build version if: startsWith(github.ref, 'refs/tags/') != true run: | + $ReleaseTag = $(git describe --match "release-*" --abbrev=0 --always HEAD) + $BuildNumber = $(git rev-list --count "$ReleaseTag..HEAD") python setup.py patch_version --platform=$Env:BUILD_NUMBER.$(git rev-parse --short HEAD) - name: Run tests timeout-minutes: 30 run: pytest --verbose - - name: Prepare code signing certificate - if: matrix.type != 'store-app' + - name: Prepare clean build environment run: | - If ($Env:CODESIGN_P12_URL -And $Env:AWS_ACCESS_KEY_ID) { - pip install awscli - aws s3 cp "$Env:CODESIGN_P12_URL" .\codesign.pfx - Add-Content $env:GITHUB_ENV "CODESIGN=1" - } Else { - Write-Output "::warning::No code signing certificate available, skipping code signing." - } - env: - AWS_DEFAULT_REGION: eu-central-1 - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - CODESIGN_P12_URL: ${{ secrets.CODESIGN_P12_URL }} - - name: Build Windows 10 store app package - if: matrix.type == 'store-app' + Remove-Item -Path build,dist/picard,locale -Recurse -ErrorAction Ignore + python setup.py clean + - name: Build run: | - & .\scripts\package\win-package-appx.ps1 -BuildNumber $Env:BUILD_NUMBER - Move-Item .\dist\*.msix .\artifacts + python setup.py build --build-number=$BuildNumber + python setup.py build_ext + pyinstaller --noconfirm --clean picard.spec + If ($env:PICARD_BUILD_PORTABLE -ne "1") { + dist\picard\_internal\fpcalc -version + } env: PICARD_APPX_PUBLISHER: CN=0A9169B7-05A3-4ED9-8876-830F17846709 - - name: Build Windows 10 signed app package - if: matrix.type == 'signed-app' && env.CODESIGN == '1' - run: | - $CertificateFile = ".\codesign.pfx" - $CertificatePassword = ConvertTo-SecureString -String $Env:CODESIGN_P12_PASSWORD -Force -AsPlainText - & .\scripts\package\win-package-appx.ps1 -BuildNumber $Env:BUILD_NUMBER ` - -CertificateFile $CertificateFile -CertificatePassword $CertificatePassword - Move-Item .\dist\*.msix .\artifacts - env: - CODESIGN_P12_PASSWORD: ${{ secrets.CODESIGN_P12_PASSWORD }} + PICARD_BUILD_PORTABLE: ${{ matrix.setup.build-portable }} + PICARD_DISABLE_AUTOUPDATE: ${{ matrix.setup.disable-autoupdate }} + - name: Sign picard.exe + uses: azure/trusted-signing-action@v0.5.0 + if: matrix.setup.type != 'portable' && env.CODESIGN == 'true' + with: + azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} + azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} + azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }} + endpoint: ${{ secrets.AZURE_ENDPOINT }} + trusted-signing-account-name: ${{ secrets.AZURE_CODE_SIGNING_NAME }} + certificate-profile-name: ${{ secrets.AZURE_CERT_PROFILE_NAME }} + files-folder: dist\picard + files-folder-filter: exe + files-folder-recurse: true + timestamp-rfc3161: http://timestamp.acs.microsoft.com + timestamp-digest: SHA256 - name: Build Windows installer - if: matrix.type == 'installer' + if: matrix.setup.type == 'installer' run: | - # choco install nsis - If ($Env:CODESIGN -eq "1") { - $CertificateFile = ".\codesign.pfx" - $CertificatePassword = ConvertTo-SecureString -String $Env:CODESIGN_P12_PASSWORD -Force -AsPlainText - } Else { - $CertificateFile = $null - $CertificatePassword = $null - } - & .\scripts\package\win-package-installer.ps1 -BuildNumber $Env:BUILD_NUMBER ` - -CertificateFile $CertificateFile -CertificatePassword $CertificatePassword + makensis.exe /INPUTCHARSET UTF8 installer\picard-setup.nsi Move-Item .\installer\*.exe .\artifacts - dist\picard\_internal\fpcalc -version - env: - CODESIGN_P12_PASSWORD: ${{ secrets.CODESIGN_P12_PASSWORD }} - name: Build Windows portable app - if: matrix.type == 'portable' + if: matrix.setup.type == 'portable' run: | - If ($Env:CODESIGN -eq "1") { - $CertificateFile = ".\codesign.pfx" - $CertificatePassword = ConvertTo-SecureString -String $Env:CODESIGN_P12_PASSWORD -Force -AsPlainText - } Else { - $CertificateFile = $null - $CertificatePassword = $null - } - & .\scripts\package\win-package-portable.ps1 -BuildNumber $Env:BUILD_NUMBER ` - -CertificateFile $CertificateFile -CertificatePassword $CertificatePassword Move-Item .\dist\*.exe .\artifacts - env: - CODESIGN_P12_PASSWORD: ${{ secrets.CODESIGN_P12_PASSWORD }} - - name: Cleanup - if: env.CODESIGN == '1' - run: Remove-Item .\codesign.pfx + - name: Build Windows 10 store app package + if: matrix.setup.type == 'store-app' + run: | + & .\scripts\package\win-package-appx.ps1 dist\picard + Move-Item .\dist\*.msix .\artifacts + - name: Sign final executable + uses: azure/trusted-signing-action@v0.5.0 + if: matrix.setup.type != 'store-app' && env.CODESIGN == 'true' + with: + azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} + azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} + azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }} + endpoint: ${{ secrets.AZURE_ENDPOINT }} + trusted-signing-account-name: ${{ secrets.AZURE_CODE_SIGNING_NAME }} + certificate-profile-name: ${{ secrets.AZURE_CERT_PROFILE_NAME }} + files-folder: artifacts + files-folder-filter: exe + timestamp-rfc3161: http://timestamp.acs.microsoft.com + timestamp-digest: SHA256 - name: Archive production artifacts uses: actions/upload-artifact@v4 - if: matrix.type != 'signed-app' || env.CODESIGN == '1' with: - name: windows-${{ matrix.type }} + name: windows-${{ matrix.setup.type }} path: artifacts/ diff --git a/scripts/package/win-build-fixes.ps1 b/scripts/package/win-build-fixes.ps1 new file mode 100644 index 0000000000..4b1ebf4fbb --- /dev/null +++ b/scripts/package/win-build-fixes.ps1 @@ -0,0 +1,19 @@ +# Apply fixes to the build result + +Param( + [ValidateScript({Test-Path $_ -PathType Container})] + [String] + $Path +) + +$ErrorActionPreference = 'Stop' + +$InternalPath = (Join-Path -Path $Path -ChildPath _internal) +# Move all Qt6 DLLs into the main folder to avoid conflicts with system wide +# versions of those dependencies. Since some version PyInstaller tries to +# maintain the file hierarchy of imported modules, but this easily breaks +# DLL loading on Windows. +# Workaround for https://tickets.metabrainz.org/browse/PICARD-2736 +$Qt6Dir = (Join-Path -Path $InternalPath -ChildPath PyQt6\Qt6) +Move-Item -Path (Join-Path -Path $Qt6Dir -ChildPath bin\*.dll) -Destination $Path -Force +Remove-Item -Path (Join-Path -Path $Qt6Dir -ChildPath bin) diff --git a/scripts/package/win-common.ps1 b/scripts/package/win-common.ps1 index 7fb196b30e..e903b8c7c0 100644 --- a/scripts/package/win-common.ps1 +++ b/scripts/package/win-common.ps1 @@ -8,55 +8,6 @@ Param( $CertificatePassword ) -# RFC 3161 timestamp server for code signing -$TimeStampServer = 'http://ts.ssl.com' - -Function CodeSignBinary { - Param( - [ValidateScript({Test-Path $_ -PathType Leaf})] - [String] - $BinaryPath - ) - If ($CertificateFile) { - SignTool sign /v /fd SHA256 /tr "$TimeStampServer" /td sha256 ` - /f "$CertificateFile" /p (ConvertFrom-SecureString -AsPlainText $CertificatePassword) ` - $BinaryPath - ThrowOnExeError "SignTool failed" - } Else { - Write-Output "Skip signing $BinaryPath" - } -} - -Function ThrowOnExeError { - Param( [String]$Message ) - If ($LastExitCode -ne 0) { - Throw $Message - } -} - -Function FinalizePackage { - Param( - [ValidateScript({Test-Path $_ -PathType Container})] - [String] - $Path - ) - - $InternalPath = (Join-Path -Path $Path -ChildPath _internal) - - CodeSignBinary -BinaryPath (Join-Path -Path $Path -ChildPath picard.exe) -ErrorAction Stop - CodeSignBinary -BinaryPath (Join-Path -Path $InternalPath -ChildPath fpcalc.exe) -ErrorAction Stop - CodeSignBinary -BinaryPath (Join-Path -Path $InternalPath -ChildPath discid.dll) -ErrorAction Stop - - # Move all Qt6 DLLs into the main folder to avoid conflicts with system wide - # versions of those dependencies. Since some version PyInstaller tries to - # maintain the file hierarchy of imported modules, but this easily breaks - # DLL loading on Windows. - # Workaround for https://tickets.metabrainz.org/browse/PICARD-2736 - $Qt6Dir = (Join-Path -Path $InternalPath -ChildPath PyQt6\Qt6) - Move-Item -Path (Join-Path -Path $Qt6Dir -ChildPath bin\*.dll) -Destination $Path -Force - Remove-Item -Path (Join-Path -Path $Qt6Dir -ChildPath bin) -} - Function DownloadFile { Param( [Parameter(Mandatory = $true)] diff --git a/scripts/package/win-package-appx.ps1 b/scripts/package/win-package-appx.ps1 index 50f41f8664..6941afc510 100644 --- a/scripts/package/win-package-appx.ps1 +++ b/scripts/package/win-package-appx.ps1 @@ -1,72 +1,24 @@ # Build a MSIX app package for Windows 10 Param( - [ValidateScript({ (Test-Path $_ -PathType Leaf) -or (-not $_) })] + [ValidateScript({ Test-Path $_ -PathType Container })] [String] - $CertificateFile, - [SecureString] - $CertificatePassword, - [Int] - $BuildNumber + $PackageDir ) -# Errors are handled explicitly. Otherwise any output to stderr when -# calling classic Windows exes causes a script error. -# TODO: For PowerShell >= 7.3 use $PSNativeCommandUseErrorActionPreference = $true -$ErrorActionPreference = 'Continue' +$ErrorActionPreference = 'Stop' +$PSNativeCommandUseErrorActionPreference = $true -If (-Not $BuildNumber) { - $BuildNumber = 0 -} - -If (-Not $Certificate -And $CertificateFile) { - $Certificate = Get-PfxCertificate -FilePath $CertificateFile -Password $CertificatePassword -} - -$ScriptDirectory = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent -. $ScriptDirectory\win-common.ps1 -CertificateFile $CertificateFile -CertificatePassword $CertificatePassword - -Write-Output "Building Windows 10 app package..." - -# Set the publisher based on the certificate subject -if ($Certificate) { - $env:PICARD_APPX_PUBLISHER = $Certificate.Subject - Write-Output "Publisher: $env:PICARD_APPX_PUBLISHER" -} - -# Build -Remove-Item -Path build,dist/picard,locale -Recurse -ErrorAction Ignore -python setup.py clean 2>&1 | %{ "$_" } -ThrowOnExeError "setup.py clean failed" -python setup.py build --build-number=$BuildNumber --disable-autoupdate 2>&1 | %{ "$_" } -ThrowOnExeError "setup.py build failed" -python setup.py build_ext -i 2>&1 | %{ "$_" } -ThrowOnExeError "setup.py build_ext -i failed" - -# Package application -pyinstaller --noconfirm --clean picard.spec 2>&1 | %{ "$_" } -ThrowOnExeError "PyInstaller failed" -$PackageDir = (Resolve-Path dist\picard) -FinalizePackage $PackageDir +$PackageDir = (Resolve-Path $PackageDir) # Generate resource files Copy-Item appxmanifest.xml $PackageDir $PriConfigFile = (Join-Path (Resolve-Path .\build) priconfig.xml) Push-Location $PackageDir MakePri createconfig /ConfigXml $PriConfigFile /Default en-US /Overwrite -ThrowOnExeError "MakePri createconfig failed" MakePri new /ProjectRoot $PackageDir /ConfigXml $PriConfigFile -ThrowOnExeError "MakePri new failed" Pop-Location # Generate msix package -$PicardVersion = (python -c "import picard; print(picard.__version__)") -If ($CertificateFile -or $Certificate) { - $PackageFile = "dist\MusicBrainz-Picard-$PicardVersion.msix" -} Else { - $PackageFile = "dist\MusicBrainz-Picard-$PicardVersion-unsigned.msix" -} +$PackageFile = "dist\MusicBrainz-Picard-${PicardVersion}_unsigned.msix" MakeAppx pack /o /h SHA256 /d $PackageDir /p $PackageFile -ThrowOnExeError "MakeAppx failed" - -CodeSignBinary -BinaryPath $PackageFile -ErrorAction Stop diff --git a/scripts/package/win-package-installer.ps1 b/scripts/package/win-package-installer.ps1 deleted file mode 100644 index 4653973df1..0000000000 --- a/scripts/package/win-package-installer.ps1 +++ /dev/null @@ -1,43 +0,0 @@ -# Build a Windows installer - -Param( - [ValidateScript({ (Test-Path $_ -PathType Leaf) -or (-not $_) })] - [String] - $CertificateFile, - [SecureString] - $CertificatePassword, - [Int] - $BuildNumber -) - -# Errors are handled explicitly. Otherwise any output to stderr when -# calling classic Windows exes causes a script error. -$ErrorActionPreference = 'Continue' - -If (-Not $BuildNumber) { - $BuildNumber = 0 -} - -$ScriptDirectory = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent -. $ScriptDirectory\win-common.ps1 -CertificateFile $CertificateFile -CertificatePassword $CertificatePassword - -Write-Output "Building Windows installer..." - -# Build -Remove-Item -Path build,dist/picard,locale -Recurse -ErrorAction Ignore -python setup.py clean 2>&1 | %{ "$_" } -ThrowOnExeError "setup.py clean failed" -python setup.py build --build-number=$BuildNumber 2>&1 | %{ "$_" } -ThrowOnExeError "setup.py build failed" -python setup.py build_ext -i 2>&1 | %{ "$_" } -ThrowOnExeError "setup.py build_ext -i failed" - -# Package application -pyinstaller --noconfirm --clean picard.spec 2>&1 | %{ "$_" } -ThrowOnExeError "PyInstaller failed" -FinalizePackage dist\picard - -# Build the installer -makensis.exe /INPUTCHARSET UTF8 installer\picard-setup.nsi 2>&1 | %{ "$_" } -ThrowOnExeError "NSIS failed" -CodeSignBinary -BinaryPath installer\picard-setup-*.exe -ErrorAction Stop diff --git a/scripts/package/win-package-portable.ps1 b/scripts/package/win-package-portable.ps1 deleted file mode 100644 index 79b4633efc..0000000000 --- a/scripts/package/win-package-portable.ps1 +++ /dev/null @@ -1,39 +0,0 @@ -# Build a portable app - -Param( - [ValidateScript({ (Test-Path $_ -PathType Leaf) -or (-not $_) })] - [String] - $CertificateFile, - [SecureString] - $CertificatePassword, - [Int] - $BuildNumber -) - -# Errors are handled explicitly. Otherwise any output to stderr when -# calling classic Windows exes causes a script error. -$ErrorActionPreference = 'Continue' - -If (-Not $BuildNumber) { - $BuildNumber = 0 -} - -$ScriptDirectory = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent -. $ScriptDirectory\win-common.ps1 -CertificateFile $CertificateFile -CertificatePassword $CertificatePassword - -Write-Output "Building portable exe..." - -# Build -Remove-Item -Path build,locale -Recurse -ErrorAction Ignore -python setup.py clean 2>&1 | %{ "$_" } -ThrowOnExeError "setup.py clean failed" -python setup.py build --build-number=$BuildNumber 2>&1 | %{ "$_" } -ThrowOnExeError "setup.py build failed" -python setup.py build_ext -i 2>&1 | %{ "$_" } -ThrowOnExeError "setup.py build_ext -i failed" - -# Package application -$env:PICARD_BUILD_PORTABLE = '1' -pyinstaller --noconfirm --clean picard.spec 2>&1 | %{ "$_" } -ThrowOnExeError "PyInstaller failed" -CodeSignBinary -BinaryPath dist\MusicBrainz-Picard-*.exe -ErrorAction Stop From a0f0f189f00db2747859eebcc6a22a60eb931286 Mon Sep 17 00:00:00 2001 From: "ApeKattQuest, MonkeyPython" Date: Sun, 8 Dec 2024 21:42:41 +0000 Subject: [PATCH 11/14] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 55.7% (703 of 1260 strings) Co-authored-by: ApeKattQuest, MonkeyPython Translate-URL: https://translations.metabrainz.org/projects/picard/3/app/nb_NO/ Translation: Picard/App --- po/nb.po | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/po/nb.po b/po/nb.po index 9a854cae03..353ac4c37f 100644 --- a/po/nb.po +++ b/po/nb.po @@ -12,14 +12,15 @@ # CatQuest, The Endeavouring Cat, 2014 # "ApeKattQuest, MonkeyPython" , 2024. # "ApeKattQuest, MonkeyPython" , 2024. +# "ApeKattQuest, MonkeyPython" , 2024. msgid "" msgstr "" "Project-Id-Version: MusicBrainz\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "POT-Creation-Date: 2024-08-15 12:56+0200\n" -"PO-Revision-Date: 2024-08-04 18:42+0000\n" -"Last-Translator: \"ApeKattQuest, MonkeyPython\" \n" +"PO-Revision-Date: 2024-12-08 21:42+0000\n" +"Last-Translator: \"ApeKattQuest, MonkeyPython\" " +"\n" "Language-Team: Norwegian Bokmål \n" "Language: nb\n" @@ -27,7 +28,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.6.2\n" +"X-Generator: Weblate 5.8.4\n" "Generated-By: Babel 2.9.1\n" #: picard/album.py:137 @@ -480,11 +481,11 @@ msgstr "Minste likhet for å samsvare filer med spor" #: picard/options.py:298 msgid "Translation locales" -msgstr "" +msgstr "Oversettelsesspråk" #: picard/options.py:299 picard/ui/forms/ui_options_metadata.py:142 msgid "Convert Unicode punctuation characters to ASCII" -msgstr "Konverter Unicode skilletegn til ASCII" +msgstr "Konverter skilletegn fra Unicode til ASCII" #: picard/options.py:300 picard/ui/forms/ui_options_metadata.py:145 msgid "Guess track number and title from filename if empty" @@ -500,7 +501,7 @@ msgstr "Bru utgivelsesrelasjoner" #: picard/options.py:303 msgid "Translation script exceptions" -msgstr "" +msgstr "Unntak av oversettelseskodeskript" #: picard/options.py:304 picard/ui/forms/ui_options_metadata.py:140 msgid "Use standardized artist names" @@ -508,7 +509,7 @@ msgstr "Bruk standardiserte artistnavn" #: picard/options.py:305 picard/ui/forms/ui_options_metadata.py:141 msgid "Use standardized instrument and vocal credits" -msgstr "" +msgstr "Bruk standardiserte instrument- og vokalkreditter" #: picard/options.py:306 #, fuzzy @@ -518,11 +519,11 @@ msgstr "Bru utgivelsesrelasjoner" #: picard/options.py:307 msgid "Translate artist names" -msgstr "" +msgstr "Oversett artistnavn" #: picard/options.py:308 msgid "Translate artist names exception" -msgstr "" +msgstr "Unntak av oversatte artistnavn" #: picard/options.py:309 msgid "Various Artists name" From 0ec85938e875fd8b87e5c780a5d52a407accab7f Mon Sep 17 00:00:00 2001 From: "ApeKattQuest, MonkeyPython" Date: Tue, 10 Dec 2024 18:42:41 +0000 Subject: [PATCH 12/14] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 56.3% (710 of 1260 strings) Co-authored-by: ApeKattQuest, MonkeyPython Translate-URL: https://translations.metabrainz.org/projects/picard/3/app/nb_NO/ Translation: Picard/App --- po/nb.po | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/po/nb.po b/po/nb.po index 353ac4c37f..284ba5517a 100644 --- a/po/nb.po +++ b/po/nb.po @@ -18,7 +18,7 @@ msgstr "" "Project-Id-Version: MusicBrainz\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "POT-Creation-Date: 2024-08-15 12:56+0200\n" -"PO-Revision-Date: 2024-12-08 21:42+0000\n" +"PO-Revision-Date: 2024-12-10 18:42+0000\n" "Last-Translator: \"ApeKattQuest, MonkeyPython\" " "\n" "Language-Team: Norwegian Bokmål Date: Mon, 23 Dec 2024 10:55:56 +0100 Subject: [PATCH 13/14] Add Python 3.13 to Python package metadata --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index b5075f8784..de00670c89 100644 --- a/setup.py +++ b/setup.py @@ -824,6 +824,7 @@ def _get_requirements(): 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', 'Operating System :: MacOS', 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX :: Linux', From e4da13da0943cd5c826c34fd9c44e0e0fa3dad20 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 27 Dec 2024 15:48:38 +0100 Subject: [PATCH 14/14] PICARD-3013: Fix case-only renaming on case-insensitive filesystems --- picard/file.py | 2 +- picard/util/__init__.py | 19 ++++++++++--------- test/test_file.py | 28 ++++++++++++++-------------- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/picard/file.py b/picard/file.py index cd6b1a4da4..dc8a19313c 100644 --- a/picard/file.py +++ b/picard/file.py @@ -545,7 +545,7 @@ def make_filename(self, filename, metadata, settings=None, naming_format=None): new_filename = self._format_filename(new_dirname, new_filename, metadata, settings, naming_format) new_path = os.path.join(new_dirname, new_filename) - return normpath(new_path) + return normpath(new_path, realpath=False) def _rename(self, old_filename, metadata, settings=None): new_filename = self.make_filename(old_filename, metadata, settings) diff --git a/picard/util/__init__.py b/picard/util/__init__.py index 453f647bff..f8e0c40024 100644 --- a/picard/util/__init__.py +++ b/picard/util/__init__.py @@ -4,7 +4,7 @@ # # Copyright (C) 2004 Robert Kaye # Copyright (C) 2006-2009, 2011-2012, 2014 Lukáš Lalinský -# Copyright (C) 2008-2011, 2014, 2018-2023 Philipp Wolfer +# Copyright (C) 2008-2011, 2014, 2018-2024 Philipp Wolfer # Copyright (C) 2009 Carlin Mangar # Copyright (C) 2009 david # Copyright (C) 2010 fatih @@ -232,15 +232,16 @@ def system_supports_long_paths(): return False -def normpath(path): +def normpath(path, realpath=True): path = os.path.normpath(path) - try: - path = os.path.realpath(path) - except OSError as why: - # realpath can fail if path does not exist or is not accessible - # or on Windows if drives are mounted without mount manager - # (see https://tickets.metabrainz.org/browse/PICARD-2425). - log.warning("Failed getting realpath for `%s`: %s", path, why) + if realpath: + try: + path = os.path.realpath(path) + except OSError as why: + # realpath can fail if path does not exist or is not accessible + # or on Windows if drives are mounted without mount manager + # (see https://tickets.metabrainz.org/browse/PICARD-2425). + log.warning("Failed getting realpath for `%s`: %s", path, why) # If the path is longer than 259 characters on Windows, prepend the \\?\ # prefix. This enables access to long paths using the Windows API. See # https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation diff --git a/test/test_file.py b/test/test_file.py index 8722216cbe..768e037f33 100644 --- a/test/test_file.py +++ b/test/test_file.py @@ -2,7 +2,7 @@ # # Picard, the next-generation MusicBrainz tagger # -# Copyright (C) 2018-2022 Philipp Wolfer +# Copyright (C) 2018-2022, 2024 Philipp Wolfer # Copyright (C) 2019-2022 Laurent Monin # Copyright (C) 2021 Bob Swift # Copyright (C) 2021 Sophist-UK @@ -228,18 +228,18 @@ def setUp(self): def test_make_filename_no_move_and_rename(self): filename = self.file.make_filename(self.file.filename, self.metadata) - self.assertEqual(os.path.realpath(self.file.filename), filename) + self.assertEqual(os.path.normpath(self.file.filename), filename) def test_make_filename_rename_only(self): config.setting['rename_files'] = True filename = self.file.make_filename(self.file.filename, self.metadata) - self.assertEqual(os.path.realpath('/somepath/sometitle.mp3'), filename) + self.assertEqual(os.path.normpath('/somepath/sometitle.mp3'), filename) def test_make_filename_move_only(self): config.setting['move_files'] = True filename = self.file.make_filename(self.file.filename, self.metadata) self.assertEqual( - os.path.realpath('/media/music/somealbum/somefile.mp3'), + os.path.normpath('/media/music/somealbum/somefile.mp3'), filename) def test_make_filename_move_and_rename(self): @@ -247,7 +247,7 @@ def test_make_filename_move_and_rename(self): config.setting['move_files'] = True filename = self.file.make_filename(self.file.filename, self.metadata) self.assertEqual( - os.path.realpath('/media/music/somealbum/sometitle.mp3'), + os.path.normpath('/media/music/somealbum/sometitle.mp3'), filename) def test_make_filename_move_relative_path(self): @@ -255,39 +255,39 @@ def test_make_filename_move_relative_path(self): config.setting['move_files_to'] = 'subdir' filename = self.file.make_filename(self.file.filename, self.metadata) self.assertEqual( - os.path.realpath('/somepath/subdir/somealbum/somefile.mp3'), + os.path.normpath('/somepath/subdir/somealbum/somefile.mp3'), filename) def test_make_filename_empty_script(self): config.setting['rename_files'] = True config.setting['file_renaming_scripts'] = {'test_id': {'script': '$noop()'}} filename = self.file.make_filename(self.file.filename, self.metadata) - self.assertEqual(os.path.realpath('/somepath/somefile.mp3'), filename) + self.assertEqual(os.path.normpath('/somepath/somefile.mp3'), filename) def test_make_filename_empty_basename(self): config.setting['move_files'] = True config.setting['rename_files'] = True config.setting['file_renaming_scripts'] = {'test_id': {'script': '/somedir/$noop()'}} filename = self.file.make_filename(self.file.filename, self.metadata) - self.assertEqual(os.path.realpath('/media/music/somedir/somefile.mp3'), filename) + self.assertEqual(os.path.normpath('/media/music/somedir/somefile.mp3'), filename) def test_make_filename_no_extension(self): config.setting['rename_files'] = True file_ = FakeMp3File('/somepath/_') filename = file_.make_filename(file_.filename, self.metadata) - self.assertEqual(os.path.realpath('/somepath/sometitle.mp3'), filename) + self.assertEqual(os.path.normpath('/somepath/sometitle.mp3'), filename) def test_make_filename_lowercase_extension(self): config.setting['rename_files'] = True file_ = FakeMp3File('/somepath/somefile.MP3') filename = file_.make_filename(file_.filename, self.metadata) - self.assertEqual(os.path.realpath('/somepath/sometitle.mp3'), filename) + self.assertEqual(os.path.normpath('/somepath/sometitle.mp3'), filename) def test_make_filename_scripted_extension(self): config.setting['rename_files'] = True config.setting['file_renaming_scripts'] = {'test_id': {'script': '$set(_extension,.foo)%title%'}} filename = self.file.make_filename(self.file.filename, self.metadata) - self.assertEqual(os.path.realpath('/somepath/sometitle.foo'), filename) + self.assertEqual(os.path.normpath('/somepath/sometitle.foo'), filename) def test_make_filename_replace_trailing_dots(self): config.setting['rename_files'] = True @@ -299,7 +299,7 @@ def test_make_filename_replace_trailing_dots(self): }) filename = self.file.make_filename(self.file.filename, metadata) self.assertEqual( - os.path.realpath('/media/music/somealbum_/sometitle.mp3'), + os.path.normpath('/media/music/somealbum_/sometitle.mp3'), filename) @unittest.skipUnless(not IS_WIN, "non-windows test") @@ -313,7 +313,7 @@ def test_make_filename_keep_trailing_dots(self): }) filename = self.file.make_filename(self.file.filename, metadata) self.assertEqual( - os.path.realpath('/media/music/somealbum./sometitle.mp3'), + os.path.normpath('/media/music/somealbum./sometitle.mp3'), filename) def test_make_filename_replace_leading_dots(self): @@ -326,7 +326,7 @@ def test_make_filename_replace_leading_dots(self): }) filename = self.file.make_filename(self.file.filename, metadata) self.assertEqual( - os.path.realpath('/media/music/_somealbum/_sometitle.mp3'), + os.path.normpath('/media/music/_somealbum/_sometitle.mp3'), filename)