Compare commits
1 Commits
master
...
networkrem
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
784c86aa80 |
4
.github/FUNDING.yml
vendored
Normal file
4
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
github: jonaski
|
||||||
|
patreon: jonaskvinge
|
||||||
|
ko_fi: jonaskvinge
|
||||||
|
custom: https://paypal.me/jonaskvinge
|
||||||
18
.github/workflows/build.yml
vendored
18
.github/workflows/build.yml
vendored
@@ -156,7 +156,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
fedora_version: [ '42', '43', '44' ]
|
fedora_version: [ '41', '42', '43' ]
|
||||||
container:
|
container:
|
||||||
image: fedora:${{matrix.fedora_version}}
|
image: fedora:${{matrix.fedora_version}}
|
||||||
steps:
|
steps:
|
||||||
@@ -542,7 +542,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
ubuntu_version: [ 'noble', 'questing', 'resolute' ]
|
ubuntu_version: [ 'noble', 'plucky', 'questing' ]
|
||||||
container:
|
container:
|
||||||
image: ubuntu:${{matrix.ubuntu_version}}
|
image: ubuntu:${{matrix.ubuntu_version}}
|
||||||
steps:
|
steps:
|
||||||
@@ -596,10 +596,10 @@ jobs:
|
|||||||
qt6-l10n-tools
|
qt6-l10n-tools
|
||||||
rapidjson-dev
|
rapidjson-dev
|
||||||
- name: Install KDSingleApplication
|
- name: Install KDSingleApplication
|
||||||
if: matrix.ubuntu_version != 'noble'
|
if: matrix.ubuntu_version != 'noble' && matrix.ubuntu_version != 'plucky'
|
||||||
run: apt install -y libkdsingleapplication-qt6-dev
|
run: apt install -y libkdsingleapplication-qt6-dev
|
||||||
- name: Build and install KDSingleApplication
|
- name: Build and install KDSingleApplication
|
||||||
if: matrix.ubuntu_version == 'noble'
|
if: matrix.ubuntu_version == 'noble' || matrix.ubuntu_version == 'plucky'
|
||||||
run: |
|
run: |
|
||||||
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
|
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
|
||||||
cd KDSingleApplication
|
cd KDSingleApplication
|
||||||
@@ -639,7 +639,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
ubuntu_version: [ 'noble', 'questing', 'resolute' ]
|
ubuntu_version: [ 'noble', 'plucky', 'questing' ]
|
||||||
container:
|
container:
|
||||||
image: ubuntu:${{matrix.ubuntu_version}}
|
image: ubuntu:${{matrix.ubuntu_version}}
|
||||||
steps:
|
steps:
|
||||||
@@ -747,7 +747,7 @@ jobs:
|
|||||||
df -h
|
df -h
|
||||||
- name: Build FreeBSD
|
- name: Build FreeBSD
|
||||||
id: build-freebsd
|
id: build-freebsd
|
||||||
uses: vmactions/freebsd-vm@v1.3.7
|
uses: vmactions/freebsd-vm@v1.3.2
|
||||||
with:
|
with:
|
||||||
usesh: true
|
usesh: true
|
||||||
mem: 8192
|
mem: 8192
|
||||||
@@ -772,7 +772,7 @@ jobs:
|
|||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Build OpenBSD
|
- name: Build OpenBSD
|
||||||
id: build-openbsd
|
id: build-openbsd
|
||||||
uses: vmactions/openbsd-vm@v1.3.1
|
uses: vmactions/openbsd-vm@v1.2.9
|
||||||
with:
|
with:
|
||||||
usesh: true
|
usesh: true
|
||||||
mem: 4096
|
mem: 4096
|
||||||
@@ -845,7 +845,7 @@ jobs:
|
|||||||
p12-password: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD }}
|
p12-password: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD }}
|
||||||
|
|
||||||
- name: Download macOS dependencies
|
- name: Download macOS dependencies
|
||||||
run: curl -f -O -L https://github.com/strawberrymusicplayer/strawberry-macos-dependencies/releases/latest/download/strawberry-macos-${{env.arch}}-${{env.buildtype}}.tar.xz
|
run: curl -f -O -L https://github.com/strawberrymusicplayer/strawberry-macos-dependencies$(test "${{env.arch}}" = "x86_64" && echo "-intel" || echo "")/releases/latest/download/strawberry-macos-${{env.arch}}-${{env.buildtype}}.tar.xz
|
||||||
|
|
||||||
- name: Extract macOS dependencies
|
- name: Extract macOS dependencies
|
||||||
run: sudo tar -C / -xf strawberry-macos-${{env.arch}}-${{env.buildtype}}.tar.xz
|
run: sudo tar -C / -xf strawberry-macos-${{env.arch}}-${{env.buildtype}}.tar.xz
|
||||||
@@ -898,7 +898,7 @@ jobs:
|
|||||||
- name: Manually Codesign
|
- name: Manually Codesign
|
||||||
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-15-intel'
|
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-15-intel'
|
||||||
working-directory: build
|
working-directory: build
|
||||||
run: codesign -s 383J84DVB6 -f strawberry.app/Contents/Frameworks/{libpcre2-8.0.dylib,libpcre2-16.0.dylib,libpng16.16.dylib,libfreetype.6.dylib,libzstd.1.dylib,libbrotlicommon.1.dylib,libbrotlienc.1.dylib,libbrotlidec.1.dylib,libsoup-3.0.0.dylib,libnghttp2.14.dylib,libpsl.5.dylib} strawberry.app/Contents/Frameworks/png.framework/png strawberry.app
|
run: codesign -s 383J84DVB6 -f strawberry.app/Contents/Frameworks/{libpcre2-8.0.dylib,libpcre2-16.0.dylib,libpng16.16.dylib,libfreetype.6.dylib,libzstd.1.dylib,libbrotlicommon.1.dylib,libbrotlienc.1.dylib} strawberry.app/Contents/Frameworks/png.framework/png strawberry.app
|
||||||
|
|
||||||
- name: Manually Codesign
|
- name: Manually Codesign
|
||||||
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-15'
|
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-15'
|
||||||
|
|||||||
31
.gitignore
vendored
31
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
/build
|
||||||
/bin
|
/bin
|
||||||
/CMakeLists.txt.user
|
/CMakeLists.txt.user
|
||||||
/.qtcreator
|
/.qtcreator
|
||||||
@@ -13,33 +14,3 @@
|
|||||||
/dist/scripts/maketarball.sh
|
/dist/scripts/maketarball.sh
|
||||||
/debian/changelog
|
/debian/changelog
|
||||||
_codeql_detected_source_root
|
_codeql_detected_source_root
|
||||||
|
|
||||||
# Build output (keep build tooling scripts in /build_tools/ tracked)
|
|
||||||
/cmake-build*/
|
|
||||||
/build*/
|
|
||||||
!/build_tools/
|
|
||||||
!/build_tools/**
|
|
||||||
|
|
||||||
# macOS noise
|
|
||||||
.DS_Store
|
|
||||||
/bin
|
|
||||||
/CMakeLists.txt.user
|
|
||||||
/.qtcreator
|
|
||||||
/.kdev4
|
|
||||||
/strawberry.kdev4
|
|
||||||
/.vscode
|
|
||||||
/.code-workspace
|
|
||||||
/.sublime-workspace
|
|
||||||
/.idea
|
|
||||||
/.vs
|
|
||||||
/out
|
|
||||||
/CMakeSettings.json
|
|
||||||
/dist/scripts/maketarball.sh
|
|
||||||
/debian/changelog
|
|
||||||
_codeql_detected_source_root
|
|
||||||
|
|
||||||
# Build output (keep build tooling scripts in /build_tools/ tracked)
|
|
||||||
/cmake-build*/
|
|
||||||
/build*/
|
|
||||||
!/build_tools/
|
|
||||||
!/build_tools/**
|
|
||||||
|
|||||||
11
3rdparty/discord-rpc/CMakeLists.txt
vendored
11
3rdparty/discord-rpc/CMakeLists.txt
vendored
@@ -33,20 +33,9 @@ if(APPLE)
|
|||||||
target_link_libraries(discord-rpc PRIVATE "-framework AppKit")
|
target_link_libraries(discord-rpc PRIVATE "-framework AppKit")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# RapidJSON (as packaged by Homebrew and others) can trigger C++17 deprecation
|
|
||||||
# warnings (e.g. std::iterator) when compiled with AppleClang/libc++.
|
|
||||||
# Keep the suppression narrowly scoped to this 3rdparty target.
|
|
||||||
if(APPLE AND CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
|
||||||
target_compile_options(discord-rpc PRIVATE -Wno-deprecated-declarations)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
target_link_libraries(discord-rpc PRIVATE psapi advapi32)
|
target_link_libraries(discord-rpc PRIVATE psapi advapi32)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(TARGET RapidJSON::RapidJSON)
|
|
||||||
target_link_libraries(discord-rpc PRIVATE RapidJSON::RapidJSON)
|
|
||||||
elseif(RapidJSON_INCLUDE_DIRS)
|
|
||||||
target_include_directories(discord-rpc SYSTEM PRIVATE ${RapidJSON_INCLUDE_DIRS})
|
target_include_directories(discord-rpc SYSTEM PRIVATE ${RapidJSON_INCLUDE_DIRS})
|
||||||
endif()
|
|
||||||
target_include_directories(discord-rpc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(discord-rpc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
|||||||
62
Brewfile
62
Brewfile
@@ -1,62 +0,0 @@
|
|||||||
# Strawberry Music Player (macOS) - Homebrew Bundle
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# brew bundle --file Brewfile
|
|
||||||
#
|
|
||||||
# Notes:
|
|
||||||
# - This is intended for macOS (Apple Silicon or Intel).
|
|
||||||
# - Some Strawberry features are optional and will auto-disable if deps are missing.
|
|
||||||
|
|
||||||
# Build tooling
|
|
||||||
brew "cmake"
|
|
||||||
brew "pkg-config"
|
|
||||||
brew "ninja"
|
|
||||||
|
|
||||||
# Optional (developer): unit tests
|
|
||||||
brew "googletest"
|
|
||||||
|
|
||||||
# Core runtime/build dependencies (required by CMakeLists.txt)
|
|
||||||
brew "qt" # Qt 6 (Core/Gui/Widgets/Network/Sql/Concurrent)
|
|
||||||
brew "vulkan-headers" # helps Qt6Gui's WrapVulkanHeaders dependency on some setups
|
|
||||||
brew "boost"
|
|
||||||
brew "icu4c"
|
|
||||||
brew "glib" # provides glib-2.0 + gobject-2.0 (via pkg-config)
|
|
||||||
brew "glib-networking" # TLS + GIO modules (helps macOS bundling via dist/macos/macgstcopy.sh)
|
|
||||||
brew "sqlite"
|
|
||||||
brew "taglib"
|
|
||||||
brew "gstreamer"
|
|
||||||
|
|
||||||
# Strawberry requires KDAB's KDSingleApplication (CMake package name: KDSingleApplication-qt6).
|
|
||||||
# Homebrew core doesn't consistently provide it, so this repo includes a local formula.
|
|
||||||
# Homebrew requires formulae to be installed from a tap; we tap *this repo* via file://.
|
|
||||||
# Use the Brewfile's directory (repo root) rather than the current working directory,
|
|
||||||
# so `brew bundle --file /path/to/Brewfile` works no matter where you run it from.
|
|
||||||
tap "strawberry/local", "file://#{File.expand_path(__dir__)}"
|
|
||||||
brew "strawberry/local/kdsingleapplication-qt6"
|
|
||||||
brew "strawberry/local/qtsparkle-qt6" # optional: QtSparkle integration
|
|
||||||
brew "strawberry/local/sparkle-framework" # optional: Sparkle integration (framework)
|
|
||||||
brew "strawberry/local/macdeploycheck" # optional: enables CMake target 'deploycheck' (sanity checks deployed .app)
|
|
||||||
|
|
||||||
# Recommended GStreamer plugin sets for broad codec support (matches README guidance)
|
|
||||||
brew "gst-plugins-base"
|
|
||||||
brew "gst-plugins-good"
|
|
||||||
brew "gst-plugins-bad"
|
|
||||||
brew "gst-plugins-ugly"
|
|
||||||
brew "gst-libav"
|
|
||||||
|
|
||||||
# Optional features (silences CMake warnings / enables extra functionality)
|
|
||||||
brew "rapidjson" # enables Discord Rich Presence (DISCORD_RPC)
|
|
||||||
brew "google-sparsehash" # enables stream tagreader (STREAMTAGREADER / libsparsehash)
|
|
||||||
brew "chromaprint" # enables MusicBrainz + song fingerprinting
|
|
||||||
brew "fftw" # enables Moodbar (fftw3)
|
|
||||||
brew "libebur128" # enables EBU R 128 loudness normalization
|
|
||||||
brew "libcdio" # enables Audio CD support
|
|
||||||
brew "libmtp" # enables MTP device support
|
|
||||||
brew "strawberry/local/libgpod" # enables iPod classic support (Homebrew core doesn't provide libgpod)
|
|
||||||
|
|
||||||
# Helpful for Strawberry's macOS "deploy" target (GStreamer dynamically loads libsoup)
|
|
||||||
brew "libsoup"
|
|
||||||
|
|
||||||
# Optional: enable building the CMake "dmg" target (cmake/Dmg.cmake)
|
|
||||||
brew "create-dmg"
|
|
||||||
|
|
||||||
154
CMakeLists.txt
154
CMakeLists.txt
@@ -6,14 +6,6 @@ if(APPLE)
|
|||||||
enable_language(OBJC OBJCXX)
|
enable_language(OBJC OBJCXX)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(APPLE)
|
|
||||||
option(BUILD_FOR_MAC_APP_STORE "Build for Mac App Store (MAS): disables Sparkle + any localhost port-listener OAuth redirect server, and uses MAS-focused defaults." OFF)
|
|
||||||
else()
|
|
||||||
set(BUILD_FOR_MAC_APP_STORE OFF)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set(MACOS_BUNDLE_ID "com.dryark.strawberry" CACHE STRING "macOS bundle identifier (CFBundleIdentifier)")
|
|
||||||
|
|
||||||
if(POLICY CMP0054)
|
if(POLICY CMP0054)
|
||||||
cmake_policy(SET CMP0054 NEW)
|
cmake_policy(SET CMP0054 NEW)
|
||||||
endif()
|
endif()
|
||||||
@@ -40,24 +32,6 @@ if(LINUX)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(APPLE)
|
if(APPLE)
|
||||||
if(BUILD_FOR_MAC_APP_STORE)
|
|
||||||
# MAS builds: Sparkle (and QtSparkle) must be disabled.
|
|
||||||
set(ENABLE_SPARKLE OFF CACHE BOOL "Sparkle integration" FORCE)
|
|
||||||
set(ENABLE_QTSPARKLE OFF CACHE BOOL "QtSparkle integration" FORCE)
|
|
||||||
else()
|
|
||||||
# Find Sparkle early so cmake/Dmg.cmake (deploy target) can bundle it into the app.
|
|
||||||
# Sparkle is optional; if not found, update functionality is disabled.
|
|
||||||
find_library(SPARKLE Sparkle
|
|
||||||
PATHS
|
|
||||||
/Library/Frameworks
|
|
||||||
/System/Library/Frameworks
|
|
||||||
/opt/homebrew/Frameworks
|
|
||||||
/opt/homebrew/opt/sparkle-framework/Frameworks
|
|
||||||
/usr/local/Frameworks
|
|
||||||
/usr/local/opt/sparkle-framework/Frameworks
|
|
||||||
PATH_SUFFIXES Frameworks
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
include(cmake/Dmg.cmake)
|
include(cmake/Dmg.cmake)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -110,6 +84,8 @@ if(MSVC)
|
|||||||
list(APPEND COMPILE_OPTIONS -MP -W4 -wd4702)
|
list(APPEND COMPILE_OPTIONS -MP -W4 -wd4702)
|
||||||
else()
|
else()
|
||||||
list(APPEND COMPILE_OPTIONS
|
list(APPEND COMPILE_OPTIONS
|
||||||
|
$<$<COMPILE_LANGUAGE:C>:-std=c11>
|
||||||
|
$<$<COMPILE_LANGUAGE:CXX>:-std=c++17>
|
||||||
-Wall
|
-Wall
|
||||||
-Wextra
|
-Wextra
|
||||||
-Wpedantic
|
-Wpedantic
|
||||||
@@ -242,7 +218,7 @@ set(QT_VERSION_MAJOR 6)
|
|||||||
set(QT_MIN_VERSION 6.4.0)
|
set(QT_MIN_VERSION 6.4.0)
|
||||||
set(QT_DEFAULT_MAJOR_VERSION ${QT_VERSION_MAJOR})
|
set(QT_DEFAULT_MAJOR_VERSION ${QT_VERSION_MAJOR})
|
||||||
set(QT_COMPONENTS Core Concurrent Gui Widgets Network Sql)
|
set(QT_COMPONENTS Core Concurrent Gui Widgets Network Sql)
|
||||||
set(QT_OPTIONAL_COMPONENTS GuiPrivate LinguistTools Test)
|
set(QT_OPTIONAL_COMPONENTS GuiPrivate LinguistTools Test Protobuf)
|
||||||
if(UNIX AND NOT APPLE)
|
if(UNIX AND NOT APPLE)
|
||||||
list(APPEND QT_OPTIONAL_COMPONENTS DBus)
|
list(APPEND QT_OPTIONAL_COMPONENTS DBus)
|
||||||
endif()
|
endif()
|
||||||
@@ -277,6 +253,11 @@ endif()
|
|||||||
|
|
||||||
find_package(KDSingleApplication-qt${QT_VERSION_MAJOR} 1.1.0 REQUIRED)
|
find_package(KDSingleApplication-qt${QT_VERSION_MAJOR} 1.1.0 REQUIRED)
|
||||||
|
|
||||||
|
if(APPLE)
|
||||||
|
find_library(SPARKLE Sparkle)
|
||||||
|
#find_package(SPMediaKeyTap REQUIRED)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
find_package(getopt NAMES getopt getopt-win unofficial-getopt-win32 REQUIRED)
|
find_package(getopt NAMES getopt getopt-win unofficial-getopt-win32 REQUIRED)
|
||||||
if(TARGET getopt::getopt)
|
if(TARGET getopt::getopt)
|
||||||
@@ -293,10 +274,11 @@ if(WIN32)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(APPLE OR WIN32)
|
if(APPLE OR WIN32)
|
||||||
find_package(qtsparkle-qt${QT_VERSION_MAJOR} QUIET)
|
find_package(qtsparkle-qt${QT_VERSION_MAJOR})
|
||||||
if(TARGET "qtsparkle-qt${QT_VERSION_MAJOR}::qtsparkle")
|
if(TARGET "qtsparkle-qt${QT_VERSION_MAJOR}::qtsparkle")
|
||||||
set(QTSPARKLE_FOUND ON)
|
set(QTSPARKLE_FOUND ON)
|
||||||
endif()
|
endif()
|
||||||
|
pkg_check_modules(TINYSVCMDNS IMPORTED_TARGET tinysvcmdns)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(UNIX AND NOT APPLE)
|
if(UNIX AND NOT APPLE)
|
||||||
@@ -400,6 +382,18 @@ optional_component(DISCORD_RPC ON "Discord Rich Presence"
|
|||||||
DEPENDS "RapidJSON" RapidJSON_FOUND
|
DEPENDS "RapidJSON" RapidJSON_FOUND
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(WIN32)
|
||||||
|
optional_component(NETWORKREMOTE ON "Network remote"
|
||||||
|
DEPENDS "Qt Protobuf" Qt${QT_VERSION_MAJOR}Protobuf_FOUND
|
||||||
|
DEPENDS "tinysvcmdns" TINYSVCMDNS_FOUND
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
optional_component(NETWORKREMOTE ON "Network remote"
|
||||||
|
DEPENDS "Qt Protobuf" Qt${QT_VERSION_MAJOR}Protobuf_FOUND
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
if(HAVE_SONGFINGERPRINTING OR HAVE_MUSICBRAINZ)
|
if(HAVE_SONGFINGERPRINTING OR HAVE_MUSICBRAINZ)
|
||||||
set(HAVE_CHROMAPRINT ON)
|
set(HAVE_CHROMAPRINT ON)
|
||||||
endif()
|
endif()
|
||||||
@@ -778,6 +772,7 @@ set(SOURCES
|
|||||||
src/widgets/loginstatewidget.cpp
|
src/widgets/loginstatewidget.cpp
|
||||||
src/widgets/ratingwidget.cpp
|
src/widgets/ratingwidget.cpp
|
||||||
src/widgets/resizabletextedit.cpp
|
src/widgets/resizabletextedit.cpp
|
||||||
|
src/widgets/filechooserwidget.cpp
|
||||||
|
|
||||||
src/osd/osdbase.cpp
|
src/osd/osdbase.cpp
|
||||||
src/osd/osdpretty.cpp
|
src/osd/osdpretty.cpp
|
||||||
@@ -842,8 +837,6 @@ set(SOURCES
|
|||||||
|
|
||||||
src/fileview/fileview.cpp
|
src/fileview/fileview.cpp
|
||||||
src/fileview/fileviewlist.cpp
|
src/fileview/fileviewlist.cpp
|
||||||
src/fileview/fileviewtree.cpp
|
|
||||||
src/fileview/fileviewtreemodel.cpp
|
|
||||||
|
|
||||||
src/device/devicemanager.cpp
|
src/device/devicemanager.cpp
|
||||||
src/device/devicelister.cpp
|
src/device/devicelister.cpp
|
||||||
@@ -1078,6 +1071,7 @@ set(HEADERS
|
|||||||
src/widgets/ratingwidget.h
|
src/widgets/ratingwidget.h
|
||||||
src/widgets/forcescrollperpixel.h
|
src/widgets/forcescrollperpixel.h
|
||||||
src/widgets/resizabletextedit.h
|
src/widgets/resizabletextedit.h
|
||||||
|
src/widgets/filechooserwidget.h
|
||||||
|
|
||||||
src/osd/osdbase.h
|
src/osd/osdbase.h
|
||||||
src/osd/osdpretty.h
|
src/osd/osdpretty.h
|
||||||
@@ -1133,8 +1127,6 @@ set(HEADERS
|
|||||||
|
|
||||||
src/fileview/fileview.h
|
src/fileview/fileview.h
|
||||||
src/fileview/fileviewlist.h
|
src/fileview/fileviewlist.h
|
||||||
src/fileview/fileviewtree.h
|
|
||||||
src/fileview/fileviewtreemodel.h
|
|
||||||
|
|
||||||
src/device/devicemanager.h
|
src/device/devicemanager.h
|
||||||
src/device/devicelister.h
|
src/device/devicelister.h
|
||||||
@@ -1237,10 +1229,6 @@ set(UI
|
|||||||
src/device/deviceviewcontainer.ui
|
src/device/deviceviewcontainer.ui
|
||||||
)
|
)
|
||||||
|
|
||||||
if(UNIX)
|
|
||||||
optional_source(UNIX SOURCES src/core/unixsignalwatcher.cpp HEADERS src/core/unixsignalwatcher.h)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(APPLE)
|
if(APPLE)
|
||||||
optional_source(APPLE
|
optional_source(APPLE
|
||||||
SOURCES
|
SOURCES
|
||||||
@@ -1469,7 +1457,6 @@ optional_source(HAVE_SPOTIFY
|
|||||||
src/spotify/spotifybaserequest.cpp
|
src/spotify/spotifybaserequest.cpp
|
||||||
src/spotify/spotifyrequest.cpp
|
src/spotify/spotifyrequest.cpp
|
||||||
src/spotify/spotifyfavoriterequest.cpp
|
src/spotify/spotifyfavoriterequest.cpp
|
||||||
src/spotify/spotifymetadatarequest.cpp
|
|
||||||
src/settings/spotifysettingspage.cpp
|
src/settings/spotifysettingspage.cpp
|
||||||
src/covermanager/spotifycoverprovider.cpp
|
src/covermanager/spotifycoverprovider.cpp
|
||||||
HEADERS
|
HEADERS
|
||||||
@@ -1477,7 +1464,6 @@ optional_source(HAVE_SPOTIFY
|
|||||||
src/spotify/spotifybaserequest.h
|
src/spotify/spotifybaserequest.h
|
||||||
src/spotify/spotifyrequest.h
|
src/spotify/spotifyrequest.h
|
||||||
src/spotify/spotifyfavoriterequest.h
|
src/spotify/spotifyfavoriterequest.h
|
||||||
src/spotify/spotifymetadatarequest.h
|
|
||||||
src/settings/spotifysettingspage.h
|
src/settings/spotifysettingspage.h
|
||||||
src/covermanager/spotifycoverprovider.h
|
src/covermanager/spotifycoverprovider.h
|
||||||
UI
|
UI
|
||||||
@@ -1492,7 +1478,6 @@ optional_source(HAVE_QOBUZ
|
|||||||
src/qobuz/qobuzrequest.cpp
|
src/qobuz/qobuzrequest.cpp
|
||||||
src/qobuz/qobuzstreamurlrequest.cpp
|
src/qobuz/qobuzstreamurlrequest.cpp
|
||||||
src/qobuz/qobuzfavoriterequest.cpp
|
src/qobuz/qobuzfavoriterequest.cpp
|
||||||
src/qobuz/qobuzmetadatarequest.cpp
|
|
||||||
src/qobuz/qobuzcredentialfetcher.cpp
|
src/qobuz/qobuzcredentialfetcher.cpp
|
||||||
src/settings/qobuzsettingspage.cpp
|
src/settings/qobuzsettingspage.cpp
|
||||||
src/covermanager/qobuzcoverprovider.cpp
|
src/covermanager/qobuzcoverprovider.cpp
|
||||||
@@ -1503,7 +1488,6 @@ optional_source(HAVE_QOBUZ
|
|||||||
src/qobuz/qobuzrequest.h
|
src/qobuz/qobuzrequest.h
|
||||||
src/qobuz/qobuzstreamurlrequest.h
|
src/qobuz/qobuzstreamurlrequest.h
|
||||||
src/qobuz/qobuzfavoriterequest.h
|
src/qobuz/qobuzfavoriterequest.h
|
||||||
src/qobuz/qobuzmetadatarequest.h
|
|
||||||
src/qobuz/qobuzcredentialfetcher.h
|
src/qobuz/qobuzcredentialfetcher.h
|
||||||
src/settings/qobuzsettingspage.h
|
src/settings/qobuzsettingspage.h
|
||||||
src/covermanager/qobuzcoverprovider.h
|
src/covermanager/qobuzcoverprovider.h
|
||||||
@@ -1511,27 +1495,63 @@ optional_source(HAVE_QOBUZ
|
|||||||
src/settings/qobuzsettingspage.ui
|
src/settings/qobuzsettingspage.ui
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(HAVE_NETWORKREMOTE)
|
||||||
|
optional_source(HAVE_NETWORKREMOTE
|
||||||
|
SOURCES
|
||||||
|
src/core/zeroconf.cpp
|
||||||
|
src/networkremote/incomingdataparser.cpp
|
||||||
|
src/networkremote/networkremote.cpp
|
||||||
|
src/networkremote/outgoingdatacreator.cpp
|
||||||
|
src/networkremote/networkremoteclient.cpp
|
||||||
|
src/networkremote/songsender.cpp
|
||||||
|
src/settings/networkremotesettingspage.cpp
|
||||||
|
HEADERS
|
||||||
|
src/networkremote/networkremote.h
|
||||||
|
src/networkremote/incomingdataparser.h
|
||||||
|
src/networkremote/outgoingdatacreator.h
|
||||||
|
src/networkremote/networkremoteclient.h
|
||||||
|
src/networkremote/songsender.h
|
||||||
|
src/settings/networkremotesettingspage.h
|
||||||
|
UI
|
||||||
|
src/settings/networkremotesettingspage.ui
|
||||||
|
)
|
||||||
|
if(UNIX AND NOT APPLE)
|
||||||
|
get_target_property(QT_DBUSXML2CPP_EXECUTABLE Qt${QT_VERSION_MAJOR}::qdbusxml2cpp LOCATION)
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/avahi/avahiserver.cpp
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/avahi/avahiserver.h
|
||||||
|
COMMAND ${QT_DBUSXML2CPP_EXECUTABLE}
|
||||||
|
${CMAKE_SOURCE_DIR}/src/avahi/org.freedesktop.Avahi.Server.xml
|
||||||
|
-p ${CMAKE_CURRENT_BINARY_DIR}/avahi/avahiserver
|
||||||
|
-i includes/dbus_metatypes.h
|
||||||
|
DEPENDS src/avahi/org.freedesktop.Avahi.Server.xml
|
||||||
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
|
)
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/avahi/avahientrygroup.cpp
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/avahi/avahientrygroup.h
|
||||||
|
COMMAND ${QT_DBUSXML2CPP_EXECUTABLE}
|
||||||
|
${CMAKE_SOURCE_DIR}/src/avahi/org.freedesktop.Avahi.EntryGroup.xml
|
||||||
|
-p ${CMAKE_CURRENT_BINARY_DIR}/avahi/avahientrygroup
|
||||||
|
-i includes/dbus_metatypes.h
|
||||||
|
DEPENDS src/avahi/org.freedesktop.Avahi.EntryGroup.xml
|
||||||
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
|
)
|
||||||
|
list(APPEND SOURCES src/avahi/avahi.cpp ${CMAKE_CURRENT_BINARY_DIR}/avahi/avahientrygroup.cpp ${CMAKE_CURRENT_BINARY_DIR}/avahi/avahiserver.cpp)
|
||||||
|
list(APPEND HEADERS src/avahi/avahi.h ${CMAKE_CURRENT_BINARY_DIR}/avahi/avahientrygroup.h ${CMAKE_CURRENT_BINARY_DIR}/avahi/avahiserver.h)
|
||||||
|
endif()
|
||||||
|
optional_source(APPLE SOURCES src/core/bonjour.mm HEADERS src/core/bonjour.h)
|
||||||
|
optional_source(WIN32 SOURCES src/core/tinysvcmdns.cpp HEADERS src/core/tinysvcmdns.h)
|
||||||
|
endif()
|
||||||
|
|
||||||
qt_wrap_cpp(SOURCES ${HEADERS})
|
qt_wrap_cpp(SOURCES ${HEADERS})
|
||||||
qt_wrap_ui(SOURCES ${UI})
|
qt_wrap_ui(SOURCES ${UI})
|
||||||
qt_add_resources(SOURCES data/data.qrc data/icons.qrc)
|
qt_add_resources(SOURCES data/data.qrc data/icons.qrc)
|
||||||
|
|
||||||
add_library(strawberry_lib STATIC ${SOURCES})
|
add_library(strawberry_lib STATIC ${SOURCES})
|
||||||
|
|
||||||
# Treat Boost headers as system headers to avoid noisy warnings from 3rdparty
|
|
||||||
# Boost code (e.g. -Wold-style-cast) when building Strawberry with strict flags.
|
|
||||||
set(_strawberry_boost_system_includes "")
|
|
||||||
if(TARGET Boost::headers)
|
|
||||||
get_target_property(_strawberry_boost_system_includes Boost::headers INTERFACE_INCLUDE_DIRECTORIES)
|
|
||||||
elseif(TARGET Boost::boost)
|
|
||||||
get_target_property(_strawberry_boost_system_includes Boost::boost INTERFACE_INCLUDE_DIRECTORIES)
|
|
||||||
elseif(DEFINED Boost_INCLUDE_DIRS)
|
|
||||||
set(_strawberry_boost_system_includes "${Boost_INCLUDE_DIRS}")
|
|
||||||
endif()
|
|
||||||
if(_strawberry_boost_system_includes)
|
|
||||||
target_include_directories(strawberry_lib SYSTEM PRIVATE ${_strawberry_boost_system_includes})
|
|
||||||
endif()
|
|
||||||
unset(_strawberry_boost_system_includes)
|
|
||||||
|
|
||||||
target_sources(strawberry PRIVATE src/main.cpp)
|
target_sources(strawberry PRIVATE src/main.cpp)
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
@@ -1555,27 +1575,21 @@ if(HAVE_DISCORD_RPC)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(HAVE_TRANSLATIONS)
|
if(HAVE_TRANSLATIONS)
|
||||||
option(TRANSLATIONS_VERBOSE "Show verbose output while generating .qm translation files" OFF)
|
|
||||||
# On non-Windows platforms Qt doesn't need a PATH-setup wrapper for tools, but we can
|
|
||||||
# provide a wrapper to filter non-actionable lrelease noise during normal builds.
|
|
||||||
if(NOT CMAKE_HOST_WIN32)
|
|
||||||
set(QT_TOOL_COMMAND_WRAPPER_PATH "${CMAKE_SOURCE_DIR}/cmake/qt_tool_wrapper.sh"
|
|
||||||
CACHE INTERNAL "Wrapper used when invoking Qt tools from CMake" FORCE
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
qt_add_lupdate(strawberry_lib TS_FILES "${CMAKE_SOURCE_DIR}/src/translations/strawberry_en_US.ts" OPTIONS -locations none -no-ui-lines -no-obsolete)
|
qt_add_lupdate(strawberry_lib TS_FILES "${CMAKE_SOURCE_DIR}/src/translations/strawberry_en_US.ts" OPTIONS -locations none -no-ui-lines -no-obsolete)
|
||||||
file(GLOB_RECURSE ts_files ${CMAKE_SOURCE_DIR}/src/translations/*.ts)
|
file(GLOB_RECURSE ts_files ${CMAKE_SOURCE_DIR}/src/translations/*.ts)
|
||||||
set_source_files_properties(${ts_files} PROPERTIES OUTPUT_LOCATION "${CMAKE_CURRENT_BINARY_DIR}/data")
|
set_source_files_properties(${ts_files} PROPERTIES OUTPUT_LOCATION "${CMAKE_CURRENT_BINARY_DIR}/data")
|
||||||
if(TRANSLATIONS_VERBOSE)
|
|
||||||
qt_add_lrelease(strawberry TS_FILES ${ts_files} QM_FILES_OUTPUT_VARIABLE INSTALL_TRANSLATIONS_FILES)
|
qt_add_lrelease(strawberry TS_FILES ${ts_files} QM_FILES_OUTPUT_VARIABLE INSTALL_TRANSLATIONS_FILES)
|
||||||
else()
|
|
||||||
qt_add_lrelease(strawberry TS_FILES ${ts_files} QM_FILES_OUTPUT_VARIABLE INSTALL_TRANSLATIONS_FILES OPTIONS -silent)
|
|
||||||
endif()
|
|
||||||
if(NOT INSTALL_TRANSLATIONS)
|
if(NOT INSTALL_TRANSLATIONS)
|
||||||
qt_add_resources(strawberry "translations" PREFIX "/i18n" BASE "${CMAKE_CURRENT_BINARY_DIR}/data" FILES "${INSTALL_TRANSLATIONS_FILES}")
|
qt_add_resources(strawberry "translations" PREFIX "/i18n" BASE "${CMAKE_CURRENT_BINARY_DIR}/data" FILES "${INSTALL_TRANSLATIONS_FILES}")
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(HAVE_NETWORKREMOTE)
|
||||||
|
qt_add_protobuf(NetworkRemoteMessages
|
||||||
|
PROTO_FILES src/networkremote/networkremotemessages.proto
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
target_include_directories(strawberry_lib PUBLIC
|
target_include_directories(strawberry_lib PUBLIC
|
||||||
${CMAKE_SOURCE_DIR}
|
${CMAKE_SOURCE_DIR}
|
||||||
${CMAKE_BINARY_DIR}
|
${CMAKE_BINARY_DIR}
|
||||||
@@ -1608,6 +1622,7 @@ target_link_libraries(strawberry_lib PUBLIC
|
|||||||
Qt${QT_VERSION_MAJOR}::Sql
|
Qt${QT_VERSION_MAJOR}::Sql
|
||||||
$<$<BOOL:${HAVE_DBUS}>:Qt${QT_VERSION_MAJOR}::DBus>
|
$<$<BOOL:${HAVE_DBUS}>:Qt${QT_VERSION_MAJOR}::DBus>
|
||||||
$<$<BOOL:${HAVE_QPA_QPLATFORMNATIVEINTERFACE}>:Qt${QT_VERSION_MAJOR}::GuiPrivate>
|
$<$<BOOL:${HAVE_QPA_QPLATFORMNATIVEINTERFACE}>:Qt${QT_VERSION_MAJOR}::GuiPrivate>
|
||||||
|
$<$<BOOL:${HAVE_NETWORKREMOTE}>:Qt${QT_VERSION_MAJOR}::Protobuf>
|
||||||
ICU::uc
|
ICU::uc
|
||||||
ICU::i18n
|
ICU::i18n
|
||||||
$<$<BOOL:${HAVE_STREAMTAGREADER}>:PkgConfig::LIBSPARSEHASH>
|
$<$<BOOL:${HAVE_STREAMTAGREADER}>:PkgConfig::LIBSPARSEHASH>
|
||||||
@@ -1627,6 +1642,7 @@ target_link_libraries(strawberry_lib PUBLIC
|
|||||||
$<$<BOOL:${MSVC}>:WindowsApp>
|
$<$<BOOL:${MSVC}>:WindowsApp>
|
||||||
KDAB::kdsingleapplication
|
KDAB::kdsingleapplication
|
||||||
$<$<BOOL:${HAVE_DISCORD_RPC}>:discord-rpc>
|
$<$<BOOL:${HAVE_DISCORD_RPC}>:discord-rpc>
|
||||||
|
$<$<BOOL:${HAVE_NETWORKREMOTE}>:NetworkRemoteMessages>
|
||||||
)
|
)
|
||||||
|
|
||||||
if(APPLE)
|
if(APPLE)
|
||||||
@@ -1645,6 +1661,10 @@ if(APPLE)
|
|||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(WIN32 AND HAVE_NETWORKREMOTE)
|
||||||
|
target_link_libraries(strawberry_lib PUBLIC PkgConfig::TINYSVCMDNS)
|
||||||
|
endif()
|
||||||
|
|
||||||
target_link_libraries(strawberry PUBLIC strawberry_lib)
|
target_link_libraries(strawberry PUBLIC strawberry_lib)
|
||||||
|
|
||||||
if(NOT APPLE)
|
if(NOT APPLE)
|
||||||
|
|||||||
36
Changelog
36
Changelog
@@ -2,40 +2,6 @@ Strawberry Music Player
|
|||||||
=======================
|
=======================
|
||||||
ChangeLog
|
ChangeLog
|
||||||
|
|
||||||
Version 1.2.17 (2026.01.18):
|
|
||||||
|
|
||||||
* Avoid re-scan of restored songs unless mtime is changed (#1819)
|
|
||||||
* Skip existing files when organizing if not overwriting (#1484)
|
|
||||||
* Fixed cursor highlight disappearing off-screen when using down cursor (#1489)
|
|
||||||
* Fixed CD playback only working for the first optical drive (#1852)
|
|
||||||
* Fixed possible race-condition when switching tracks (#1863)
|
|
||||||
* Fixed possible file descriptor exhaustion by using shared thread pool (#1687)
|
|
||||||
* Don't automatically sort playlist with the auto sort option before it's fully loaded (#1690)
|
|
||||||
* Fixed network features stop working after computer suspends and resumes (#1521)
|
|
||||||
* Fixed crash on exit after Qobuz login
|
|
||||||
* Added tag editor option to select ID3v2 version (#1861)
|
|
||||||
* Fixed Qobuz authentication and added automatic credential fetching (#1898)
|
|
||||||
* Fixed playback stopping after deleting a song from disk via context menu (#1783)
|
|
||||||
* Added option to restore smart playlists to the defaults (#1848)
|
|
||||||
* Fixed possible race condition in pipeline destructor (#1875)
|
|
||||||
* Fixed buffering issue near track end during gapless playback (#1725)
|
|
||||||
* Fixed duplicate collection entries for the same artist if they have different sort tags (#1899)
|
|
||||||
* Defer playcount and rating tag writes for currently playing Ogg songs to prevent playback shutter (#1816)
|
|
||||||
* Fixed tag editing not working for Opus sort tags (#1929)
|
|
||||||
* Show playlist load errors (#1470)
|
|
||||||
* Fallback to delete if moving to trash fails (#1679)
|
|
||||||
* Prefer filenames with "front" or "cover" in the filename for album cover art for songs outside of the collection (#1745)
|
|
||||||
* Fixed collection enter/return behavior to respect double-click settings (#1691)
|
|
||||||
* Added tree view mode to files tab (#1922)
|
|
||||||
* Include .webp in allowed extensions for album covers (#1941)
|
|
||||||
* Exit gracefully on SIGTERM signal for Unix systems (#1942)
|
|
||||||
* Optimize the collection scanning process by deferring media file validation from the initial directory scan (#1954)
|
|
||||||
* Fixed collection scan not finding new directories in the top level collection directory when the mountpoint is restored (#1914)
|
|
||||||
* Added genre metadata parsing for Tidal, Qobuz and Spotify (#1913)
|
|
||||||
* Allow editing metadata for stream songs (#1913)
|
|
||||||
* Optimized collection/playlist filtering
|
|
||||||
* Added sort tags to collection/playlist filtering (#1966)
|
|
||||||
|
|
||||||
Version 1.2.16 (2025.12.16):
|
Version 1.2.16 (2025.12.16):
|
||||||
|
|
||||||
* Make Discord Rich presence use filename if song title is missing
|
* Make Discord Rich presence use filename if song title is missing
|
||||||
@@ -343,7 +309,7 @@ Version 1.1.0 (2024.07.14):
|
|||||||
* Only use playbin3 with GStreamer 1.24 and higher, not with GStreamer 1.22 or lower.
|
* Only use playbin3 with GStreamer 1.24 and higher, not with GStreamer 1.22 or lower.
|
||||||
* (macOS/Windows) Fixed dash and hls streaming, plugins were missing.
|
* (macOS/Windows) Fixed dash and hls streaming, plugins were missing.
|
||||||
* (Windows) Fixed incorrect colors in smart playlist wizard with Fusion in dark mode (#1399).
|
* (Windows) Fixed incorrect colors in smart playlist wizard with Fusion in dark mode (#1399).
|
||||||
* (Windows) Fixed update window blocking startup window on launch.
|
* (Windows) Fixed update window blocking sponsor window on startup.
|
||||||
|
|
||||||
Enhancements:
|
Enhancements:
|
||||||
* Improve error messages when connecting and copying to devices.
|
* Improve error messages when connecting and copying to devices.
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
class KdsingleapplicationQt6 < Formula
|
|
||||||
desc "Helper class for single-instance Qt applications (Qt 6 build)"
|
|
||||||
homepage "https://github.com/KDAB/KDSingleApplication"
|
|
||||||
url "https://github.com/KDAB/KDSingleApplication/archive/refs/tags/v1.1.0.tar.gz"
|
|
||||||
sha256 "1f19124c0aa5c6fffee3da174f7d2e091fab6dca1e123da70bb0fe615bfbe3e8"
|
|
||||||
license "MIT"
|
|
||||||
|
|
||||||
depends_on "cmake" => :build
|
|
||||||
depends_on "ninja" => :build
|
|
||||||
depends_on "qt"
|
|
||||||
|
|
||||||
def install
|
|
||||||
args = std_cmake_args + %W[
|
|
||||||
-GNinja
|
|
||||||
-DKDSingleApplication_QT6=ON
|
|
||||||
-DKDSingleApplication_TESTS=OFF
|
|
||||||
-DKDSingleApplication_EXAMPLES=OFF
|
|
||||||
-DKDSingleApplication_DOCS=OFF
|
|
||||||
-DKDSingleApplication_DEVELOPER_MODE=OFF
|
|
||||||
-DKDSingleApplication_STATIC=OFF
|
|
||||||
]
|
|
||||||
|
|
||||||
system "cmake", "-S", ".", "-B", "build", *args
|
|
||||||
system "cmake", "--build", "build"
|
|
||||||
system "cmake", "--install", "build"
|
|
||||||
end
|
|
||||||
|
|
||||||
test do
|
|
||||||
# Verify CMake package is usable via find_package(KDSingleApplication-qt6 CONFIG REQUIRED)
|
|
||||||
(testpath/"CMakeLists.txt").write <<~CMAKE
|
|
||||||
cmake_minimum_required(VERSION 3.16)
|
|
||||||
project(kdsa_test LANGUAGES CXX)
|
|
||||||
find_package(KDSingleApplication-qt6 CONFIG REQUIRED)
|
|
||||||
add_executable(test_kdsa main.cpp)
|
|
||||||
target_link_libraries(test_kdsa PRIVATE KDAB::kdsingleapplication)
|
|
||||||
CMAKE
|
|
||||||
|
|
||||||
(testpath/"main.cpp").write <<~CPP
|
|
||||||
#include <QCoreApplication>
|
|
||||||
int main(int argc, char** argv) {
|
|
||||||
QCoreApplication app(argc, argv);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
CPP
|
|
||||||
|
|
||||||
system "cmake", "-S", ".", "-B", "build",
|
|
||||||
"-DCMAKE_PREFIX_PATH=#{opt_prefix}"
|
|
||||||
system "cmake", "--build", "build"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# kdsingleapplication-qt6 (local Homebrew formula)
|
|
||||||
|
|
||||||
This directory exists to keep any supporting files for the local Homebrew formula
|
|
||||||
next to it (e.g. patches or notes).
|
|
||||||
|
|
||||||
## Install (from this Strawberry repo)
|
|
||||||
|
|
||||||
From the repo root:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
brew tap strawberry/local "file://$PWD"
|
|
||||||
brew install strawberry/local/kdsingleapplication-qt6
|
|
||||||
```
|
|
||||||
|
|
||||||
## Why it exists
|
|
||||||
|
|
||||||
Strawberry’s build requires the CMake package `KDSingleApplication-qt6`, but it is
|
|
||||||
not consistently available via Homebrew core. Shipping a local formula makes the
|
|
||||||
dependency easy to install for anyone building this repo on macOS.
|
|
||||||
|
|
||||||
## Note for local development
|
|
||||||
|
|
||||||
Homebrew taps are Git clones, so the formula must be committed (or pushed to a remote)
|
|
||||||
to be visible to `brew tap`.
|
|
||||||
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
class Libgpod < Formula
|
|
||||||
desc "Library to access the contents of classic iPods"
|
|
||||||
homepage "https://gtkpod.org/libgpod/"
|
|
||||||
url "https://github.com/neuschaefer/libgpod/archive/0dda196286f5e42be89f0b870abd9278213989a5.tar.gz"
|
|
||||||
sha256 "a9809f85b2b763196ac7c94903211a927efd37a24ef39c355c21b4a1bed28e52"
|
|
||||||
license "LGPL-2.0-only"
|
|
||||||
|
|
||||||
depends_on "autoconf" => :build
|
|
||||||
depends_on "automake" => :build
|
|
||||||
depends_on "libtool" => :build
|
|
||||||
depends_on "pkg-config" => :build
|
|
||||||
depends_on "gtk-doc" => :build
|
|
||||||
depends_on "intltool" => :build
|
|
||||||
|
|
||||||
depends_on "glib"
|
|
||||||
depends_on "gdk-pixbuf"
|
|
||||||
depends_on "libplist"
|
|
||||||
depends_on "libxml2"
|
|
||||||
depends_on "sqlite"
|
|
||||||
|
|
||||||
def install
|
|
||||||
# libgpod's configure.ac checks for pkg-config module name "libplist".
|
|
||||||
# Homebrew provides "libplist-2.0", so we provide a tiny shim .pc file to
|
|
||||||
# satisfy the expected name.
|
|
||||||
(buildpath/"brew-pkgconfig").mkpath
|
|
||||||
(buildpath/"brew-pkgconfig/libplist.pc").write <<~EOS
|
|
||||||
prefix=#{Formula["libplist"].opt_prefix}
|
|
||||||
exec_prefix=${prefix}
|
|
||||||
libdir=#{Formula["libplist"].opt_lib}
|
|
||||||
includedir=#{Formula["libplist"].opt_include}
|
|
||||||
|
|
||||||
Name: libplist
|
|
||||||
Description: Apple property list library (Homebrew shim for libgpod)
|
|
||||||
Version: #{Formula["libplist"].version}
|
|
||||||
Libs: -L${libdir} -lplist-2.0
|
|
||||||
Cflags: -I${includedir}
|
|
||||||
EOS
|
|
||||||
|
|
||||||
ENV.prepend_path "PKG_CONFIG_PATH", buildpath/"brew-pkgconfig"
|
|
||||||
|
|
||||||
# Ensure pkg-config can find Homebrew keg .pc files during configure.
|
|
||||||
ENV.prepend_path "PKG_CONFIG_PATH", Formula["libplist"].opt_lib/"pkgconfig"
|
|
||||||
ENV.prepend_path "PKG_CONFIG_PATH", Formula["sqlite"].opt_lib/"pkgconfig"
|
|
||||||
ENV.prepend_path "PKG_CONFIG_PATH", Formula["glib"].opt_lib/"pkgconfig"
|
|
||||||
ENV.prepend_path "PKG_CONFIG_PATH", Formula["glib"].opt_share/"pkgconfig"
|
|
||||||
ENV.prepend_path "PKG_CONFIG_PATH", Formula["gdk-pixbuf"].opt_lib/"pkgconfig"
|
|
||||||
|
|
||||||
# Upstream's autogen.sh is very old and may hardcode ancient automake checks
|
|
||||||
# (e.g. looking for automake-1.7). Using autoreconf is the standard Homebrew
|
|
||||||
# way and works with modern autotools.
|
|
||||||
#
|
|
||||||
# libgpod's build system expects gtk-doc's makefile snippet to exist (gtk-doc.make),
|
|
||||||
# which is normally provided by running gtkdocize.
|
|
||||||
system "gtkdocize", "--copy"
|
|
||||||
|
|
||||||
# libgpod also uses intltool's Autoconf macros (IT_PROG_INTLTOOL). If intltoolize
|
|
||||||
# is not run, the generated ./configure may contain unexpanded macros and fail.
|
|
||||||
system "intltoolize", "--force", "--copy", "--automake"
|
|
||||||
system "autoreconf", "-fiv"
|
|
||||||
|
|
||||||
system "./configure", *std_configure_args,
|
|
||||||
"--disable-dependency-tracking",
|
|
||||||
"--with-hal=no",
|
|
||||||
"--disable-udev",
|
|
||||||
"--without-libimobiledevice",
|
|
||||||
"--with-python=no",
|
|
||||||
"--with-mono=no",
|
|
||||||
"--disable-gtk-doc",
|
|
||||||
"--disable-gtk-doc-html",
|
|
||||||
"--disable-gtk-doc-pdf",
|
|
||||||
"--enable-more-warnings=no"
|
|
||||||
|
|
||||||
system "make", "install"
|
|
||||||
end
|
|
||||||
|
|
||||||
test do
|
|
||||||
# Ensure pkg-config can find the expected module name used by Strawberry.
|
|
||||||
assert_match "libgpod", shell_output("pkg-config --libs libgpod-1.0")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# libgpod (local Homebrew formula)
|
|
||||||
|
|
||||||
Homebrew core does not currently ship `libgpod`, but Strawberry can optionally use it
|
|
||||||
to support **classic iPod** devices (via `libgpod-1.0` + `gdk-pixbuf-2.0`).
|
|
||||||
|
|
||||||
This formula is pinned to a known-good upstream snapshot and disables Linux-specific
|
|
||||||
integration (udev/HAL) and language bindings to keep the build reliable on macOS.
|
|
||||||
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
class Macdeploycheck < Formula
|
|
||||||
desc "Sanity checks a macOS .app bundle for accidental Homebrew runtime dependencies"
|
|
||||||
homepage "https://github.com/strawberrymusicplayer/strawberry"
|
|
||||||
version "0.1.0"
|
|
||||||
# Homebrew requires a URL stanza. Use the script shipped in this tap (file://),
|
|
||||||
# so installs always match the tapped revision.
|
|
||||||
url "file://#{File.expand_path("../dist/macos/macdeploycheck.sh", __dir__)}"
|
|
||||||
sha256 "07d361dcecf98af44fa076cc4253af907e23dee273c198a60128dae41b98432d"
|
|
||||||
license "MIT"
|
|
||||||
|
|
||||||
depends_on :macos
|
|
||||||
|
|
||||||
def install
|
|
||||||
bin.install "macdeploycheck.sh" => "macdeploycheck"
|
|
||||||
end
|
|
||||||
|
|
||||||
test do
|
|
||||||
# Basic smoke test: tool runs and prints usage.
|
|
||||||
system bin/"macdeploycheck"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# `macdeploycheck` (local Homebrew formula)
|
|
||||||
|
|
||||||
This repository includes a small helper tool called `macdeploycheck`, packaged as a local Homebrew formula.
|
|
||||||
|
|
||||||
## What it does
|
|
||||||
|
|
||||||
`macdeploycheck` scans a built `.app` bundle and flags common **accidental runtime dependencies** on:
|
|
||||||
|
|
||||||
- Homebrew paths like `/opt/homebrew/...` or `/usr/local/...`
|
|
||||||
- MacPorts paths like `/opt/local/...`
|
|
||||||
|
|
||||||
These dependencies usually mean the `.app` is **not self-contained** and may fail to run on other machines or fail notarization validation.
|
|
||||||
|
|
||||||
## Install (via this repo's tap)
|
|
||||||
|
|
||||||
From the Strawberry repo root:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
brew tap strawberry/local "file://$PWD"
|
|
||||||
brew install strawberry/local/macdeploycheck
|
|
||||||
```
|
|
||||||
|
|
||||||
Or use the repo `Brewfile`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
brew bundle --file Brewfile
|
|
||||||
```
|
|
||||||
|
|
||||||
## Use
|
|
||||||
|
|
||||||
```bash
|
|
||||||
macdeploycheck /path/to/Strawberry.app
|
|
||||||
```
|
|
||||||
|
|
||||||
It exits non-zero if it finds external runtime deps.
|
|
||||||
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
class QtsparkleQt6 < Formula
|
|
||||||
desc "Qt wrapper library for in-app updates (Qt 6 build)"
|
|
||||||
homepage "https://github.com/strawberrymusicplayer/qtsparkle"
|
|
||||||
url "https://github.com/strawberrymusicplayer/qtsparkle/archive/95ca3b77a79540d632b29e9a4df9aed30af5f901.tar.gz"
|
|
||||||
sha256 "945c9e96d2f6175b134a8ccfd6ec1acd268266d31969b5870d4037e8e5877834"
|
|
||||||
license "GPL-3.0-or-later"
|
|
||||||
|
|
||||||
depends_on "cmake" => :build
|
|
||||||
depends_on "ninja" => :build
|
|
||||||
depends_on "qt"
|
|
||||||
|
|
||||||
def install
|
|
||||||
args = std_cmake_args + %W[
|
|
||||||
-GNinja
|
|
||||||
-DBUILD_WITH_QT6=ON
|
|
||||||
-DBUILD_WITH_QT5=OFF
|
|
||||||
-DBUILD_SHARED_LIBS=ON
|
|
||||||
-DBUILD_STATIC_LIBS=OFF
|
|
||||||
]
|
|
||||||
|
|
||||||
system "cmake", "-S", ".", "-B", "build", *args
|
|
||||||
system "cmake", "--build", "build"
|
|
||||||
system "cmake", "--install", "build"
|
|
||||||
end
|
|
||||||
|
|
||||||
test do
|
|
||||||
# Strawberry expects: find_package(qtsparkle-qt6) and target qtsparkle-qt6::qtsparkle
|
|
||||||
(testpath/"CMakeLists.txt").write <<~CMAKE
|
|
||||||
cmake_minimum_required(VERSION 3.16)
|
|
||||||
project(qtsparkle_test LANGUAGES CXX)
|
|
||||||
find_package(qtsparkle-qt6 CONFIG REQUIRED)
|
|
||||||
add_library(dummy STATIC dummy.cpp)
|
|
||||||
target_link_libraries(dummy PRIVATE qtsparkle-qt6::qtsparkle)
|
|
||||||
CMAKE
|
|
||||||
|
|
||||||
(testpath/"dummy.cpp").write <<~CPP
|
|
||||||
int dummy() { return 0; }
|
|
||||||
CPP
|
|
||||||
|
|
||||||
system "cmake", "-S", ".", "-B", "build",
|
|
||||||
"-DCMAKE_PREFIX_PATH=#{opt_prefix}"
|
|
||||||
system "cmake", "--build", "build"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# qtsparkle-qt6 (local Homebrew formula)
|
|
||||||
|
|
||||||
This installs Strawberry’s Qt updater helper library as a CMake package:
|
|
||||||
|
|
||||||
- `find_package(qtsparkle-qt6 CONFIG REQUIRED)`
|
|
||||||
- target: `qtsparkle-qt6::qtsparkle`
|
|
||||||
|
|
||||||
Strawberry will pick it up automatically when present and enable the optional
|
|
||||||
**QtSparkle integration**.
|
|
||||||
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
class SparkleFramework < Formula
|
|
||||||
desc "Sparkle.framework for macOS app updates (framework-only packaging)"
|
|
||||||
homepage "https://sparkle-project.org/"
|
|
||||||
url "https://github.com/sparkle-project/Sparkle/releases/download/2.8.1/Sparkle-2.8.1.tar.xz"
|
|
||||||
sha256 "5cddb7695674ef7704268f38eccaee80e3accbf19e61c1689efff5b6116d85be"
|
|
||||||
license "MIT"
|
|
||||||
|
|
||||||
depends_on :macos
|
|
||||||
|
|
||||||
def install
|
|
||||||
frameworks = prefix/"Frameworks"
|
|
||||||
frameworks.install "Sparkle.framework"
|
|
||||||
end
|
|
||||||
|
|
||||||
test do
|
|
||||||
assert_predicate prefix/"Frameworks/Sparkle.framework", :exist?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# sparkle-framework (local Homebrew formula)
|
|
||||||
|
|
||||||
Installs the upstream `Sparkle.framework` into:
|
|
||||||
|
|
||||||
- `$(brew --prefix sparkle-framework)/Frameworks/Sparkle.framework`
|
|
||||||
|
|
||||||
This is used to enable Strawberry’s optional **Sparkle integration** on macOS
|
|
||||||
(`find_library(SPARKLE Sparkle)` in the main `CMakeLists.txt`).
|
|
||||||
|
|
||||||
86
README.md
86
README.md
@@ -1,55 +1,60 @@
|
|||||||
# Strawberry (macOS-focused fork)
|
# :strawberry: Strawberry Music Player [](https://github.com/strawberrymusicplayer/strawberry/actions)
|
||||||
|
[](https://github.com/sponsors/jonaski)
|
||||||
|
[](https://patreon.com/jonaskvinge)
|
||||||
|
[](https://paypal.me/jonaskvinge)
|
||||||
|
|
||||||
This repository is a **macOS-focused fork** of upstream Strawberry.
|
Strawberry is a **music player and music collection organizer**, originally forked from *Clementine* in 2018.
|
||||||
|
It’s written in **C++ using the Qt framework**, designed for **audiophiles and music collectors**.
|
||||||
|
|
||||||
The goal of this fork is to make Strawberry **build cleanly and repeatably on macOS**, with:
|

|
||||||
|
|
||||||
- Homebrew dependency installation via `Brewfile`
|
---
|
||||||
- local Homebrew formulas (tap) for missing dependencies
|
|
||||||
- build / deploy / signing / notarization helper scripts under `build_tools/`
|
|
||||||
- Sparkle feed configuration knobs so you can publish your own updates
|
|
||||||
|
|
||||||
## Upstream vs this fork (macOS distribution)
|
## :globe_with_meridians: Resources
|
||||||
|
|
||||||
Upstream Strawberry is where ongoing development happens:
|
- **Website:** https://www.strawberrymusicplayer.org
|
||||||
|
- **Wiki:** https://wiki.strawberrymusicplayer.org
|
||||||
|
- **Forum:** https://forum.strawberrymusicplayer.org
|
||||||
|
- **GitHub:** https://github.com/strawberrymusicplayer/strawberry
|
||||||
|
- **Latest builds:** https://builds.strawberrymusicplayer.org
|
||||||
|
- **openSUSE Build Service:**
|
||||||
|
- Stable: https://build.opensuse.org/package/show/home:jonaski:strawberry/strawberry
|
||||||
|
- Unstable: https://build.opensuse.org/package/show/home:jonaski:strawberry-dev/strawberry
|
||||||
|
- **Ubuntu PPAs:**
|
||||||
|
- Stable: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry
|
||||||
|
- Unstable: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry-unstable
|
||||||
|
- **Translations:** https://crowdin.com/project/strawberrymusicplayer
|
||||||
|
|
||||||
- Upstream: `https://github.com/strawberrymusicplayer/strawberry`
|
---
|
||||||
|
|
||||||
This fork’s source (the code you are building here):
|
## :warning: Opening an Issue
|
||||||
|
|
||||||
- Fork: `https://gitea.dryark.com/dryark/strawberry`
|
Before creating a new GitHub issue:
|
||||||
|
|
||||||
This fork is intended for people who want to:
|
1. **Read the [FAQ](https://wiki.strawberrymusicplayer.org/wiki/FAQ)**.
|
||||||
|
2. **Search existing issues** to avoid duplicates. If one already exists, comment there with any additional information.
|
||||||
|
3. **Use the [forum](https://forum.strawberrymusicplayer.org/)** for technical problems, discussions or feature suggestions — it’s better suited for back-and-forth conversation.
|
||||||
|
4. **Feature requests are not accepted on GitHub.** Issues created for feature requests will be closed. You can still discuss ideas on the forum.
|
||||||
|
5. **Flatpak users:** We do **not** maintain the Flatpak package. Report Flatpak-specific issues via [Flatpak support](https://flatpak.org/about/).
|
||||||
|
|
||||||
- **build from source on macOS** without guesswork
|
---
|
||||||
- **produce signed + notarized binaries** themselves (and optionally distribute them)
|
|
||||||
|
|
||||||
General safety note: whether you use upstream builds, your own builds, or someone else’s, only install software from sources you trust and prefer **signed + notarized** releases.
|
## :moneybag: Sponsoring
|
||||||
|
|
||||||
## Quick start (macOS)
|
Strawberry is **free software released under the GPL**.
|
||||||
|
If you enjoy using it, please consider **supporting development** through sponsorship or donation.
|
||||||
|
|
||||||
Install Homebrew dependencies:
|
**Sponsorship options:**
|
||||||
|
1. [Patreon](https://www.patreon.com/jonaskvinge)
|
||||||
|
2. [GitHub](https://github.com/sponsors/jonaski)
|
||||||
|
3. [Ko-fi](https://ko-fi.com/jonaskvinge)
|
||||||
|
4. [PayPal](https://paypal.me/jonaskvinge)
|
||||||
|
|
||||||
```bash
|
Supporting open-source developers helps ensure continued maintenance and improvements.
|
||||||
./build_tools/macos/install_brew_deps.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Build:
|
---
|
||||||
|
|
||||||
```bash
|
## :white_check_mark: Features
|
||||||
./build_tools/macos/build_app.sh --release --clean
|
|
||||||
open ./cmake-build-macos-release/strawberry.app
|
|
||||||
```
|
|
||||||
|
|
||||||
Build + deploy + sign + notarize (+ DMG):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/build_sign_notarize.sh --run --release --clean --deploy --dmg \
|
|
||||||
--identity "Developer ID Application: Your Name (TEAMID)" \
|
|
||||||
--notary-profile "<profile-name>"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Play and organize your music collection
|
- Play and organize your music collection
|
||||||
- Supports formats: WAV, FLAC, WavPack, Ogg Vorbis, Opus, MPC, TrueAudio, AIFF, MP4, MP3, ASF, and Monkey’s Audio
|
- Supports formats: WAV, FLAC, WavPack, Ogg Vorbis, Opus, MPC, TrueAudio, AIFF, MP4, MP3, ASF, and Monkey’s Audio
|
||||||
@@ -73,6 +78,11 @@ Build + deploy + sign + notarize (+ DMG):
|
|||||||
|
|
||||||
:white_check_mark: Tested on **Linux**, **OpenBSD**, **FreeBSD**, **macOS**, and **Windows**.
|
:white_check_mark: Tested on **Linux**, **OpenBSD**, **FreeBSD**, **macOS**, and **Windows**.
|
||||||
|
|
||||||
|
> **Note:** macOS and Windows releases are currently **available to sponsors only**.
|
||||||
|
> A monthly sponsorship via [Patreon](https://www.patreon.com/jonaskvinge) grants direct access to new releases.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## :gear: Requirements
|
## :gear: Requirements
|
||||||
|
|
||||||
To build Strawberry from source, you’ll need:
|
To build Strawberry from source, you’ll need:
|
||||||
@@ -107,9 +117,9 @@ Also install GStreamer plugins **base**, **good**, and optionally **bad**, **ugl
|
|||||||
|
|
||||||
## :wrench: Build from Source
|
## :wrench: Build from Source
|
||||||
|
|
||||||
**Get the code (this fork):**
|
**Get the code:**
|
||||||
|
|
||||||
git clone --recursive https://gitea.dryark.com/dryark/strawberry
|
git clone --recursive https://github.com/strawberrymusicplayer/strawberry
|
||||||
|
|
||||||
**Build and install:**
|
**Build and install:**
|
||||||
|
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
# Build helper scripts
|
|
||||||
|
|
||||||
This `build_tools/` directory contains **helper scripts and notes** for building Strawberry.
|
|
||||||
|
|
||||||
- It is **not** intended to be your CMake build output directory.
|
|
||||||
- Recommended CMake build output directories: `cmake-build/`, `build-release/`, etc.
|
|
||||||
|
|
||||||
## macOS
|
|
||||||
|
|
||||||
- Install dependencies via Homebrew:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/install_brew_deps.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
- Build Strawberry:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/build_app.sh --release
|
|
||||||
open ./cmake-build-macos-release/strawberry.app
|
|
||||||
```
|
|
||||||
|
|
||||||
## macOS signing + notarization (Developer ID distribution)
|
|
||||||
|
|
||||||
This repo includes `build_tools/macos/build_sign_notarize.sh` to automate:
|
|
||||||
|
|
||||||
- build → (optional deploy) → codesign → notarize → staple → verify
|
|
||||||
|
|
||||||
### One-time setup (Apple Developer)
|
|
||||||
|
|
||||||
- **Install certificates**:
|
|
||||||
- In the Apple Developer portal, create (or download) a **Developer ID Application** certificate.
|
|
||||||
- Install it into your login keychain (Xcode can manage this via **Xcode → Settings → Accounts**).
|
|
||||||
|
|
||||||
- **Provisioning profiles**:
|
|
||||||
- For **Developer ID distribution (outside the Mac App Store)**, you typically **do not need a provisioning profile**.
|
|
||||||
- You *do* need profiles if you are building a **Mac App Store**-signed app (not what this repo’s scripts target).
|
|
||||||
|
|
||||||
- **Notarization credentials**:
|
|
||||||
- Create a `notarytool` keychain profile (recommended) so you don’t have to pass secrets on the command line:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# NOTE: <profile-name> is a positional argument (not a flag).
|
|
||||||
# Pick any name you want, e.g. "strawberry-notary".
|
|
||||||
xcrun notarytool store-credentials "<profile-name>" \
|
|
||||||
--apple-id "<your-apple-id>" \
|
|
||||||
--team-id "<TEAMID>" \
|
|
||||||
--password "<app-specific-password>"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Listing what’s installed locally
|
|
||||||
|
|
||||||
Run with no args to list local signing identities + notarytool profiles:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/build_sign_notarize.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build + sign + notarize
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/build_sign_notarize.sh --run --release --clean --deploy \
|
|
||||||
--identity "Developer ID Application: Your Name (TEAMID)" \
|
|
||||||
--notary-profile "<profile-name>"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build + sign + notarize + DMG (recommended for public distribution)
|
|
||||||
|
|
||||||
This produces:
|
|
||||||
|
|
||||||
- a notarized `strawberry.app` (stapled)
|
|
||||||
- a notarized `strawberry-notarize.zip` (useful for Sparkle / downloads)
|
|
||||||
- a notarized `strawberry-*.dmg` (stapled)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/build_sign_notarize.sh --run --release --clean --deploy --dmg \
|
|
||||||
--identity "Developer ID Application: Your Name (TEAMID)" \
|
|
||||||
--notary-profile "<profile-name>"
|
|
||||||
```
|
|
||||||
|
|
||||||
## macOS Mac App Store (MAS) build + signed PKG
|
|
||||||
|
|
||||||
This repo includes `build_tools/macos/build_mas_pkg.sh` to automate:
|
|
||||||
|
|
||||||
- build (MAS mode) → deploy (bundle deps) → embed provisioning profile → codesign → `productbuild` a signed `.pkg`
|
|
||||||
|
|
||||||
### Requirements (Apple Developer)
|
|
||||||
|
|
||||||
- An App Store Connect app record with bundle id **`com.dryark.strawberry`** (or your own).
|
|
||||||
- A **Mac App Store provisioning profile** for that App ID.
|
|
||||||
- Signing identities installed in your Keychain:
|
|
||||||
- **Apple Distribution** (for the `.app`)
|
|
||||||
- **3rd Party Mac Developer Installer** (for the `.pkg`)
|
|
||||||
|
|
||||||
Tip: list what you have installed:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
security find-identity -p codesigning -v
|
|
||||||
security find-identity -p basic -v
|
|
||||||
ls -la "$HOME/Library/MobileDevice/Provisioning Profiles" | head -n 50
|
|
||||||
```
|
|
||||||
|
|
||||||
### Manual setup guide (certificates, Keychain Access, profiles)
|
|
||||||
|
|
||||||
See: `build_tools/macos/README_MAS.md`
|
|
||||||
|
|
||||||
### Build the signed upload PKG
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/build_mas_pkg.sh --run --release --clean \
|
|
||||||
--codesign-identity "Apple Distribution: Your Name (TEAMID)" \
|
|
||||||
--installer-identity "3rd Party Mac Developer Installer: Your Name (TEAMID)" \
|
|
||||||
--provisionprofile "$HOME/Library/MobileDevice/Provisioning Profiles/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX.provisionprofile"
|
|
||||||
```
|
|
||||||
|
|
||||||
Output:
|
|
||||||
|
|
||||||
- `cmake-build-macos-release-mas/strawberry.app`
|
|
||||||
- `cmake-build-macos-release-mas/strawberry-mas.pkg`
|
|
||||||
|
|
||||||
### Upload + submit for review
|
|
||||||
|
|
||||||
- Upload the `.pkg` using Apple’s **Transporter** app (App Store Connect), or with `iTMSTransporter`.
|
|
||||||
- In App Store Connect, wait for processing, select the build, then **Submit for Review**.
|
|
||||||
@@ -1,421 +0,0 @@
|
|||||||
# Mac App Store (MAS) submission guide (manual steps)
|
|
||||||
|
|
||||||
This repo supports a **Mac App Store build mode** (`BUILD_FOR_MAC_APP_STORE=ON`) and includes scripts to build a signed upload `.pkg`.
|
|
||||||
|
|
||||||
If you’re blocked because `security find-identity` only shows **Developer ID** and not **Apple Distribution / Installer**, follow the steps below.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Open Keychain Access (macOS “hidden” Utilities)
|
|
||||||
|
|
||||||
Any of these work:
|
|
||||||
|
|
||||||
- **Spotlight**: press `⌘ + Space` → type **Keychain Access** → Enter
|
|
||||||
- **Finder**: Applications → Utilities → **Keychain Access**
|
|
||||||
- **Terminal**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
open -a "Keychain Access"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The core issue: certificate exists but is not a usable identity
|
|
||||||
|
|
||||||
If you see certificates like:
|
|
||||||
|
|
||||||
- `Apple Distribution: ...`
|
|
||||||
- `3rd Party Mac Developer Installer: ...`
|
|
||||||
|
|
||||||
but `security find-identity` does **not** list them, then the certificate is present but **the private key is missing** (or not paired / in the wrong keychain).
|
|
||||||
|
|
||||||
You can confirm with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/check_signing_identities.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 1 — Create the private keys on this Mac (CSR)
|
|
||||||
|
|
||||||
1. Open **Keychain Access**
|
|
||||||
2. Menu: **Keychain Access → Certificate Assistant → Request a Certificate From a Certificate Authority…**
|
|
||||||
3. Fill:
|
|
||||||
- **User Email Address**: your Apple ID email
|
|
||||||
- **Common Name**: e.g. `Dry Ark LLC` (any label is fine)
|
|
||||||
- **CA Email Address**: leave blank
|
|
||||||
- Select: **Saved to disk**
|
|
||||||
4. Save the CSR (`.certSigningRequest`) somewhere safe
|
|
||||||
|
|
||||||
This CSR step is what creates the **private key** locally in your login keychain.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 2 — Create + download the certificates (Apple Developer portal)
|
|
||||||
|
|
||||||
In Apple Developer → **Certificates, Identifiers & Profiles** → **Certificates** → **+**:
|
|
||||||
|
|
||||||
- Create **Apple Distribution** (use the CSR you just made)
|
|
||||||
- Create **Mac Installer Distribution** (or “3rd Party Mac Developer Installer”, wording varies) (use a CSR)
|
|
||||||
|
|
||||||
Download the resulting `.cer` files.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 3 — Install certificates into your login keychain
|
|
||||||
|
|
||||||
Double-click each downloaded `.cer` to install it.
|
|
||||||
|
|
||||||
Then in **Keychain Access → login → My Certificates**:
|
|
||||||
|
|
||||||
- Find **Apple Distribution: ...** and **expand it**
|
|
||||||
- You must see a **private key** under it.
|
|
||||||
- Find **... Installer ...** and expand it
|
|
||||||
- You must see a **private key** under it.
|
|
||||||
|
|
||||||
If there’s no private key under the certificate, it will not be usable for signing on this Mac.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 4 — Verify identities from the CLI
|
|
||||||
|
|
||||||
### Common failure: errSecInternalComponent / chain-to-root warnings
|
|
||||||
|
|
||||||
If you see errors like:
|
|
||||||
|
|
||||||
- `Warning: unable to build chain to self-signed root for signer "Apple Distribution: ..."`
|
|
||||||
- `errSecInternalComponent`
|
|
||||||
|
|
||||||
This is almost always a **keychain search list / trust chain** issue.
|
|
||||||
|
|
||||||
#### Important: do NOT “Always Trust” your Apple Distribution / Installer certs
|
|
||||||
|
|
||||||
Setting your leaf signing certificates (e.g. **Apple Distribution** / **3rd Party Mac Developer Installer**) to **Always Trust** can make things worse by overriding the normal trust chain and causing codesign to fail chain building.
|
|
||||||
|
|
||||||
If you changed trust settings:
|
|
||||||
|
|
||||||
- In **Keychain Access → login → My Certificates**
|
|
||||||
- open the cert → **Trust**
|
|
||||||
- set **“When using this certificate” = “Use System Defaults”**
|
|
||||||
|
|
||||||
Fix (safe, common): ensure the System keychains are included in the user search list:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
security list-keychains -d user
|
|
||||||
security list-keychains -d user -s "$HOME/Library/Keychains/login.keychain-db" "/Library/Keychains/System.keychain" "/System/Library/Keychains/SystemRootCertificates.keychain"
|
|
||||||
```
|
|
||||||
|
|
||||||
Then re-run the build/sign script.
|
|
||||||
|
|
||||||
#### Install the correct Apple intermediate certificates (WWDR)
|
|
||||||
|
|
||||||
If the System keychains are already in the search list and you still get chain errors, you’re likely missing an Apple intermediate (commonly **WWDR**).
|
|
||||||
|
|
||||||
Download the current Apple WWDR intermediate certificate(s) from Apple’s official Certificate Authority page:
|
|
||||||
|
|
||||||
- `https://www.apple.com/certificateauthority/`
|
|
||||||
|
|
||||||
Then import into the **System** keychain (recommended):
|
|
||||||
|
|
||||||
- Keychain Access → **System** keychain → File → **Import Items…** → select the downloaded `.cer`
|
|
||||||
|
|
||||||
Or via CLI (requires admin):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo security add-certificates -k /Library/Keychains/System.keychain "/path/to/WWDR.cer"
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify it’s visible:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
security find-certificate -a -c "Apple Worldwide Developer Relations" /Library/Keychains/System.keychain | head -n 10
|
|
||||||
```
|
|
||||||
|
|
||||||
If needed, you can also verify the chain for your distribution cert:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
security verify-cert -c "Apple Distribution: Dry Ark LLC (7628766FL2)" 2>&1 | head -n 80
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
security find-identity -p codesigning -v
|
|
||||||
security find-identity -p basic -v
|
|
||||||
./build_tools/macos/check_signing_identities.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected:
|
|
||||||
|
|
||||||
- `Apple Distribution: ...` shows up under **codesigning**
|
|
||||||
- `... Installer ...` shows up as an **installer identity** (used to sign upload `.pkg`)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 5 — Create + install the provisioning profile (Mac App Store)
|
|
||||||
|
|
||||||
In Apple Developer → **Profiles** → **+**:
|
|
||||||
|
|
||||||
- Platform: **macOS**
|
|
||||||
- Type: **Mac App Store**
|
|
||||||
- App ID: `com.dryark.strawberry` (or your own bundle id)
|
|
||||||
- Select the **Apple Distribution** certificate
|
|
||||||
- Generate + Download
|
|
||||||
|
|
||||||
### Where the `.provisionprofile` ends up (newer Xcode/macOS)
|
|
||||||
|
|
||||||
Recent Xcode versions store “downloaded manual profiles” under:
|
|
||||||
|
|
||||||
- `~/Library/Developer/Xcode/UserData/Provisioning Profiles/`
|
|
||||||
|
|
||||||
Older tooling sometimes used:
|
|
||||||
|
|
||||||
- `~/Library/MobileDevice/Provisioning Profiles/`
|
|
||||||
|
|
||||||
This repo’s MAS build script does **not** require the profile to be in a specific folder — you can pass the path directly.
|
|
||||||
|
|
||||||
To locate and pick the right profile, use:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/find_mas_provisioning_profile.sh --bundle-id com.dryark.strawberry
|
|
||||||
```
|
|
||||||
|
|
||||||
### (Optional) Copy to the legacy folder
|
|
||||||
|
|
||||||
If some other tools expect the legacy folder, you can copy it there:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles"
|
|
||||||
cp -f "/path/to/profile.provisionprofile" "$HOME/Library/MobileDevice/Provisioning Profiles/"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 6 — Build the signed upload package (.pkg)
|
|
||||||
|
|
||||||
This repo provides:
|
|
||||||
|
|
||||||
- `build_tools/macos/build_mas_pkg.sh` (build → deploy → embed profile → sign → productbuild)
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/build_mas_pkg.sh --run --release --clean \
|
|
||||||
--codesign-identity "Apple Distribution: Dry Ark LLC (7628766FL2)" \
|
|
||||||
--installer-identity "3rd Party Mac Developer Installer: Dry Ark LLC (7628766FL2)" \
|
|
||||||
--provisionprofile "$HOME/Library/MobileDevice/Provisioning Profiles/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX.provisionprofile"
|
|
||||||
```
|
|
||||||
|
|
||||||
Outputs:
|
|
||||||
|
|
||||||
- `cmake-build-macos-release-mas/strawberry.app`
|
|
||||||
- `cmake-build-macos-release-mas/strawberry-mas.pkg`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture note — arm64 vs universal (arm64+x86_64)
|
|
||||||
|
|
||||||
For Mac App Store uploads, your `.pkg` can contain either:
|
|
||||||
|
|
||||||
- **arm64-only** app (Apple Silicon only), or
|
|
||||||
- **universal** app (arm64 + x86_64), or
|
|
||||||
- **x86_64-only** app (runs on Apple Silicon under Rosetta 2, but native Intel only otherwise)
|
|
||||||
|
|
||||||
Apple does **not** require universal binaries for review. **arm64-only is allowed**, but:
|
|
||||||
|
|
||||||
- Intel Macs **cannot** run an arm64-only app.
|
|
||||||
- If you ship arm64-only, App Store Connect will effectively make the app available only to Apple Silicon Macs (and your listing will reflect that).
|
|
||||||
|
|
||||||
### Recommendation
|
|
||||||
|
|
||||||
- If you want the broadest compatibility, aim for a **universal build**.
|
|
||||||
- If you’re okay supporting only Apple Silicon Macs, arm64-only is the simplest path.
|
|
||||||
|
|
||||||
### Can I upload two different `.pkg`s (one arm64, one x86_64)?
|
|
||||||
|
|
||||||
Not in the way you want.
|
|
||||||
|
|
||||||
- In App Store Connect you can upload multiple builds over time, but for any given version/submission you ultimately pick **one build** to submit.
|
|
||||||
- Apple will not “merge” two separate uploads (arm64-only + x86_64-only) into one app for customers.
|
|
||||||
|
|
||||||
If you want both Apple Silicon and Intel supported **natively**, you need to produce a **single universal** app bundle and package that into **one** `.pkg`.
|
|
||||||
|
|
||||||
If you don’t want to deal with universal yet, your practical choices are:
|
|
||||||
|
|
||||||
- **arm64-only**: Apple Silicon only.
|
|
||||||
- **x86_64-only**: runs on Intel natively, and on Apple Silicon under **Rosetta 2** (slower, but widely compatible).
|
|
||||||
|
|
||||||
### Practical reality for this repo
|
|
||||||
|
|
||||||
This project depends on large native dependency stacks (Qt, GStreamer, plugins). If you build those via Homebrew, you typically end up with **single-architecture** libraries (arm64 under `/opt/homebrew`, x86_64 under `/usr/local`).
|
|
||||||
|
|
||||||
A true universal app requires **all bundled native code** (your executable *and* all `.dylib`/plugins/frameworks you ship) to be universal as well.
|
|
||||||
|
|
||||||
If you decide you want universal:
|
|
||||||
|
|
||||||
- You’ll need a universal build of **Qt** and **GStreamer** (and all bundled plugins), or
|
|
||||||
- Build arm64 and x86_64 bundles separately and combine *matching* binaries where possible (advanced; easy to break signing / plugin loading).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting — `productbuild` fails with CSSM `-60008` (authorization)
|
|
||||||
|
|
||||||
If you see something like:
|
|
||||||
|
|
||||||
- `SignData failed ... CSSM Exception: -60008 Unable to obtain authorization for this operation`
|
|
||||||
|
|
||||||
That means the **Installer** certificate is present, but macOS is not allowing `productbuild` to use the **private key** without additional authorization.
|
|
||||||
|
|
||||||
### Fix option A (recommended): set key partition list (CLI)
|
|
||||||
|
|
||||||
This is the standard “allow Apple tools to sign without GUI prompts” fix:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
security unlock-keychain "$HOME/Library/Keychains/login.keychain-db"
|
|
||||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "<login-keychain-password>" "$HOME/Library/Keychains/login.keychain-db"
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: if your password contains characters like `!` or `$` and you paste it into a command in `zsh`,
|
|
||||||
the shell can modify it (history/variable expansion) and `security ... -k` may claim it’s “incorrect”.
|
|
||||||
Use **single quotes** (or the env var path shown below) to avoid this, e.g.:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k 'p@ssw0rd!$' "$HOME/Library/Keychains/login.keychain-db"
|
|
||||||
```
|
|
||||||
|
|
||||||
Then rerun:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/build_mas_pkg.sh --run ...
|
|
||||||
```
|
|
||||||
|
|
||||||
This repo’s script also supports:
|
|
||||||
|
|
||||||
- `--keychain-password <pw>` (or env var `STRAWBERRY_KEYCHAIN_PASSWORD`)
|
|
||||||
|
|
||||||
### Fix option B: Keychain Access UI (one-time)
|
|
||||||
|
|
||||||
1. Open **Keychain Access**
|
|
||||||
2. Select **login** keychain → **My Certificates**
|
|
||||||
3. Find your installer cert (e.g. `3rd Party Mac Developer Installer: ...`) and **expand it**
|
|
||||||
4. Select the **private key** under it
|
|
||||||
5. **Get Info → Access Control**
|
|
||||||
- Add `/usr/bin/productbuild` (and optionally `/usr/bin/pkgbuild`) to the allowed apps
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 7 — Upload + submit for review
|
|
||||||
|
|
||||||
### 7.1 Install Apple “Transporter” (the upload tool)
|
|
||||||
|
|
||||||
Apple requires Mac App Store submissions to be uploaded using **Transporter** (a macOS app published by Apple).
|
|
||||||
|
|
||||||
Where to get it:
|
|
||||||
|
|
||||||
- Install **Transporter** from the **Mac App Store** (search for “Transporter”).
|
|
||||||
- App Store listing name is typically **“Transporter”** by Apple.
|
|
||||||
|
|
||||||
### 7.2 Upload the `.pkg` with Transporter
|
|
||||||
|
|
||||||
1. Open **Transporter**
|
|
||||||
2. Sign in with the Apple ID that has access to **App Store Connect**
|
|
||||||
3. Click **Add App** (or **+**) and choose your signed upload package:
|
|
||||||
- `cmake-build-macos-release-mas/strawberry-mas.pkg` (or your custom `--pkg-out` path)
|
|
||||||
4. Click **Deliver**
|
|
||||||
5. Wait for upload + server-side validation to complete
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
|
|
||||||
- Uploading can take a while depending on your connection.
|
|
||||||
- If Transporter reports an error, the message usually includes the exact App Store Connect requirement you violated (bundle id mismatch, missing entitlements, invalid signature, etc.).
|
|
||||||
|
|
||||||
### 7.3 Submit the build in App Store Connect
|
|
||||||
|
|
||||||
1. Open **App Store Connect** in your browser and go to **My Apps**
|
|
||||||
2. Select your app, then go to the **macOS App** platform section
|
|
||||||
3. Find your uploaded build under **TestFlight** or **Prepare for Submission** (Apple’s UI wording changes over time)
|
|
||||||
4. Wait for Apple to finish “Processing” the build
|
|
||||||
5. Select the build for your version, complete required metadata, then click **Submit for Review**
|
|
||||||
|
|
||||||
### (Optional) CLI upload (advanced): `iTMSTransporter`
|
|
||||||
|
|
||||||
If you prefer uploading from the command line, Apple’s underlying uploader is **iTMSTransporter**.
|
|
||||||
On most systems it’s available via Xcode command line tools as:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
xcrun iTMSTransporter -help
|
|
||||||
```
|
|
||||||
|
|
||||||
CLI upload requires additional credentials (App Store Connect API key or Apple ID auth) and is easier to get wrong than the Transporter GUI.
|
|
||||||
For most folks, **Transporter.app is the recommended path**.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Creating a universal Mac App Store upload using two Macs (arm64 + x86_64)
|
|
||||||
|
|
||||||
If you have both an Apple Silicon Mac and an Intel Mac, the most reliable way to ship a universal app for this repo is:
|
|
||||||
|
|
||||||
1. Build + deploy the **unsigned** MAS app bundle on each machine (arm64 and x86_64).
|
|
||||||
2. Copy both `.app` bundles to the machine that has your signing keys.
|
|
||||||
3. Merge them with `lipo` and then **sign + package** once, producing a single universal `.pkg`.
|
|
||||||
|
|
||||||
### Step A — Build + deploy (arm64 machine)
|
|
||||||
|
|
||||||
On your Apple Silicon Mac:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/build_app.sh --release --clean --mas --deploy --build-dir ./cmake-build-macos-release-mas-arm64
|
|
||||||
```
|
|
||||||
|
|
||||||
This produces (unsigned):
|
|
||||||
|
|
||||||
- `cmake-build-macos-release-mas-arm64/strawberry.app`
|
|
||||||
|
|
||||||
### Step B — Build + deploy (x86_64 machine)
|
|
||||||
|
|
||||||
On your Intel Mac:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/build_app.sh --release --clean --mas --deploy --build-dir ./cmake-build-macos-release-mas-x86_64
|
|
||||||
```
|
|
||||||
|
|
||||||
This produces (unsigned):
|
|
||||||
|
|
||||||
- `cmake-build-macos-release-mas-x86_64/strawberry.app`
|
|
||||||
|
|
||||||
### Step C — Copy both app bundles to one “packaging” machine
|
|
||||||
|
|
||||||
Pick the Mac that has your **Apple Distribution** and **Installer** identities (private keys) installed.
|
|
||||||
Copy both `.app` bundles onto that Mac, for example:
|
|
||||||
|
|
||||||
- `/path/to/inputs/strawberry-arm64.app`
|
|
||||||
- `/path/to/inputs/strawberry-x86_64.app`
|
|
||||||
|
|
||||||
Tip: `rsync` works well for app bundles:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rsync -a "/path/to/arm64/strawberry.app" "/path/to/inputs/strawberry-arm64.app"
|
|
||||||
rsync -a "/path/to/x86_64/strawberry.app" "/path/to/inputs/strawberry-x86_64.app"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step D — Merge + sign + build the universal `.pkg`
|
|
||||||
|
|
||||||
On the packaging machine:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/build_mas_universal_pkg.sh --run \
|
|
||||||
--arm-app "/path/to/inputs/strawberry-arm64.app" \
|
|
||||||
--x86-app "/path/to/inputs/strawberry-x86_64.app" \
|
|
||||||
--codesign-identity "Apple Distribution: Your Name (TEAMID)" \
|
|
||||||
--installer-identity "3rd Party Mac Developer Installer: Your Name (TEAMID)" \
|
|
||||||
--provisionprofile "/path/to/profile.provisionprofile"
|
|
||||||
```
|
|
||||||
|
|
||||||
Outputs:
|
|
||||||
|
|
||||||
- `cmake-build-macos-release-mas-universal/strawberry.app` (universal)
|
|
||||||
- `cmake-build-macos-release-mas-universal/strawberry-mas-universal.pkg`
|
|
||||||
|
|
||||||
### Important constraints (don’t skip)
|
|
||||||
|
|
||||||
- The two input apps must be built from the **same commit** with the **same enabled features** so the app bundle layouts match.
|
|
||||||
- Do **not** sign the per-arch apps first; `lipo` invalidates signatures. Sign **only after** merging.
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ts() { date +"%H:%M:%S"; }
|
|
||||||
lower() { echo "$1" | tr '[:upper:]' '[:lower:]'; }
|
|
||||||
|
|
||||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
repo_root="$(cd -- "${script_dir}/../.." && pwd)"
|
|
||||||
|
|
||||||
current_cmd_pid=""
|
|
||||||
current_hb_pid=""
|
|
||||||
|
|
||||||
kill_tree() {
|
|
||||||
local pid="$1"
|
|
||||||
[[ -z "${pid}" ]] && return 0
|
|
||||||
# Recurse into children first (best-effort).
|
|
||||||
local child
|
|
||||||
for child in $(pgrep -P "$pid" 2>/dev/null || true); do
|
|
||||||
kill_tree "$child"
|
|
||||||
done
|
|
||||||
kill -TERM "$pid" >/dev/null 2>&1 || true
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
# Never fail cleanup on errors.
|
|
||||||
set +e
|
|
||||||
if [[ -n "${current_hb_pid}" ]]; then
|
|
||||||
kill "${current_hb_pid}" >/dev/null 2>&1 || true
|
|
||||||
wait "${current_hb_pid}" >/dev/null 2>&1 || true
|
|
||||||
current_hb_pid=""
|
|
||||||
fi
|
|
||||||
if [[ -n "${current_cmd_pid}" ]]; then
|
|
||||||
# If still running, terminate process tree.
|
|
||||||
kill -0 "${current_cmd_pid}" >/dev/null 2>&1 && kill_tree "${current_cmd_pid}"
|
|
||||||
current_cmd_pid=""
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
trap 'cleanup; exit 130' INT TERM
|
|
||||||
trap 'cleanup' EXIT
|
|
||||||
|
|
||||||
run_with_heartbeat() {
|
|
||||||
local desc="$1"
|
|
||||||
shift
|
|
||||||
|
|
||||||
local start now elapsed hb_pid
|
|
||||||
start="$(date +%s)"
|
|
||||||
|
|
||||||
echo "==> [$(ts)] ${desc}"
|
|
||||||
|
|
||||||
# Run the command in the background so we can reliably clean it up on Ctrl-C.
|
|
||||||
set +e
|
|
||||||
"$@" &
|
|
||||||
local cmd_pid=$!
|
|
||||||
set -e
|
|
||||||
current_cmd_pid="$cmd_pid"
|
|
||||||
|
|
||||||
(
|
|
||||||
while kill -0 "$cmd_pid" >/dev/null 2>&1; do
|
|
||||||
sleep 20
|
|
||||||
now="$(date +%s)"
|
|
||||||
elapsed="$((now - start))"
|
|
||||||
echo " [$(ts)] ... still working (${elapsed}s elapsed) ..."
|
|
||||||
done
|
|
||||||
) &
|
|
||||||
hb_pid="$!"
|
|
||||||
current_hb_pid="$hb_pid"
|
|
||||||
|
|
||||||
set +e
|
|
||||||
wait "$cmd_pid"
|
|
||||||
local rc=$?
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Clear globals before stopping heartbeat to avoid cleanup double-kill.
|
|
||||||
current_cmd_pid=""
|
|
||||||
kill "$hb_pid" >/dev/null 2>&1 || true
|
|
||||||
wait "$hb_pid" >/dev/null 2>&1 || true
|
|
||||||
current_hb_pid=""
|
|
||||||
|
|
||||||
now="$(date +%s)"
|
|
||||||
elapsed="$((now - start))"
|
|
||||||
|
|
||||||
if [[ $rc -ne 0 ]]; then
|
|
||||||
echo "Error: '${desc}' failed after ${elapsed}s (exit $rc)." >&2
|
|
||||||
return "$rc"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Done: ${desc} (${elapsed}s)"
|
|
||||||
}
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage:
|
|
||||||
./build_tools/macos/build_app.sh [--debug|--release] [--mas] [--deploy] [--dmg] [--clean] [--build-dir <path>]
|
|
||||||
|
|
||||||
What it does:
|
|
||||||
- Configures and builds Strawberry with CMake + Ninja
|
|
||||||
- Optional: runs CMake targets 'deploy' (bundle deps) and 'dmg' (create DMG)
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--release Release build (default)
|
|
||||||
--debug Debug build
|
|
||||||
--mas Build for Mac App Store (BUILD_FOR_MAC_APP_STORE=ON). Disables Sparkle/QtSparkle and any localhost OAuth redirect listener.
|
|
||||||
--deploy Run: cmake --build <builddir> --target deploy
|
|
||||||
--dmg Run: cmake --build <builddir> --target dmg (implies --deploy)
|
|
||||||
--clean Delete the build dir before configuring
|
|
||||||
--build-dir Override build directory (default: <repo>/cmake-build-macos-<config>)
|
|
||||||
-h, --help Show help
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
config="Release"
|
|
||||||
do_mas=0
|
|
||||||
do_deploy=0
|
|
||||||
do_dmg=0
|
|
||||||
do_clean=0
|
|
||||||
build_dir=""
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--release) config="Release"; shift ;;
|
|
||||||
--debug) config="Debug"; shift ;;
|
|
||||||
--mas) do_mas=1; shift ;;
|
|
||||||
--deploy) do_deploy=1; shift ;;
|
|
||||||
--dmg) do_dmg=1; do_deploy=1; shift ;;
|
|
||||||
--clean) do_clean=1; shift ;;
|
|
||||||
--build-dir) build_dir="${2:-}"; shift 2 ;;
|
|
||||||
-h|--help) usage; exit 0 ;;
|
|
||||||
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
||||||
echo "Error: This script is for macOS only." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v xcode-select >/dev/null 2>&1 || ! xcode-select -p >/dev/null 2>&1; then
|
|
||||||
echo "Error: Xcode Command Line Tools not found." >&2
|
|
||||||
echo "Install them first: xcode-select --install" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v brew >/dev/null 2>&1; then
|
|
||||||
echo "Error: Homebrew ('brew') not found in PATH." >&2
|
|
||||||
echo "Install Homebrew first: https://brew.sh/" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v cmake >/dev/null 2>&1; then
|
|
||||||
echo "Error: cmake not found. Did you run ./build_tools/macos/install_brew_deps.sh ?" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v ninja >/dev/null 2>&1; then
|
|
||||||
echo "Error: ninja not found. Did you run ./build_tools/macos/install_brew_deps.sh ?" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
brew_prefix="$(brew --prefix)"
|
|
||||||
qt_prefix="$(brew --prefix qt)"
|
|
||||||
icu_prefix="$(brew --prefix icu4c || true)"
|
|
||||||
|
|
||||||
if [[ -z "$build_dir" ]]; then
|
|
||||||
build_dir="${repo_root}/cmake-build-macos-$(lower "$config")"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Repo: ${repo_root}"
|
|
||||||
echo "==> [$(ts)] Build dir: ${build_dir}"
|
|
||||||
echo "==> [$(ts)] Config: ${config}"
|
|
||||||
if [[ "$do_mas" -eq 1 ]]; then
|
|
||||||
echo "==> [$(ts)] MAS: enabled (BUILD_FOR_MAC_APP_STORE=ON)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$do_clean" -eq 1 ]]; then
|
|
||||||
echo "==> [$(ts)] Cleaning build dir"
|
|
||||||
# macOS 26+ can apply provenance metadata that blocks deletion even when permissions look normal.
|
|
||||||
# Clear common xattrs and immutable flags before deleting.
|
|
||||||
xattr -dr com.apple.provenance "$build_dir" >/dev/null 2>&1 || true
|
|
||||||
xattr -dr com.apple.quarantine "$build_dir" >/dev/null 2>&1 || true
|
|
||||||
chflags -R nouchg,noschg "$build_dir" >/dev/null 2>&1 || true
|
|
||||||
rm -rf "$build_dir" || {
|
|
||||||
echo "Error: failed to remove build dir: $build_dir" >&2
|
|
||||||
echo "This is usually due to macOS provenance/flags. Try:" >&2
|
|
||||||
echo " xattr -dr com.apple.provenance \"$build_dir\"" >&2
|
|
||||||
echo " chflags -R nouchg,noschg \"$build_dir\"" >&2
|
|
||||||
echo " rm -rf \"$build_dir\"" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$build_dir"
|
|
||||||
|
|
||||||
# If you've run a previously-built app directly from the build directory, macOS can apply provenance
|
|
||||||
# metadata that makes the bundle effectively immutable (even when permissions look normal).
|
|
||||||
# That breaks CMake because it needs to update strawberry.app/Contents/Info.plist during configure/build.
|
|
||||||
app_bundle="${build_dir}/strawberry.app"
|
|
||||||
if [[ -d "${app_bundle}/Contents" ]]; then
|
|
||||||
# Try to clear provenance/quarantine metadata first (best effort).
|
|
||||||
xattr -dr com.apple.provenance "${app_bundle}" >/dev/null 2>&1 || true
|
|
||||||
xattr -dr com.apple.quarantine "${app_bundle}" >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
# If the bundle is still not writable, remove it so CMake can recreate it.
|
|
||||||
if ! ( : > "${app_bundle}/Contents/.cmake_write_test" ) 2>/dev/null; then
|
|
||||||
echo "==> [$(ts)] Existing ${app_bundle} is not writable (likely macOS provenance). Removing it."
|
|
||||||
rm -rf "${app_bundle}"
|
|
||||||
else
|
|
||||||
rm -f "${app_bundle}/Contents/.cmake_write_test" >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Make pkg-config more reliable with Homebrew.
|
|
||||||
export PKG_CONFIG_PATH="${brew_prefix}/lib/pkgconfig:${brew_prefix}/share/pkgconfig:${PKG_CONFIG_PATH:-}"
|
|
||||||
|
|
||||||
# For dist/CMakeLists.txt Info.plist minimum version logic.
|
|
||||||
export MACOSX_DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:-12.0}"
|
|
||||||
|
|
||||||
cmake_prefix_path="${qt_prefix};${brew_prefix}"
|
|
||||||
|
|
||||||
cmake_extra_args=()
|
|
||||||
|
|
||||||
# Mac App Store build mode
|
|
||||||
if [[ "$do_mas" -eq 1 ]]; then
|
|
||||||
cmake_extra_args+=("-DBUILD_FOR_MAC_APP_STORE=ON")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Optional: override Sparkle update feed / key for your own published builds.
|
|
||||||
# Example:
|
|
||||||
# export SPARKLE_FEED_URL="https://example.com/appcast.xml"
|
|
||||||
# export SPARKLE_PUBLIC_ED25519_KEY="base64=="
|
|
||||||
if [[ -n "${SPARKLE_FEED_URL:-}" ]]; then
|
|
||||||
cmake_extra_args+=("-DSPARKLE_FEED_URL=${SPARKLE_FEED_URL}")
|
|
||||||
fi
|
|
||||||
if [[ -n "${SPARKLE_PUBLIC_ED25519_KEY:-}" ]]; then
|
|
||||||
cmake_extra_args+=("-DSPARKLE_PUBLIC_ED25519_KEY=${SPARKLE_PUBLIC_ED25519_KEY}")
|
|
||||||
fi
|
|
||||||
|
|
||||||
run_with_heartbeat "Configuring (CMAKE_PREFIX_PATH=${cmake_prefix_path})" \
|
|
||||||
cmake -S "$repo_root" -B "$build_dir" -G Ninja \
|
|
||||||
-DCMAKE_BUILD_TYPE="$config" \
|
|
||||||
-DCMAKE_PREFIX_PATH="$cmake_prefix_path" \
|
|
||||||
-DCMAKE_FRAMEWORK_PATH="${brew_prefix}/Frameworks;${brew_prefix}/opt/sparkle-framework/Frameworks" \
|
|
||||||
-DOPTIONAL_COMPONENTS_MISSING_DEPS_ARE_FATAL=OFF \
|
|
||||||
${cmake_extra_args+"${cmake_extra_args[@]}"} \
|
|
||||||
${icu_prefix:+-DICU_ROOT="$icu_prefix"}
|
|
||||||
|
|
||||||
run_with_heartbeat "Building" \
|
|
||||||
cmake --build "$build_dir" --parallel
|
|
||||||
|
|
||||||
if [[ "$do_deploy" -eq 1 ]]; then
|
|
||||||
echo "==> [$(ts)] Preparing env for 'deploy' target (GIO/GStreamer)"
|
|
||||||
export GIO_EXTRA_MODULES="${brew_prefix}/lib/gio/modules"
|
|
||||||
export GST_PLUGIN_SCANNER="${brew_prefix}/opt/gstreamer/libexec/gstreamer-1.0/gst-plugin-scanner"
|
|
||||||
export GST_PLUGIN_PATH="${brew_prefix}/lib/gstreamer-1.0"
|
|
||||||
|
|
||||||
# Optional, but helps dist/macos/macgstcopy.sh bundle libsoup which GStreamer loads dynamically.
|
|
||||||
libsoup_prefix="$(brew --prefix libsoup 2>/dev/null || true)"
|
|
||||||
if [[ -n "${libsoup_prefix}" ]]; then
|
|
||||||
libsoup_dylib="$(ls -1 "${libsoup_prefix}"/lib/libsoup-*.dylib 2>/dev/null | head -n 1 || true)"
|
|
||||||
if [[ -n "${libsoup_dylib}" ]]; then
|
|
||||||
export LIBSOUP_LIBRARY_PATH="${libsoup_dylib}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
run_with_heartbeat "Running: deploy" \
|
|
||||||
cmake --build "$build_dir" --target deploy
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$do_dmg" -eq 1 ]]; then
|
|
||||||
run_with_heartbeat "Running: dmg" \
|
|
||||||
cmake --build "$build_dir" --target dmg
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Done"
|
|
||||||
echo "Built app:"
|
|
||||||
echo " ${build_dir}/strawberry.app"
|
|
||||||
|
|
||||||
@@ -1,324 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Guard: this script must be executed with bash (not sourced into zsh, not run via sh).
|
|
||||||
if [[ -z "${BASH_VERSION:-}" ]]; then
|
|
||||||
echo "Error: this script must be run with bash (it uses bash arrays)." >&2
|
|
||||||
echo "Run:" >&2
|
|
||||||
echo " bash ./build_tools/macos/build_mas_pkg.sh --run ..." >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
ts() { date +"%H:%M:%S"; }
|
|
||||||
lower() { echo "$1" | tr '[:upper:]' '[:lower:]'; }
|
|
||||||
|
|
||||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
repo_root="$(cd -- "${script_dir}/../.." && pwd)"
|
|
||||||
|
|
||||||
prepare_login_keychain_for_signing() {
|
|
||||||
# Some setups require explicitly granting Apple tooling access to the private key(s)
|
|
||||||
# (otherwise productbuild/codesign can fail with authorization errors like:
|
|
||||||
# CSSM Exception: -60008 Unable to obtain authorization for this operation
|
|
||||||
# or "User interaction is not allowed").
|
|
||||||
#
|
|
||||||
# This function is optional and only runs if a keychain password is provided.
|
|
||||||
local keychain_path="$HOME/Library/Keychains/login.keychain-db"
|
|
||||||
local pw="${1:-}"
|
|
||||||
|
|
||||||
if [[ -z "$pw" ]]; then
|
|
||||||
echo "==> [$(ts)] Note: no keychain password provided; skipping keychain access-control preparation."
|
|
||||||
echo " If productbuild later fails with -60008 authorization errors, fix it with either:"
|
|
||||||
echo " - Keychain Access → login → My Certificates → select the *private key* under the Installer cert → Get Info → Access Control → allow productbuild"
|
|
||||||
echo " - OR (CLI): security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k <password> \"$keychain_path\""
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Preparing login keychain for signing (unlock + key partition list)"
|
|
||||||
# Unlock so Security/Authorization can use keys without prompting.
|
|
||||||
security unlock-keychain -p "$pw" "$keychain_path" >/dev/null 2>&1 || true
|
|
||||||
# Allow Apple tools (codesign/productbuild) to access the private key without GUI prompts.
|
|
||||||
# This is the standard fix used for non-interactive signing.
|
|
||||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$pw" "$keychain_path" >/dev/null 2>&1 || true
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_keychain_search_list() {
|
|
||||||
# codesign builds the cert chain using the user keychain search list.
|
|
||||||
# If the list is missing the System keychain, you can get:
|
|
||||||
# "unable to build chain to self-signed root" + errSecInternalComponent
|
|
||||||
local login_kc="$HOME/Library/Keychains/login.keychain-db"
|
|
||||||
local system_kc="/Library/Keychains/System.keychain"
|
|
||||||
local roots_kc="/System/Library/Keychains/SystemRootCertificates.keychain"
|
|
||||||
|
|
||||||
local current
|
|
||||||
current="$(security list-keychains -d user 2>/dev/null | tr -d '"' | tr -d ' ' || true)"
|
|
||||||
|
|
||||||
if echo "$current" | grep -Fq "$system_kc"; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Note: adding System keychains to the user keychain search list (fixes common codesign chain errors)"
|
|
||||||
echo " (This changes the user keychain search list; run 'security list-keychains -d user' to view.)"
|
|
||||||
security list-keychains -d user -s "$login_kc" "$system_kc" "$roots_kc" >/dev/null 2>&1 || true
|
|
||||||
}
|
|
||||||
|
|
||||||
diagnose_chain_failure() {
|
|
||||||
echo "==> [$(ts)] Codesign failed. Common causes on macOS:"
|
|
||||||
echo " - System keychains not in the user keychain search list"
|
|
||||||
echo " - Missing/invalid WWDR intermediate certificate"
|
|
||||||
echo " - Keychain/key access issues"
|
|
||||||
echo
|
|
||||||
echo "==> [$(ts)] Keychain search list:"
|
|
||||||
security list-keychains -d user || true
|
|
||||||
echo
|
|
||||||
echo "==> [$(ts)] Checking for Apple WWDR intermediate in System keychain:"
|
|
||||||
security find-certificate -a -c "Apple Worldwide Developer Relations" /Library/Keychains/System.keychain 2>/dev/null | head -n 5 || true
|
|
||||||
echo
|
|
||||||
echo "==> [$(ts)] If WWDR is missing, install the current Apple WWDR intermediate certificate (via Xcode or Apple Developer portal)."
|
|
||||||
echo "==> [$(ts)] Then re-run this script."
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
preflight_identity() {
|
|
||||||
local what="$1"
|
|
||||||
local policy="$2"
|
|
||||||
local identity="$3"
|
|
||||||
|
|
||||||
# NOTE: security expects "-p <policy>" as *two* args; do not pass "-p codesigning" as one string.
|
|
||||||
if ! security find-identity -p "$policy" -v 2>/dev/null | grep -Fq "$identity"; then
|
|
||||||
echo "Error: ${what} identity not found/usable in Keychain: $identity" >&2
|
|
||||||
echo "Run: ./build_tools/macos/check_signing_identities.sh" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage:
|
|
||||||
./build_tools/macos/build_mas_pkg.sh --run [options]
|
|
||||||
|
|
||||||
What it does:
|
|
||||||
- Builds Strawberry in Mac App Store mode (BUILD_FOR_MAC_APP_STORE=ON)
|
|
||||||
- Runs deploy (macdeployqt + bundling) so the app bundle is self-contained
|
|
||||||
- Embeds a Mac App Store provisioning profile into the app bundle
|
|
||||||
- Codesigns the app with an Apple Distribution identity + entitlements
|
|
||||||
- Builds a signed .pkg suitable for uploading to App Store Connect
|
|
||||||
|
|
||||||
Required options:
|
|
||||||
--run
|
|
||||||
--codesign-identity "<name>" (e.g. "Apple Distribution: Dry Ark LLC (TEAMID)")
|
|
||||||
--installer-identity "<name>" (e.g. "3rd Party Mac Developer Installer: Dry Ark LLC (TEAMID)")
|
|
||||||
--provisionprofile <path> Path to a *Mac App Store* provisioning profile (*.provisionprofile)
|
|
||||||
|
|
||||||
Optional:
|
|
||||||
--release | --debug Build config (default: Release)
|
|
||||||
--clean Clean build dir before build
|
|
||||||
--build-dir <path> Override build directory
|
|
||||||
--entitlements <plist> Codesign entitlements (default: dist/macos/entitlements.mas.plist)
|
|
||||||
--bundle-id <id> Override CFBundleIdentifier (default: com.dryark.strawberry)
|
|
||||||
--pkg-out <path> Output .pkg path (default: <build-dir>/strawberry-mas.pkg)
|
|
||||||
--keychain-password <pw> OPTIONAL: unlock/login keychain + set key partition list for Apple tools
|
|
||||||
(alternative: set env var STRAWBERRY_KEYCHAIN_PASSWORD)
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
# Tip: if your keychain password contains characters like ! or $, prefer the env var or single quotes.
|
|
||||||
STRAWBERRY_KEYCHAIN_PASSWORD='your-login-keychain-password' \
|
|
||||||
./build_tools/macos/build_mas_pkg.sh --run --release --clean \
|
|
||||||
--codesign-identity "Apple Distribution: Your Name (TEAMID)" \
|
|
||||||
--installer-identity "3rd Party Mac Developer Installer: Your Name (TEAMID)" \
|
|
||||||
--provisionprofile "$HOME/Library/MobileDevice/Provisioning Profiles/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX.provisionprofile"
|
|
||||||
|
|
||||||
./build_tools/macos/build_mas_pkg.sh --run --release --clean \
|
|
||||||
--codesign-identity "Apple Distribution: Your Name (TEAMID)" \
|
|
||||||
--installer-identity "3rd Party Mac Developer Installer: Your Name (TEAMID)" \
|
|
||||||
--provisionprofile "$HOME/Library/MobileDevice/Provisioning Profiles/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX.provisionprofile"
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- Mac App Store submissions do NOT use Developer ID notarization.
|
|
||||||
- You must create a Mac App Store provisioning profile for your App ID in Apple Developer.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
||||||
echo "Error: This script is for macOS only." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
do_run=0
|
|
||||||
config="Release"
|
|
||||||
do_clean=0
|
|
||||||
build_dir=""
|
|
||||||
codesign_identity=""
|
|
||||||
installer_identity=""
|
|
||||||
provisionprofile=""
|
|
||||||
entitlements=""
|
|
||||||
bundle_id="com.dryark.strawberry"
|
|
||||||
pkg_out=""
|
|
||||||
keychain_password="${STRAWBERRY_KEYCHAIN_PASSWORD:-}"
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--run) do_run=1; shift ;;
|
|
||||||
--release) config="Release"; shift ;;
|
|
||||||
--debug) config="Debug"; shift ;;
|
|
||||||
--clean) do_clean=1; shift ;;
|
|
||||||
--build-dir) build_dir="${2:-}"; shift 2 ;;
|
|
||||||
--codesign-identity) codesign_identity="${2:-}"; shift 2 ;;
|
|
||||||
--installer-identity) installer_identity="${2:-}"; shift 2 ;;
|
|
||||||
--provisionprofile) provisionprofile="${2:-}"; shift 2 ;;
|
|
||||||
--entitlements) entitlements="${2:-}"; shift 2 ;;
|
|
||||||
--bundle-id) bundle_id="${2:-}"; shift 2 ;;
|
|
||||||
--pkg-out) pkg_out="${2:-}"; shift 2 ;;
|
|
||||||
--keychain-password) keychain_password="${2:-}"; shift 2 ;;
|
|
||||||
-h|--help) usage; exit 0 ;;
|
|
||||||
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "$do_run" -eq 0 ]]; then
|
|
||||||
usage
|
|
||||||
echo
|
|
||||||
echo "==> [$(ts)] Tip: list available signing identities:"
|
|
||||||
echo " security find-identity -p codesigning -v"
|
|
||||||
echo " security find-identity -p basic -v"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$codesign_identity" ]]; then
|
|
||||||
echo "Error: missing --codesign-identity" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
if [[ -z "$installer_identity" ]]; then
|
|
||||||
echo "Error: missing --installer-identity" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
if [[ -z "$provisionprofile" || ! -f "$provisionprofile" ]]; then
|
|
||||||
echo "Error: missing/invalid --provisionprofile: $provisionprofile" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$entitlements" ]]; then
|
|
||||||
entitlements="${repo_root}/dist/macos/entitlements.mas.plist"
|
|
||||||
fi
|
|
||||||
if [[ ! -f "$entitlements" ]]; then
|
|
||||||
echo "Error: entitlements file not found: $entitlements" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$build_dir" ]]; then
|
|
||||||
build_dir="${repo_root}/cmake-build-macos-$(lower "$config")-mas"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$pkg_out" ]]; then
|
|
||||||
pkg_out="${build_dir}/strawberry-mas.pkg"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Repo: ${repo_root}"
|
|
||||||
echo "==> [$(ts)] Build dir: ${build_dir}"
|
|
||||||
echo "==> [$(ts)] Config: ${config}"
|
|
||||||
echo "==> [$(ts)] Bundle ID: ${bundle_id}"
|
|
||||||
echo "==> [$(ts)] Entitlements: ${entitlements}"
|
|
||||||
echo "==> [$(ts)] Provisioning profile: ${provisionprofile}"
|
|
||||||
echo "==> [$(ts)] Output pkg: ${pkg_out}"
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Building (Mac App Store mode)"
|
|
||||||
build_args=( "--release" )
|
|
||||||
if [[ "$config" == "Debug" ]]; then build_args=( "--debug" ); fi
|
|
||||||
if [[ "$do_clean" -eq 1 ]]; then build_args+=( "--clean" ); fi
|
|
||||||
build_args+=( "--build-dir" "$build_dir" "--mas" "--deploy" )
|
|
||||||
|
|
||||||
# Provide bundle id via CMake cache variable.
|
|
||||||
export MACOS_BUNDLE_ID="$bundle_id"
|
|
||||||
|
|
||||||
"${repo_root}/build_tools/macos/build_app.sh" "${build_args[@]}"
|
|
||||||
|
|
||||||
app_path="${build_dir}/strawberry.app"
|
|
||||||
bin_path="${app_path}/Contents/MacOS/strawberry"
|
|
||||||
if [[ ! -x "$bin_path" ]]; then
|
|
||||||
echo "Error: built app not found at: $app_path" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Embedding provisioning profile"
|
|
||||||
cp -f "$provisionprofile" "${app_path}/Contents/embedded.provisionprofile"
|
|
||||||
|
|
||||||
ensure_keychain_search_list
|
|
||||||
prepare_login_keychain_for_signing "$keychain_password"
|
|
||||||
preflight_identity "codesign" "codesigning" "$codesign_identity"
|
|
||||||
preflight_identity "installer" "basic" "$installer_identity"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Codesigning app (Mac App Store)"
|
|
||||||
codesign_args=( --force --timestamp --options runtime --sign "$codesign_identity" --entitlements "$entitlements" )
|
|
||||||
|
|
||||||
# Clean up any leftover codesign temp files from previous interrupted runs.
|
|
||||||
find "$app_path" -name "*.cstemp" -print0 2>/dev/null | while IFS= read -r -d '' f; do
|
|
||||||
rm -f "$f" || true
|
|
||||||
done
|
|
||||||
|
|
||||||
# Clear macOS provenance/quarantine metadata which can interfere with modifying files in-place.
|
|
||||||
xattr -dr com.apple.provenance "$app_path" >/dev/null 2>&1 || true
|
|
||||||
xattr -dr com.apple.quarantine "$app_path" >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
# Sign nested code first, then frameworks, then the main app bundle.
|
|
||||||
find "$app_path" -type f \( -name "*.dylib" -o -name "*.so" -o -perm -111 \) \
|
|
||||||
! -name "*.cstemp" \
|
|
||||||
! -path "*/Contents/Frameworks/*.framework/*" \
|
|
||||||
! -path "*/Contents/Frameworks/*.app/*" \
|
|
||||||
! -path "*/Contents/Frameworks/*.xpc/*" \
|
|
||||||
! -path "*/Contents/PlugIns/*.framework/*" \
|
|
||||||
! -path "*/Contents/PlugIns/*.app/*" \
|
|
||||||
! -path "*/Contents/PlugIns/*.xpc/*" \
|
|
||||||
-print0 | while IFS= read -r -d '' f; do
|
|
||||||
# Only sign Mach-O binaries.
|
|
||||||
if file -b "$f" | grep -q "Mach-O"; then
|
|
||||||
codesign "${codesign_args[@]}" "$f" >/dev/null
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
find "$app_path" -type d \( -name "*.xpc" -o -name "*.app" \) -print0 2>/dev/null | while IFS= read -r -d '' b; do
|
|
||||||
codesign "${codesign_args[@]}" "$b" >/dev/null
|
|
||||||
done
|
|
||||||
|
|
||||||
find "$app_path/Contents/Frameworks" "$app_path/Contents/PlugIns" -type d -name "*.framework" -print0 2>/dev/null | while IFS= read -r -d '' fw; do
|
|
||||||
codesign "${codesign_args[@]}" "$fw" >/dev/null
|
|
||||||
done
|
|
||||||
|
|
||||||
codesign "${codesign_args[@]}" "$app_path" >/dev/null
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Verifying codesign"
|
|
||||||
codesign --verify --deep --strict --verbose=2 "$app_path"
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Building signed .pkg for App Store upload"
|
|
||||||
rm -f "$pkg_out" >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
if ! productbuild \
|
|
||||||
--component "$app_path" /Applications \
|
|
||||||
--sign "$installer_identity" \
|
|
||||||
"$pkg_out"; then
|
|
||||||
echo "Error: productbuild failed while signing the .pkg." >&2
|
|
||||||
echo "Common cause: keychain/private-key authorization (e.g. CSSM -60008)." >&2
|
|
||||||
echo >&2
|
|
||||||
echo "Fix options:" >&2
|
|
||||||
echo "1) Keychain Access UI:" >&2
|
|
||||||
echo " - Keychain Access → login → My Certificates" >&2
|
|
||||||
echo " - Find: $installer_identity" >&2
|
|
||||||
echo " - Expand it and select the *private key* under it" >&2
|
|
||||||
echo " - Get Info → Access Control → allow /usr/bin/productbuild (optionally also allow /usr/bin/pkgbuild)" >&2
|
|
||||||
echo "2) CLI (recommended for repeatable builds):" >&2
|
|
||||||
echo " security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k <password> \"$HOME/Library/Keychains/login.keychain-db\"" >&2
|
|
||||||
echo >&2
|
|
||||||
echo "Tip: you can also rerun this script with:" >&2
|
|
||||||
echo " --keychain-password <login-keychain-password>" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Verifying pkg signature"
|
|
||||||
pkgutil --check-signature "$pkg_out" || true
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "Done."
|
|
||||||
echo "App: $app_path"
|
|
||||||
echo "PKG: $pkg_out"
|
|
||||||
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Build a universal (arm64+x86_64) Mac App Store upload package by:
|
|
||||||
# - merging two already-deployed Strawberry.app bundles (arm64 + x86_64) using lipo
|
|
||||||
# - embedding a Mac App Store provisioning profile
|
|
||||||
# - codesigning with Apple Distribution (+ entitlements)
|
|
||||||
# - producing a signed .pkg with productbuild (Installer identity)
|
|
||||||
#
|
|
||||||
# Intended workflow with two Macs:
|
|
||||||
# 1) On Apple Silicon Mac: build+deploy MAS app bundle (unsigned) → copy strawberry.app somewhere
|
|
||||||
# 2) On Intel Mac: build+deploy MAS app bundle (unsigned) → copy strawberry.app somewhere
|
|
||||||
# 3) On the Mac that holds your signing keys (either one): run THIS script to merge+sign+package
|
|
||||||
|
|
||||||
ts() { date +"%H:%M:%S"; }
|
|
||||||
|
|
||||||
if [[ -z "${BASH_VERSION:-}" ]]; then
|
|
||||||
echo "Error: this script must be run with bash." >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
||||||
echo "Error: This script is for macOS only." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
repo_root="$(cd -- "${script_dir}/../.." && pwd)"
|
|
||||||
|
|
||||||
ensure_keychain_search_list() {
|
|
||||||
local login_kc="$HOME/Library/Keychains/login.keychain-db"
|
|
||||||
local system_kc="/Library/Keychains/System.keychain"
|
|
||||||
local roots_kc="/System/Library/Keychains/SystemRootCertificates.keychain"
|
|
||||||
|
|
||||||
local current
|
|
||||||
current="$(security list-keychains -d user 2>/dev/null | tr -d '"' | tr -d ' ' || true)"
|
|
||||||
if echo "$current" | grep -Fq "$system_kc"; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
echo "==> [$(ts)] Note: adding System keychains to the user keychain search list"
|
|
||||||
security list-keychains -d user -s "$login_kc" "$system_kc" "$roots_kc" >/dev/null 2>&1 || true
|
|
||||||
}
|
|
||||||
|
|
||||||
prepare_login_keychain_for_signing() {
|
|
||||||
local keychain_path="$HOME/Library/Keychains/login.keychain-db"
|
|
||||||
local pw="${1:-}"
|
|
||||||
if [[ -z "$pw" ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
echo "==> [$(ts)] Preparing login keychain for signing (unlock + key partition list)"
|
|
||||||
security unlock-keychain -p "$pw" "$keychain_path" >/dev/null 2>&1 || true
|
|
||||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$pw" "$keychain_path" >/dev/null 2>&1 || true
|
|
||||||
}
|
|
||||||
|
|
||||||
preflight_identity() {
|
|
||||||
local what="$1"
|
|
||||||
local policy="$2"
|
|
||||||
local identity="$3"
|
|
||||||
if ! security find-identity -p "$policy" -v 2>/dev/null | grep -Fq "$identity"; then
|
|
||||||
echo "Error: ${what} identity not found/usable in Keychain: $identity" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage:
|
|
||||||
./build_tools/macos/build_mas_universal_pkg.sh --run [options]
|
|
||||||
|
|
||||||
Required:
|
|
||||||
--run
|
|
||||||
--arm-app <path> Path to arm64 Strawberry.app (already built+deployed, unsigned)
|
|
||||||
--x86-app <path> Path to x86_64 Strawberry.app (already built+deployed, unsigned)
|
|
||||||
--codesign-identity "<name>" Apple Distribution: ...
|
|
||||||
--installer-identity "<name>" 3rd Party Mac Developer Installer: ...
|
|
||||||
--provisionprofile <path> Mac App Store provisioning profile (*.provisionprofile)
|
|
||||||
|
|
||||||
Optional:
|
|
||||||
--out-dir <path> Output directory (default: cmake-build-macos-release-mas-universal)
|
|
||||||
--entitlements <plist> Codesign entitlements (default: dist/macos/entitlements.mas.plist)
|
|
||||||
--pkg-out <path> Output .pkg path (default: <out-dir>/strawberry-mas-universal.pkg)
|
|
||||||
--bundle-id <id> For display/logging only (does not rewrite Info.plist)
|
|
||||||
--keychain-password <pw> Or set env var STRAWBERRY_KEYCHAIN_PASSWORD (quote it!)
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- This script does NOT build Strawberry. It merges two pre-built app bundles.
|
|
||||||
- After lipo-merging, the app must be re-signed (this script does that).
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
do_run=0
|
|
||||||
arm_app=""
|
|
||||||
x86_app=""
|
|
||||||
out_dir=""
|
|
||||||
codesign_identity=""
|
|
||||||
installer_identity=""
|
|
||||||
provisionprofile=""
|
|
||||||
entitlements=""
|
|
||||||
pkg_out=""
|
|
||||||
bundle_id=""
|
|
||||||
keychain_password="${STRAWBERRY_KEYCHAIN_PASSWORD:-}"
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--run) do_run=1; shift ;;
|
|
||||||
--arm-app) arm_app="${2:-}"; shift 2 ;;
|
|
||||||
--x86-app) x86_app="${2:-}"; shift 2 ;;
|
|
||||||
--out-dir) out_dir="${2:-}"; shift 2 ;;
|
|
||||||
--codesign-identity) codesign_identity="${2:-}"; shift 2 ;;
|
|
||||||
--installer-identity) installer_identity="${2:-}"; shift 2 ;;
|
|
||||||
--provisionprofile) provisionprofile="${2:-}"; shift 2 ;;
|
|
||||||
--entitlements) entitlements="${2:-}"; shift 2 ;;
|
|
||||||
--pkg-out) pkg_out="${2:-}"; shift 2 ;;
|
|
||||||
--bundle-id) bundle_id="${2:-}"; shift 2 ;;
|
|
||||||
--keychain-password) keychain_password="${2:-}"; shift 2 ;;
|
|
||||||
-h|--help) usage; exit 0 ;;
|
|
||||||
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "$do_run" -eq 0 ]]; then
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$arm_app" || ! -d "$arm_app" ]]; then
|
|
||||||
echo "Error: missing/invalid --arm-app: $arm_app" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
if [[ -z "$x86_app" || ! -d "$x86_app" ]]; then
|
|
||||||
echo "Error: missing/invalid --x86-app: $x86_app" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
if [[ -z "$codesign_identity" ]]; then
|
|
||||||
echo "Error: missing --codesign-identity" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
if [[ -z "$installer_identity" ]]; then
|
|
||||||
echo "Error: missing --installer-identity" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
if [[ -z "$provisionprofile" || ! -f "$provisionprofile" ]]; then
|
|
||||||
echo "Error: missing/invalid --provisionprofile: $provisionprofile" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$entitlements" ]]; then
|
|
||||||
entitlements="${repo_root}/dist/macos/entitlements.mas.plist"
|
|
||||||
fi
|
|
||||||
if [[ ! -f "$entitlements" ]]; then
|
|
||||||
echo "Error: entitlements file not found: $entitlements" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$out_dir" ]]; then
|
|
||||||
out_dir="${repo_root}/cmake-build-macos-release-mas-universal"
|
|
||||||
fi
|
|
||||||
mkdir -p "$out_dir"
|
|
||||||
|
|
||||||
universal_app="${out_dir}/strawberry.app"
|
|
||||||
if [[ -e "$universal_app" ]]; then
|
|
||||||
rm -rf "$universal_app"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Repo: $repo_root"
|
|
||||||
echo "==> [$(ts)] arm app: $arm_app"
|
|
||||||
echo "==> [$(ts)] x86 app: $x86_app"
|
|
||||||
echo "==> [$(ts)] out dir: $out_dir"
|
|
||||||
if [[ -n "$bundle_id" ]]; then
|
|
||||||
echo "==> [$(ts)] bundle id (expected): $bundle_id"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Creating universal app bundle (lipo merge)"
|
|
||||||
"${repo_root}/build_tools/macos/make_universal_app.sh" \
|
|
||||||
--arm-app "$arm_app" \
|
|
||||||
--x86-app "$x86_app" \
|
|
||||||
--out-app "$universal_app" \
|
|
||||||
--clean
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Embedding provisioning profile"
|
|
||||||
cp -f "$provisionprofile" "${universal_app}/Contents/embedded.provisionprofile"
|
|
||||||
|
|
||||||
ensure_keychain_search_list
|
|
||||||
prepare_login_keychain_for_signing "$keychain_password"
|
|
||||||
preflight_identity "codesign" "codesigning" "$codesign_identity"
|
|
||||||
preflight_identity "installer" "basic" "$installer_identity"
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Codesigning universal app (Mac App Store)"
|
|
||||||
codesign_args=( --force --timestamp --options runtime --sign "$codesign_identity" --entitlements "$entitlements" )
|
|
||||||
|
|
||||||
# Clean up any leftover codesign temp files and xattrs.
|
|
||||||
find "$universal_app" -name "*.cstemp" -print0 2>/dev/null | while IFS= read -r -d '' f; do rm -f "$f" || true; done
|
|
||||||
xattr -dr com.apple.provenance "$universal_app" >/dev/null 2>&1 || true
|
|
||||||
xattr -dr com.apple.quarantine "$universal_app" >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
# Sign nested code first, then frameworks, then the main app bundle.
|
|
||||||
find "$universal_app" -type f \( -name "*.dylib" -o -name "*.so" -o -perm -111 \) \
|
|
||||||
! -name "*.cstemp" \
|
|
||||||
! -path "*/Contents/Frameworks/*.framework/*" \
|
|
||||||
! -path "*/Contents/Frameworks/*.app/*" \
|
|
||||||
! -path "*/Contents/Frameworks/*.xpc/*" \
|
|
||||||
! -path "*/Contents/PlugIns/*.framework/*" \
|
|
||||||
! -path "*/Contents/PlugIns/*.app/*" \
|
|
||||||
! -path "*/Contents/PlugIns/*.xpc/*" \
|
|
||||||
-print0 | while IFS= read -r -d '' f; do
|
|
||||||
if /usr/bin/file -b "$f" | grep -q "Mach-O"; then
|
|
||||||
codesign "${codesign_args[@]}" "$f" >/dev/null
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
find "$universal_app" -type d \( -name "*.xpc" -o -name "*.app" \) -print0 2>/dev/null | while IFS= read -r -d '' b; do
|
|
||||||
codesign "${codesign_args[@]}" "$b" >/dev/null
|
|
||||||
done
|
|
||||||
|
|
||||||
find "$universal_app/Contents/Frameworks" "$universal_app/Contents/PlugIns" -type d -name "*.framework" -print0 2>/dev/null | while IFS= read -r -d '' fw; do
|
|
||||||
codesign "${codesign_args[@]}" "$fw" >/dev/null
|
|
||||||
done
|
|
||||||
|
|
||||||
codesign "${codesign_args[@]}" "$universal_app" >/dev/null
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Verifying codesign"
|
|
||||||
codesign --verify --deep --strict --verbose=2 "$universal_app"
|
|
||||||
|
|
||||||
if [[ -z "$pkg_out" ]]; then
|
|
||||||
pkg_out="${out_dir}/strawberry-mas-universal.pkg"
|
|
||||||
fi
|
|
||||||
rm -f "$pkg_out" >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Building signed .pkg for App Store upload"
|
|
||||||
productbuild \
|
|
||||||
--component "$universal_app" /Applications \
|
|
||||||
--sign "$installer_identity" \
|
|
||||||
"$pkg_out"
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Verifying pkg signature"
|
|
||||||
pkgutil --check-signature "$pkg_out" || true
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "Done."
|
|
||||||
echo "Universal app: $universal_app"
|
|
||||||
echo "PKG: $pkg_out"
|
|
||||||
|
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ts() { date +"%H:%M:%S"; }
|
|
||||||
|
|
||||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
repo_root="$(cd -- "${script_dir}/../.." && pwd)"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage:
|
|
||||||
./build_tools/macos/build_sign_notarize.sh # list local signing identities + notary profiles
|
|
||||||
./build_tools/macos/build_sign_notarize.sh --run [options] # build, sign, notarize, staple
|
|
||||||
|
|
||||||
Common options:
|
|
||||||
--run Perform build/sign/notarize (otherwise list identities/profiles)
|
|
||||||
--release | --debug Build config (default: Release)
|
|
||||||
--clean Clean build dir before build
|
|
||||||
--deploy Run CMake 'deploy' target before signing (default: on)
|
|
||||||
--no-deploy Do not run 'deploy' (not recommended for distribution)
|
|
||||||
--dmg Build a DMG after app notarization, then notarize+staple the DMG too
|
|
||||||
--build-dir <path> Override build directory
|
|
||||||
|
|
||||||
Signing options:
|
|
||||||
--identity "<name>" Codesign identity (e.g. "Developer ID Application: Your Name (TEAMID)")
|
|
||||||
--entitlements <plist> Optional entitlements plist for codesign
|
|
||||||
|
|
||||||
Notarization options (recommended):
|
|
||||||
--notary-profile <name> notarytool keychain profile name (created via `xcrun notarytool store-credentials <name> ...`)
|
|
||||||
--skip-notarize Skip notarization
|
|
||||||
|
|
||||||
Outputs:
|
|
||||||
- Signed app: <build-dir>/strawberry.app
|
|
||||||
- Zip for notarization: <build-dir>/strawberry-notarize.zip
|
|
||||||
- DMG (optional): <build-dir>/strawberry-*.dmg
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- This script is intended for Developer ID distribution (outside Mac App Store).
|
|
||||||
- If you want Sparkle updates, you'll typically ship a notarized .zip + an appcast feed.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
list_identities_and_profiles() {
|
|
||||||
echo "==> [$(ts)] macOS code signing identities (Keychain)"
|
|
||||||
security find-identity -p codesigning -v || true
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "==> [$(ts)] notarytool credential profiles"
|
|
||||||
echo "Note: this Xcode notarytool version does not provide a 'list-profiles' command."
|
|
||||||
echo "If you forgot the profile name you created, check Keychain Access or re-run:"
|
|
||||||
echo " xcrun notarytool store-credentials \"<profile-name>\" --apple-id \"you@example.com\" --team-id \"TEAMID\""
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "==> [$(ts)] Provisioning profiles (macOS)"
|
|
||||||
prof_dir="${HOME}/Library/MobileDevice/Provisioning Profiles"
|
|
||||||
if [[ -d "${prof_dir}" ]]; then
|
|
||||||
ls -la "${prof_dir}" | head -n 50
|
|
||||||
else
|
|
||||||
echo "(none found at '${prof_dir}')"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
||||||
echo "Error: This script is for macOS only." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v xcode-select >/dev/null 2>&1 || ! xcode-select -p >/dev/null 2>&1; then
|
|
||||||
echo "Error: Xcode Command Line Tools not found." >&2
|
|
||||||
echo "Install them first: xcode-select --install" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
do_run=0
|
|
||||||
config="Release"
|
|
||||||
do_clean=0
|
|
||||||
do_deploy=1
|
|
||||||
do_dmg=0
|
|
||||||
build_dir=""
|
|
||||||
identity=""
|
|
||||||
entitlements=""
|
|
||||||
notary_profile=""
|
|
||||||
skip_notarize=0
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--run) do_run=1; shift ;;
|
|
||||||
--release) config="Release"; shift ;;
|
|
||||||
--debug) config="Debug"; shift ;;
|
|
||||||
--clean) do_clean=1; shift ;;
|
|
||||||
--deploy) do_deploy=1; shift ;;
|
|
||||||
--no-deploy) do_deploy=0; shift ;;
|
|
||||||
--dmg) do_dmg=1; shift ;;
|
|
||||||
--build-dir) build_dir="${2:-}"; shift 2 ;;
|
|
||||||
--identity) identity="${2:-}"; shift 2 ;;
|
|
||||||
--entitlements) entitlements="${2:-}"; shift 2 ;;
|
|
||||||
--notary-profile) notary_profile="${2:-}"; shift 2 ;;
|
|
||||||
--skip-notarize) skip_notarize=1; shift ;;
|
|
||||||
-h|--help) usage; exit 0 ;;
|
|
||||||
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "$do_run" -eq 0 ]]; then
|
|
||||||
usage
|
|
||||||
echo
|
|
||||||
list_identities_and_profiles
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$build_dir" ]]; then
|
|
||||||
lc_config="$(echo "$config" | tr '[:upper:]' '[:lower:]')"
|
|
||||||
build_dir="${repo_root}/cmake-build-macos-${lc_config}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
app_path="${build_dir}/strawberry.app"
|
|
||||||
bin_path="${app_path}/Contents/MacOS/strawberry"
|
|
||||||
zip_path="${build_dir}/strawberry-notarize.zip"
|
|
||||||
dmg_path=""
|
|
||||||
|
|
||||||
notarize_and_maybe_staple() {
|
|
||||||
local file_path="$1"
|
|
||||||
local label="$2"
|
|
||||||
local do_staple="${3:-1}"
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Notarizing ${label}"
|
|
||||||
local out
|
|
||||||
out="$(mktemp -t notarytool-submit.XXXXXX)"
|
|
||||||
xcrun notarytool submit "$file_path" --keychain-profile "$notary_profile" --wait --output-format plist --no-progress >"$out"
|
|
||||||
|
|
||||||
local submit_id submit_status
|
|
||||||
submit_id="$(/usr/bin/plutil -extract id raw -o - "$out" 2>/dev/null || true)"
|
|
||||||
submit_status="$(/usr/bin/plutil -extract status raw -o - "$out" 2>/dev/null || true)"
|
|
||||||
rm -f "$out" || true
|
|
||||||
|
|
||||||
if [[ -z "$submit_id" ]]; then
|
|
||||||
echo "Error: could not parse notarization submission id for ${label}." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Notary submission id: $submit_id"
|
|
||||||
echo "==> [$(ts)] Notary status: $submit_status"
|
|
||||||
|
|
||||||
if [[ "$submit_status" != "Accepted" ]]; then
|
|
||||||
echo "Error: notarization failed for ${label} with status '$submit_status'. Fetching log..." >&2
|
|
||||||
xcrun notarytool log "$submit_id" --keychain-profile "$notary_profile" || true
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$do_staple" -eq 1 ]]; then
|
|
||||||
echo "==> [$(ts)] Stapling ${label}"
|
|
||||||
xcrun stapler staple "$file_path"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ -z "$identity" ]]; then
|
|
||||||
echo "Error: Missing --identity (Developer ID Application identity)." >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$skip_notarize" -eq 0 && -z "$notary_profile" ]]; then
|
|
||||||
echo "Error: Missing --notary-profile (or pass --skip-notarize)." >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Building Strawberry"
|
|
||||||
build_args=( "--release" )
|
|
||||||
if [[ "$config" == "Debug" ]]; then build_args=( "--debug" ); fi
|
|
||||||
if [[ "$do_clean" -eq 1 ]]; then build_args+=( "--clean" ); fi
|
|
||||||
if [[ -n "$build_dir" ]]; then build_args+=( "--build-dir" "$build_dir" ); fi
|
|
||||||
if [[ "$do_deploy" -eq 1 ]]; then build_args+=( "--deploy" ); fi
|
|
||||||
|
|
||||||
"${repo_root}/build_tools/macos/build_app.sh" "${build_args[@]}"
|
|
||||||
|
|
||||||
if [[ ! -x "$bin_path" ]]; then
|
|
||||||
echo "Error: built app not found at: $app_path" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Codesigning (hardened runtime)"
|
|
||||||
codesign_args=( --force --timestamp --options runtime --sign "$identity" )
|
|
||||||
if [[ -n "$entitlements" ]]; then
|
|
||||||
codesign_args+=( --entitlements "$entitlements" )
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Clean up any leftover codesign temp files from previous interrupted runs.
|
|
||||||
# codesign may create *.cstemp alongside binaries while updating signatures.
|
|
||||||
find "$app_path" -name "*.cstemp" -print0 2>/dev/null | while IFS= read -r -d '' f; do
|
|
||||||
rm -f "$f" || true
|
|
||||||
done
|
|
||||||
|
|
||||||
# Clear macOS provenance/quarantine metadata which can interfere with modifying files in-place.
|
|
||||||
xattr -dr com.apple.provenance "$app_path" >/dev/null 2>&1 || true
|
|
||||||
xattr -dr com.apple.quarantine "$app_path" >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
# Sign nested code first, then frameworks, then the main app bundle.
|
|
||||||
#
|
|
||||||
# Important: do NOT codesign individual files *inside* a .framework bundle (e.g. Sparkle.framework/Sparkle),
|
|
||||||
# because codesign expects frameworks to be signed as bundles and may error with
|
|
||||||
# "bundle format is ambiguous (could be app or framework)".
|
|
||||||
|
|
||||||
# 1) Sign dylibs and standalone executables that are NOT inside a .framework/.app/.xpc bundle.
|
|
||||||
find "$app_path" -type f \( -name "*.dylib" -o -name "*.so" -o -perm -111 \) \
|
|
||||||
! -name "*.cstemp" \
|
|
||||||
! -path "*/Contents/Frameworks/*.framework/*" \
|
|
||||||
! -path "*/Contents/Frameworks/*.app/*" \
|
|
||||||
! -path "*/Contents/Frameworks/*.xpc/*" \
|
|
||||||
! -path "*/Contents/PlugIns/*.framework/*" \
|
|
||||||
! -path "*/Contents/PlugIns/*.app/*" \
|
|
||||||
! -path "*/Contents/PlugIns/*.xpc/*" \
|
|
||||||
-print0 | while IFS= read -r -d '' f; do
|
|
||||||
codesign "${codesign_args[@]}" "$f" >/dev/null
|
|
||||||
done
|
|
||||||
|
|
||||||
# 2) Sign nested helper apps and XPC services (Sparkle ships these inside its framework).
|
|
||||||
find "$app_path" -type d \( -name "*.xpc" -o -name "*.app" \) -print0 2>/dev/null | while IFS= read -r -d '' b; do
|
|
||||||
codesign "${codesign_args[@]}" "$b" >/dev/null
|
|
||||||
done
|
|
||||||
|
|
||||||
# 2b) Sparkle.framework contains a standalone helper executable "Autoupdate" under Versions/* that is
|
|
||||||
# not inside an .app or .xpc bundle. Notarization requires it be signed with Developer ID + timestamp.
|
|
||||||
sparkle_fw="$app_path/Contents/Frameworks/Sparkle.framework"
|
|
||||||
if [[ -d "$sparkle_fw" ]]; then
|
|
||||||
find "$sparkle_fw/Versions" -type f -perm -111 \
|
|
||||||
! -name "*.cstemp" \
|
|
||||||
! -path "*/_CodeSignature/*" \
|
|
||||||
-print0 2>/dev/null | while IFS= read -r -d '' f; do
|
|
||||||
codesign "${codesign_args[@]}" "$f" >/dev/null
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 3) Sign frameworks as bundles.
|
|
||||||
find "$app_path/Contents/Frameworks" "$app_path/Contents/PlugIns" -type d -name "*.framework" -print0 2>/dev/null | while IFS= read -r -d '' fw; do
|
|
||||||
codesign "${codesign_args[@]}" "$fw" >/dev/null
|
|
||||||
done
|
|
||||||
|
|
||||||
# 4) Finally sign the main app.
|
|
||||||
codesign "${codesign_args[@]}" "$app_path" >/dev/null
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Verifying codesign"
|
|
||||||
codesign --verify --deep --strict --verbose=2 "$app_path"
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Creating zip for notarization"
|
|
||||||
rm -f "$zip_path"
|
|
||||||
ditto -c -k --sequesterRsrc --keepParent "$app_path" "$zip_path"
|
|
||||||
|
|
||||||
if [[ "$skip_notarize" -eq 0 ]]; then
|
|
||||||
# ZIP archives cannot be stapled; notarization is still useful for distribution and Sparkle.
|
|
||||||
notarize_and_maybe_staple "$zip_path" "ZIP" 0
|
|
||||||
echo "==> [$(ts)] Stapling app"
|
|
||||||
xcrun stapler staple "$app_path"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$do_dmg" -eq 1 ]]; then
|
|
||||||
echo "==> [$(ts)] Building DMG (from already-signed app; no redeploy)"
|
|
||||||
if ! command -v create-dmg >/dev/null 2>&1; then
|
|
||||||
echo "Error: create-dmg not found. Install it with Homebrew (it's in Brewfile):" >&2
|
|
||||||
echo " ./build_tools/macos/install_brew_deps.sh" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build a versioned DMG name using Info.plist (falls back to Strawberry version constant).
|
|
||||||
plist="${app_path}/Contents/Info.plist"
|
|
||||||
bundle_version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$plist" 2>/dev/null || true)"
|
|
||||||
if [[ -z "${bundle_version}" ]]; then
|
|
||||||
bundle_version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$plist" 2>/dev/null || true)"
|
|
||||||
fi
|
|
||||||
if [[ -z "${bundle_version}" ]]; then
|
|
||||||
bundle_version="unknown"
|
|
||||||
fi
|
|
||||||
arch="$(uname -m)"
|
|
||||||
dmg_path="${build_dir}/strawberry-${bundle_version}-${arch}.dmg"
|
|
||||||
|
|
||||||
rm -f "$dmg_path"
|
|
||||||
(
|
|
||||||
cd "$build_dir"
|
|
||||||
create-dmg \
|
|
||||||
--volname strawberry \
|
|
||||||
--background "${repo_root}/dist/macos/dmg_background.png" \
|
|
||||||
--app-drop-link 450 218 \
|
|
||||||
--icon strawberry.app 150 218 \
|
|
||||||
--window-size 600 450 \
|
|
||||||
"$(basename "$dmg_path")" \
|
|
||||||
strawberry.app
|
|
||||||
)
|
|
||||||
if [[ -z "$dmg_path" ]]; then
|
|
||||||
echo "Error: DMG was not created in $build_dir" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Codesigning DMG"
|
|
||||||
codesign --force --timestamp --sign "$identity" "$dmg_path"
|
|
||||||
|
|
||||||
if [[ "$skip_notarize" -eq 0 ]]; then
|
|
||||||
notarize_and_maybe_staple "$dmg_path" "DMG" 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Gatekeeper assessment"
|
|
||||||
spctl -a -vv --type execute "$app_path" || true
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "Done."
|
|
||||||
echo "App: $app_path"
|
|
||||||
echo "Zip: $zip_path"
|
|
||||||
if [[ -n "${dmg_path}" ]]; then
|
|
||||||
echo "DMG: $dmg_path"
|
|
||||||
fi
|
|
||||||
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# macOS signing identity sanity check for:
|
|
||||||
# - Developer ID (outside Mac App Store)
|
|
||||||
# - Mac App Store (Apple Distribution + 3rd Party Mac Developer Installer)
|
|
||||||
|
|
||||||
ts() { date +"%H:%M:%S"; }
|
|
||||||
|
|
||||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
||||||
echo "Error: This script is for macOS only." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Strawberry macOS signing identity check"
|
|
||||||
echo "==> [$(ts)] Host: $(sw_vers -productName 2>/dev/null || true) $(sw_vers -productVersion 2>/dev/null || true)"
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Keychains searched by 'security' (user)"
|
|
||||||
security list-keychains -d user || true
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Valid code signing identities (must include private key)"
|
|
||||||
codesigning_out="$(security find-identity -p codesigning -v 2>&1 || true)"
|
|
||||||
echo "$codesigning_out"
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Valid installer/pkg identities (must include private key)"
|
|
||||||
basic_out="$(security find-identity -p basic -v 2>&1 || true)"
|
|
||||||
echo "$basic_out"
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Note"
|
|
||||||
cat <<'EOF'
|
|
||||||
- Apple uses multiple certificate types. The "basic" identity list can include certificates that are not usable
|
|
||||||
for signing a Mac App Store upload package.
|
|
||||||
- For App Store Connect uploads via .pkg, you typically need an *Installer* identity (e.g. "3rd Party Mac Developer Installer"
|
|
||||||
or "Mac Installer Distribution") and it must have a private key on this Mac.
|
|
||||||
EOF
|
|
||||||
echo
|
|
||||||
|
|
||||||
list_cert_labels() {
|
|
||||||
local query="$1"
|
|
||||||
# Extract "labl" lines like: "labl"<blob>="Apple Distribution: ..."
|
|
||||||
security find-certificate -a -c "$query" 2>/dev/null \
|
|
||||||
| sed -n 's/.*"labl"<blob>="\(.*\)".*/\1/p' \
|
|
||||||
| sort -u
|
|
||||||
}
|
|
||||||
|
|
||||||
check_label_in_identities() {
|
|
||||||
local label="$1"
|
|
||||||
local out="$2"
|
|
||||||
if echo "$out" | grep -Fq "$label"; then
|
|
||||||
echo "YES"
|
|
||||||
else
|
|
||||||
echo "NO"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
check_label_in_installer_identities() {
|
|
||||||
local label="$1"
|
|
||||||
local out="$2"
|
|
||||||
# Only treat as installer-capable if the cert label itself is an installer cert.
|
|
||||||
case "$label" in
|
|
||||||
*Installer*|*installer*) ;;
|
|
||||||
*) echo "NO"; return 0 ;;
|
|
||||||
esac
|
|
||||||
if echo "$out" | grep -Fq "$label"; then
|
|
||||||
echo "YES"
|
|
||||||
else
|
|
||||||
echo "NO"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
print_section() {
|
|
||||||
local title="$1"
|
|
||||||
shift
|
|
||||||
local queries=("$@")
|
|
||||||
|
|
||||||
echo "==> [$(ts)] ${title}"
|
|
||||||
local any=0
|
|
||||||
|
|
||||||
local q
|
|
||||||
for q in "${queries[@]}"; do
|
|
||||||
local labels
|
|
||||||
labels="$(list_cert_labels "$q" || true)"
|
|
||||||
if [[ -z "$labels" ]]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
any=1
|
|
||||||
while IFS= read -r label; do
|
|
||||||
[[ -z "$label" ]] && continue
|
|
||||||
local in_codesign in_basic
|
|
||||||
in_codesign="$(check_label_in_identities "$label" "$codesigning_out")"
|
|
||||||
in_basic="$(check_label_in_installer_identities "$label" "$basic_out")"
|
|
||||||
printf -- "- %s\n" "$label"
|
|
||||||
printf -- " - codesigning identity: %s\n" "$in_codesign"
|
|
||||||
printf -- " - installer identity: %s\n" "$in_basic"
|
|
||||||
if [[ "$in_codesign" == "NO" && "$in_basic" == "NO" ]]; then
|
|
||||||
printf -- " - note: certificate exists, but it is NOT a usable identity on this Mac (almost always missing private key)\n"
|
|
||||||
fi
|
|
||||||
done <<<"$labels"
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "$any" -eq 0 ]]; then
|
|
||||||
echo "(no matching certificates found)"
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
}
|
|
||||||
|
|
||||||
print_section "Expected for Developer ID (outside Mac App Store)" \
|
|
||||||
"Developer ID Application" \
|
|
||||||
"Developer ID Installer"
|
|
||||||
|
|
||||||
print_section "Expected for Mac App Store submissions" \
|
|
||||||
"Apple Distribution" \
|
|
||||||
"Mac App Distribution" \
|
|
||||||
"3rd Party Mac Developer Application" \
|
|
||||||
"3rd Party Mac Developer Installer" \
|
|
||||||
"Mac Installer Distribution"
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Quick interpretation"
|
|
||||||
cat <<'EOF'
|
|
||||||
- If a certificate label appears above, but both:
|
|
||||||
- codesigning identity: NO
|
|
||||||
- installer identity: NO
|
|
||||||
then the certificate is present but NOT usable for signing on this Mac.
|
|
||||||
The most common cause is: the private key is missing.
|
|
||||||
|
|
||||||
Fix:
|
|
||||||
- Open Keychain Access → login → "My Certificates"
|
|
||||||
- Expand the certificate. You must see a private key underneath it.
|
|
||||||
- If there is no private key:
|
|
||||||
- Recreate the certificate on this Mac via Xcode (Accounts → Manage Certificates), OR
|
|
||||||
- Import a .p12 that includes the private key from the machine where it was created.
|
|
||||||
EOF
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Provisioning profiles (Mac App Store builds require one)"
|
|
||||||
prof_dirs=(
|
|
||||||
"${HOME}/Library/Developer/Xcode/UserData/Provisioning Profiles"
|
|
||||||
"${HOME}/Library/MobileDevice/Provisioning Profiles"
|
|
||||||
)
|
|
||||||
any_prof=0
|
|
||||||
for prof_dir in "${prof_dirs[@]}"; do
|
|
||||||
if [[ -d "${prof_dir}" ]]; then
|
|
||||||
any_prof=1
|
|
||||||
echo " ${prof_dir}"
|
|
||||||
ls -la "${prof_dir}" | head -n 20
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [[ "$any_prof" -eq 0 ]]; then
|
|
||||||
echo "(no provisioning profile directories found in common locations)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Tip: to pick the right MAS profile for a bundle id, run:"
|
|
||||||
echo " ./build_tools/macos/find_mas_provisioning_profile.sh --bundle-id com.dryark.strawberry"
|
|
||||||
\n\necho\n
|
|
||||||
echo "==> [$(ts)] Recommended SHA-1 values to use (avoids ambiguity when names are duplicated)"
|
|
||||||
cat <<'EOF'
|
|
||||||
When you have multiple identities with the same display name, prefer using the SHA-1 hash in scripts:
|
|
||||||
|
|
||||||
--codesign-identity "<SHA1>"
|
|
||||||
--installer-identity "<SHA1>"
|
|
||||||
|
|
||||||
This prevents codesign/productbuild from picking an unexpected identity.
|
|
||||||
EOF
|
|
||||||
echo
|
|
||||||
|
|
||||||
extract_identities() {
|
|
||||||
local policy="$1" # codesigning | basic
|
|
||||||
# Output: SHA1|LABEL
|
|
||||||
security find-identity -p "$policy" -v 2>/dev/null \
|
|
||||||
| sed -n 's/^[[:space:]]*[0-9][0-9]*[)] \([0-9A-F]\{40\}\) "\(.*\)"$/\1|\2/p'
|
|
||||||
}
|
|
||||||
|
|
||||||
print_sha_list() {
|
|
||||||
local title="$1"
|
|
||||||
local policy="$2"
|
|
||||||
local label_match="$3"
|
|
||||||
|
|
||||||
echo "$title"
|
|
||||||
local matches
|
|
||||||
matches="$(extract_identities "$policy" | grep -F "$label_match" || true)"
|
|
||||||
if [[ -z "$matches" ]]; then
|
|
||||||
echo " (none found)"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
local first=1
|
|
||||||
while IFS='|' read -r sha label; do
|
|
||||||
[[ -z "$sha" || -z "$label" ]] && continue
|
|
||||||
if [[ $first -eq 1 ]]; then
|
|
||||||
echo " recommended: $sha ($label)"
|
|
||||||
first=0
|
|
||||||
else
|
|
||||||
echo " alternative: $sha ($label)"
|
|
||||||
fi
|
|
||||||
done <<<"$matches"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_sha_list "Mac App Store (app signing) [use with --codesign-identity]:" "codesigning" "Apple Distribution:"
|
|
||||||
print_sha_list "Mac App Store (pkg signing) [use with --installer-identity]:" "basic" "3rd Party Mac Developer Installer:"
|
|
||||||
print_sha_list "Developer ID (app signing) [outside App Store]:" "codesigning" "Developer ID Application:"
|
|
||||||
print_sha_list "Developer ID (pkg signing) [outside App Store]:" "basic" "Developer ID Installer:"
|
|
||||||
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
Encryption Export Compliance Statement (EAR)
|
|
||||||
|
|
||||||
App Name: Strawberry
|
|
||||||
Bundle ID: com.dryark.strawberry
|
|
||||||
Version: 0.0.0
|
|
||||||
Developer: Dry Ark LLC
|
|
||||||
Date: 2026-01-22
|
|
||||||
Contact: support@example.com
|
|
||||||
|
|
||||||
Statement
|
|
||||||
---------
|
|
||||||
This app uses encryption only for standard network communications security (for example, TLS/HTTPS connections to web services).
|
|
||||||
|
|
||||||
The app does not implement proprietary or non-standard encryption algorithms, and it does not ship its own cryptographic library in place of Apple’s operating system encryption.
|
|
||||||
|
|
||||||
The app uses only encryption provided by Apple’s operating system (e.g., Apple-provided TLS stacks accessed through system frameworks used by the app and its dependencies).
|
|
||||||
|
|
||||||
The app is not a VPN client/server, does not provide end-to-end encrypted messaging, and does not provide user-controlled key management or custom cryptographic functionality beyond standard transport security.
|
|
||||||
|
|
||||||
Accordingly, the app’s use of encryption is believed to qualify as exempt under U.S. export regulations for mass-market software using standard OS-provided encryption.
|
|
||||||
|
|
||||||
Reference
|
|
||||||
---------
|
|
||||||
Apple: Complying with Encryption Export Regulations
|
|
||||||
https://developer.apple.com/documentation/security/complying-with-encryption-export-regulations
|
|
||||||
|
|
||||||
Binary file not shown.
@@ -1,26 +0,0 @@
|
|||||||
Encryption Export Compliance Statement (EAR)
|
|
||||||
|
|
||||||
App Name: Strawberry
|
|
||||||
Bundle ID: @BUNDLE_ID@
|
|
||||||
Version: @VERSION@
|
|
||||||
Developer: @DEVELOPER@
|
|
||||||
Date: @DATE@
|
|
||||||
Contact: @CONTACT@
|
|
||||||
|
|
||||||
Statement
|
|
||||||
---------
|
|
||||||
This app uses encryption only for standard network communications security (for example, TLS/HTTPS connections to web services).
|
|
||||||
|
|
||||||
The app does not implement proprietary or non-standard encryption algorithms, and it does not ship its own cryptographic library in place of Apple’s operating system encryption.
|
|
||||||
|
|
||||||
The app uses only encryption provided by Apple’s operating system (e.g., Apple-provided TLS stacks accessed through system frameworks used by the app and its dependencies).
|
|
||||||
|
|
||||||
The app is not a VPN client/server, does not provide end-to-end encrypted messaging, and does not provide user-controlled key management or custom cryptographic functionality beyond standard transport security.
|
|
||||||
|
|
||||||
Accordingly, the app’s use of encryption is believed to qualify as exempt under U.S. export regulations for mass-market software using standard OS-provided encryption.
|
|
||||||
|
|
||||||
Reference
|
|
||||||
---------
|
|
||||||
Apple: Complying with Encryption Export Regulations
|
|
||||||
https://developer.apple.com/documentation/security/complying-with-encryption-export-regulations
|
|
||||||
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# Export compliance (encryption)
|
|
||||||
|
|
||||||
Apple may require an "Export Compliance" statement upload when submitting to the Mac App Store.
|
|
||||||
|
|
||||||
This folder contains:
|
|
||||||
|
|
||||||
- `EXPORT_COMPLIANCE.txt`: a template statement (fill-in placeholders)
|
|
||||||
- `make_pdf.sh`: a helper to fill the template and generate a PDF you can upload
|
|
||||||
|
|
||||||
## Generate the PDF
|
|
||||||
|
|
||||||
From the repo root:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/export_compliance/make_pdf.sh \
|
|
||||||
--bundle-id com.dryark.strawberry \
|
|
||||||
--version 1.2.3 \
|
|
||||||
--developer "Dry Ark LLC" \
|
|
||||||
--contact "support@example.com"
|
|
||||||
```
|
|
||||||
|
|
||||||
Outputs:
|
|
||||||
|
|
||||||
- `build_tools/macos/export_compliance/EXPORT_COMPLIANCE.filled.txt`
|
|
||||||
- `build_tools/macos/export_compliance/EXPORT_COMPLIANCE.pdf`
|
|
||||||
|
|
||||||
## Important
|
|
||||||
|
|
||||||
This template assumes the app uses **only standard OS-provided encryption** (e.g. TLS/HTTPS via system frameworks) and does **not** ship proprietary or standalone crypto libraries.
|
|
||||||
|
|
||||||
If you bundle your own crypto library (e.g. OpenSSL) or implement custom encryption, you likely need different answers/documentation.
|
|
||||||
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage:
|
|
||||||
./build_tools/macos/export_compliance/make_pdf.sh \
|
|
||||||
--bundle-id com.dryark.strawberry \
|
|
||||||
--version 1.2.3 \
|
|
||||||
--developer "Dry Ark LLC" \
|
|
||||||
--contact "support@example.com"
|
|
||||||
|
|
||||||
Outputs (in the same folder as this script):
|
|
||||||
- EXPORT_COMPLIANCE.filled.txt
|
|
||||||
- EXPORT_COMPLIANCE.pdf
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- Uses macOS built-in /usr/sbin/cupsfilter to generate the PDF from plain text.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
template="${script_dir}/EXPORT_COMPLIANCE.txt"
|
|
||||||
filled="${script_dir}/EXPORT_COMPLIANCE.filled.txt"
|
|
||||||
pdf="${script_dir}/EXPORT_COMPLIANCE.pdf"
|
|
||||||
tmp_html="${script_dir}/EXPORT_COMPLIANCE.tmp.html"
|
|
||||||
|
|
||||||
bundle_id=""
|
|
||||||
version=""
|
|
||||||
developer=""
|
|
||||||
contact=""
|
|
||||||
date_str="$(date +%Y-%m-%d)"
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--bundle-id) bundle_id="${2:-}"; shift 2 ;;
|
|
||||||
--version) version="${2:-}"; shift 2 ;;
|
|
||||||
--developer) developer="${2:-}"; shift 2 ;;
|
|
||||||
--contact) contact="${2:-}"; shift 2 ;;
|
|
||||||
-h|--help) usage; exit 0 ;;
|
|
||||||
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ -z "$bundle_id" || -z "$version" || -z "$developer" || -z "$contact" ]]; then
|
|
||||||
echo "Error: missing required args." >&2
|
|
||||||
usage
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
||||||
echo "Error: This script is for macOS only (uses textutil)." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -f "$template" ]]; then
|
|
||||||
echo "Error: missing template file: $template" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -x /usr/sbin/cupsfilter ]]; then
|
|
||||||
echo "Error: /usr/sbin/cupsfilter not found. This should exist on macOS." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
escape_sed_repl() {
|
|
||||||
# Escape characters that are special in sed replacement strings: \ & and delimiter |
|
|
||||||
# bash 3.2 compatible
|
|
||||||
echo "$1" | sed -e 's/[\\&|]/\\&/g'
|
|
||||||
}
|
|
||||||
|
|
||||||
bundle_id_esc="$(escape_sed_repl "$bundle_id")"
|
|
||||||
version_esc="$(escape_sed_repl "$version")"
|
|
||||||
developer_esc="$(escape_sed_repl "$developer")"
|
|
||||||
contact_esc="$(escape_sed_repl "$contact")"
|
|
||||||
date_esc="$(escape_sed_repl "$date_str")"
|
|
||||||
|
|
||||||
sed \
|
|
||||||
-e "s|@BUNDLE_ID@|${bundle_id_esc}|g" \
|
|
||||||
-e "s|@VERSION@|${version_esc}|g" \
|
|
||||||
-e "s|@DEVELOPER@|${developer_esc}|g" \
|
|
||||||
-e "s|@CONTACT@|${contact_esc}|g" \
|
|
||||||
-e "s|@DATE@|${date_esc}|g" \
|
|
||||||
"$template" > "$filled"
|
|
||||||
|
|
||||||
rm -f "$pdf" >/dev/null 2>&1 || true
|
|
||||||
rm -f "$tmp_html" >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
# Convert plain text to PDF. cupsfilter writes PDF to stdout.
|
|
||||||
# Suppress noisy DEBUG output from cupsfilter on stderr.
|
|
||||||
/usr/sbin/cupsfilter -i text/plain -m application/pdf "$filled" > "$pdf" 2>/dev/null
|
|
||||||
|
|
||||||
echo "Wrote:"
|
|
||||||
echo " $filled"
|
|
||||||
echo " $pdf"
|
|
||||||
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import argparse
|
|
||||||
import datetime as dt
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
|
||||||
|
|
||||||
import plistlib
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ProfileInfo:
|
|
||||||
path: Path
|
|
||||||
uuid: str
|
|
||||||
name: str
|
|
||||||
team_id: str
|
|
||||||
expiration: Optional[dt.datetime]
|
|
||||||
app_id: str
|
|
||||||
platforms: List[str]
|
|
||||||
|
|
||||||
|
|
||||||
def run(cmd: List[str]) -> Tuple[int, bytes, bytes]:
|
|
||||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
out, err = p.communicate()
|
|
||||||
return p.returncode, out, err
|
|
||||||
|
|
||||||
|
|
||||||
def decode_profile_plist_bytes(profile_path: Path) -> Optional[bytes]:
|
|
||||||
# Provisioning profiles are typically CMS/PKCS#7 SignedData blobs whose payload is a plist.
|
|
||||||
# However, some tools store them as plain XML plists already. Also, LibreSSL/OpenSSL behavior
|
|
||||||
# differs: LibreSSL usually requires an explicit '-verify' to emit the embedded content.
|
|
||||||
data = profile_path.read_bytes()
|
|
||||||
|
|
||||||
# Fast path: already a plist (XML).
|
|
||||||
if b"<plist" in data:
|
|
||||||
return data
|
|
||||||
|
|
||||||
# Decode CMS/PKCS7 to extract embedded plist payload.
|
|
||||||
# Try a small matrix of commands/inform formats for compatibility.
|
|
||||||
candidates: List[List[str]] = []
|
|
||||||
for inform in ("DER", "PEM"):
|
|
||||||
candidates.append(["/usr/bin/openssl", "cms", "-verify", "-noverify", "-inform", inform, "-in", str(profile_path)])
|
|
||||||
candidates.append(["/usr/bin/openssl", "smime", "-verify", "-noverify", "-inform", inform, "-in", str(profile_path)])
|
|
||||||
|
|
||||||
for cmd in candidates:
|
|
||||||
rc, out, _err = run(cmd)
|
|
||||||
if rc == 0 and b"<plist" in out:
|
|
||||||
return out
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def parse_plist(plist_bytes: bytes) -> Dict[str, Any]:
|
|
||||||
return plistlib.loads(plist_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
def iso(dt_obj: Optional[dt.datetime]) -> str:
|
|
||||||
if not dt_obj:
|
|
||||||
return "(unknown)"
|
|
||||||
# Force UTC-ish display if tz-aware, otherwise as-is.
|
|
||||||
try:
|
|
||||||
return dt_obj.isoformat().replace("+00:00", "Z")
|
|
||||||
except Exception:
|
|
||||||
return str(dt_obj)
|
|
||||||
|
|
||||||
|
|
||||||
def safe_str(v: Any) -> str:
|
|
||||||
if v is None:
|
|
||||||
return ""
|
|
||||||
if isinstance(v, bytes):
|
|
||||||
try:
|
|
||||||
return v.decode("utf-8", errors="replace")
|
|
||||||
except Exception:
|
|
||||||
return repr(v)
|
|
||||||
return str(v)
|
|
||||||
|
|
||||||
|
|
||||||
def profile_info_from_plist(path: Path, p: Dict[str, Any]) -> ProfileInfo:
|
|
||||||
uuid = safe_str(p.get("UUID", "")) or "(unknown)"
|
|
||||||
name = safe_str(p.get("Name", "")) or "(unknown)"
|
|
||||||
team_ids = p.get("TeamIdentifier") or []
|
|
||||||
team_id = safe_str(team_ids[0]) if isinstance(team_ids, list) and team_ids else ""
|
|
||||||
if not team_id:
|
|
||||||
prefixes = p.get("ApplicationIdentifierPrefix") or []
|
|
||||||
team_id = safe_str(prefixes[0]) if isinstance(prefixes, list) and prefixes else "(unknown)"
|
|
||||||
exp = p.get("ExpirationDate")
|
|
||||||
expiration = exp if isinstance(exp, dt.datetime) else None
|
|
||||||
ent = p.get("Entitlements") or {}
|
|
||||||
app_id = safe_str(ent.get("application-identifier") or ent.get("com.apple.application-identifier") or "") or "(unknown)"
|
|
||||||
platforms = p.get("Platform") or []
|
|
||||||
if isinstance(platforms, str):
|
|
||||||
platforms = [platforms]
|
|
||||||
platforms = [safe_str(x) for x in platforms if x is not None]
|
|
||||||
return ProfileInfo(path=path, uuid=uuid, name=name, team_id=team_id or "(unknown)", expiration=expiration, app_id=app_id, platforms=platforms)
|
|
||||||
|
|
||||||
|
|
||||||
def score(profile: ProfileInfo, bundle_id: str, now: dt.datetime) -> Tuple[int, str]:
|
|
||||||
# Prefer non-expired.
|
|
||||||
if profile.expiration and profile.expiration < now:
|
|
||||||
return (-1, "expired")
|
|
||||||
|
|
||||||
score = 0
|
|
||||||
reason = []
|
|
||||||
|
|
||||||
# Prefer exact app id match TEAMID.bundle_id
|
|
||||||
if profile.team_id != "(unknown)" and profile.app_id != "(unknown)":
|
|
||||||
exact = f"{profile.team_id}.{bundle_id}"
|
|
||||||
if profile.app_id == exact:
|
|
||||||
score += 100
|
|
||||||
reason.append(f"exact {profile.app_id}")
|
|
||||||
elif profile.app_id.endswith(f".{bundle_id}"):
|
|
||||||
score += 60
|
|
||||||
reason.append(f"endswith {profile.app_id}")
|
|
||||||
elif "*" in profile.app_id and profile.app_id.startswith(f"{profile.team_id}."):
|
|
||||||
score += 40
|
|
||||||
reason.append(f"wildcard {profile.app_id}")
|
|
||||||
|
|
||||||
# Heuristic: name suggests MAS.
|
|
||||||
n = profile.name.lower()
|
|
||||||
if "mac app store" in n or "app store" in n or "appstore" in n:
|
|
||||||
score += 5
|
|
||||||
reason.append("name looks like MAS")
|
|
||||||
|
|
||||||
# Prefer macOS platform if present.
|
|
||||||
plats = [p.lower() for p in profile.platforms]
|
|
||||||
if any("macos" in p for p in plats):
|
|
||||||
score += 2
|
|
||||||
reason.append("platform macos")
|
|
||||||
|
|
||||||
return (score, ", ".join(reason) if reason else "")
|
|
||||||
|
|
||||||
|
|
||||||
def find_profiles() -> List[Path]:
|
|
||||||
dirs = [
|
|
||||||
Path.home() / "Library" / "Developer" / "Xcode" / "UserData" / "Provisioning Profiles",
|
|
||||||
Path.home() / "Library" / "MobileDevice" / "Provisioning Profiles",
|
|
||||||
]
|
|
||||||
out: List[Path] = []
|
|
||||||
for d in dirs:
|
|
||||||
if not d.is_dir():
|
|
||||||
continue
|
|
||||||
for p in d.iterdir():
|
|
||||||
if p.is_file() and (p.name.endswith(".provisionprofile") or p.name.endswith(".mobileprovision")):
|
|
||||||
out.append(p)
|
|
||||||
return sorted(out)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
ap = argparse.ArgumentParser()
|
|
||||||
ap.add_argument("--bundle-id", required=True)
|
|
||||||
args = ap.parse_args()
|
|
||||||
bundle_id = args.bundle_id
|
|
||||||
|
|
||||||
if not Path("/usr/bin/openssl").exists():
|
|
||||||
print("Error: /usr/bin/openssl not found.", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
candidates = find_profiles()
|
|
||||||
if not candidates:
|
|
||||||
print("No provisioning profiles found in common locations.", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
print(f"Scanning {len(candidates)} provisioning profile(s) for bundle id: {bundle_id}")
|
|
||||||
print()
|
|
||||||
print(f"{'No.':<4} {'UUID':<36} {'TeamID':<10} {'Expires':<25} {'AppID':<45} Path")
|
|
||||||
print(f"{'-'*4} {'-'*36} {'-'*10} {'-'*25} {'-'*45} ----")
|
|
||||||
|
|
||||||
infos: List[ProfileInfo] = []
|
|
||||||
for i, p in enumerate(candidates, start=1):
|
|
||||||
plist_bytes = decode_profile_plist_bytes(p)
|
|
||||||
if not plist_bytes:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
pl = parse_plist(plist_bytes)
|
|
||||||
info = profile_info_from_plist(p, pl)
|
|
||||||
infos.append(info)
|
|
||||||
print(f"{i:<4} {info.uuid:<36} {info.team_id:<10} {iso(info.expiration):<25} {info.app_id:<45} {info.path}")
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not infos:
|
|
||||||
print("\nCould not decode any provisioning profiles with openssl cms.", file=sys.stderr)
|
|
||||||
return 2
|
|
||||||
|
|
||||||
now = dt.datetime.now(dt.timezone.utc)
|
|
||||||
best: Optional[Tuple[int, str, ProfileInfo]] = None
|
|
||||||
for info in infos:
|
|
||||||
sc, why = score(info, bundle_id, now)
|
|
||||||
if best is None or sc > best[0]:
|
|
||||||
best = (sc, why, info)
|
|
||||||
|
|
||||||
print()
|
|
||||||
if best is None or best[0] <= 0:
|
|
||||||
print(f"Could not confidently auto-select a profile for {bundle_id}.", file=sys.stderr)
|
|
||||||
print("Pick the profile whose AppID is TEAMID.<bundle-id> and is a macOS Mac App Store profile.", file=sys.stderr)
|
|
||||||
return 2
|
|
||||||
|
|
||||||
_, why, info = best
|
|
||||||
print("Recommended profile:")
|
|
||||||
print(f" {info.path}")
|
|
||||||
print(f" reason: {why}")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ts() { date +"%H:%M:%S"; }
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage:
|
|
||||||
./build_tools/macos/find_mas_provisioning_profile.sh --bundle-id com.dryark.strawberry
|
|
||||||
|
|
||||||
What it does:
|
|
||||||
- Scans common macOS provisioning profile locations (new Xcode + legacy)
|
|
||||||
- Uses Apple's `security cms -D` to decode each *.provisionprofile into a plist
|
|
||||||
- Prints a readable table and recommends a best match for the given bundle id
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- A provisioning profile is required for Mac App Store signing.
|
|
||||||
- This script only helps you *find* the right profile file.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
||||||
echo "Error: This script is for macOS only." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
bundle_id=""
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--bundle-id) bundle_id="${2:-}"; shift 2 ;;
|
|
||||||
-h|--help) usage; exit 0 ;;
|
|
||||||
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ -z "$bundle_id" ]]; then
|
|
||||||
echo "Error: missing --bundle-id" >&2
|
|
||||||
usage
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v security >/dev/null 2>&1; then
|
|
||||||
echo "Error: 'security' not found." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
plistbuddy_print() {
|
|
||||||
local keypath="$1"
|
|
||||||
local plist="$2"
|
|
||||||
/usr/libexec/PlistBuddy -c "Print :${keypath}" "$plist" 2>/dev/null || true
|
|
||||||
}
|
|
||||||
|
|
||||||
plutil_extract() {
|
|
||||||
local keypath="$1"
|
|
||||||
local plist="$2"
|
|
||||||
/usr/bin/plutil -extract "$keypath" raw -o - "$plist" 2>/dev/null || true
|
|
||||||
}
|
|
||||||
|
|
||||||
find_profiles_in_dir() {
|
|
||||||
local dir="$1"
|
|
||||||
if [[ -d "$dir" ]]; then
|
|
||||||
find "$dir" -maxdepth 1 -type f \( -name "*.provisionprofile" -o -name "*.mobileprovision" \) 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
declare -a candidates
|
|
||||||
candidates=()
|
|
||||||
while IFS= read -r f; do candidates+=("$f"); done < <(find_profiles_in_dir "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles")
|
|
||||||
while IFS= read -r f; do candidates+=("$f"); done < <(find_profiles_in_dir "$HOME/Library/MobileDevice/Provisioning Profiles")
|
|
||||||
|
|
||||||
if [[ ${#candidates[@]} -eq 0 ]]; then
|
|
||||||
echo "==> [$(ts)] No provisioning profiles found in common locations."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Scanning ${#candidates[@]} provisioning profile(s) for bundle id: ${bundle_id}"
|
|
||||||
echo
|
|
||||||
printf "%-4s %-36s %-10s %-25s %-45s %s\n" "No." "UUID" "TeamID" "Expires" "AppID" "Path"
|
|
||||||
printf "%s\n" "---- ------------------------------------ ---------- ------------------------- --------------------------------------------- ----"
|
|
||||||
|
|
||||||
best_score=-1
|
|
||||||
best_path=""
|
|
||||||
best_reason=""
|
|
||||||
|
|
||||||
idx=0
|
|
||||||
for f in "${candidates[@]}"; do
|
|
||||||
idx=$((idx + 1))
|
|
||||||
|
|
||||||
tmp="$(mktemp -t strawberry-profile.XXXXXX.plist)"
|
|
||||||
if ! security cms -D -i "$f" >"$tmp" 2>/dev/null; then
|
|
||||||
rm -f "$tmp" >/dev/null 2>&1 || true
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
uuid="$(plutil_extract UUID "$tmp")"
|
|
||||||
name="$(plutil_extract Name "$tmp")"
|
|
||||||
teamid="$(plutil_extract 'TeamIdentifier.0' "$tmp")"
|
|
||||||
if [[ -z "$teamid" ]]; then
|
|
||||||
teamid="$(plutil_extract 'ApplicationIdentifierPrefix.0' "$tmp")"
|
|
||||||
fi
|
|
||||||
exp="$(plutil_extract ExpirationDate "$tmp")"
|
|
||||||
|
|
||||||
# App identifier lives under Entitlements; use PlistBuddy because some key names contain dots.
|
|
||||||
appid="$(plistbuddy_print 'Entitlements:application-identifier' "$tmp")"
|
|
||||||
if [[ -z "$appid" ]]; then
|
|
||||||
appid="$(plistbuddy_print 'Entitlements:com.apple.application-identifier' "$tmp")"
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f "$tmp" >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
[[ -z "$uuid" ]] && uuid="(unknown)"
|
|
||||||
[[ -z "$teamid" ]] && teamid="(unknown)"
|
|
||||||
[[ -z "$exp" ]] && exp="(unknown)"
|
|
||||||
[[ -z "$appid" ]] && appid="(unknown)"
|
|
||||||
|
|
||||||
printf "%-4s %-36s %-10s %-25s %-45s %s\n" "$idx" "$uuid" "$teamid" "$exp" "$appid" "$f"
|
|
||||||
|
|
||||||
score=0
|
|
||||||
reason=""
|
|
||||||
|
|
||||||
if [[ "$appid" != "(unknown)" && "$teamid" != "(unknown)" ]]; then
|
|
||||||
if [[ "$appid" == "${teamid}.${bundle_id}" ]]; then
|
|
||||||
score=100
|
|
||||||
reason="exact match (${appid})"
|
|
||||||
elif [[ "$appid" == *".${bundle_id}" ]]; then
|
|
||||||
score=50
|
|
||||||
reason="endswith match (${appid})"
|
|
||||||
elif [[ "$appid" == "${teamid}."* && "$appid" == *"*"* ]]; then
|
|
||||||
score=40
|
|
||||||
reason="wildcard match (${appid})"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$score" -gt 0 && -n "$name" ]]; then
|
|
||||||
case "$name" in
|
|
||||||
*Mac\ App\ Store*|*App\ Store*|*appstore*|*AppStore*)
|
|
||||||
score=$((score + 5))
|
|
||||||
reason="${reason}, name looks like MAS"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$score" -gt "$best_score" ]]; then
|
|
||||||
best_score="$score"
|
|
||||||
best_path="$f"
|
|
||||||
best_reason="$reason"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
if [[ "$best_score" -le 0 ]]; then
|
|
||||||
echo "==> [$(ts)] Could not confidently auto-select a profile for ${bundle_id}."
|
|
||||||
echo "Pick the profile whose AppID is TEAMID.${bundle_id} and is a macOS Mac App Store profile."
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Recommended profile:"
|
|
||||||
echo " $best_path"
|
|
||||||
echo " reason: $best_reason"
|
|
||||||
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
repo_root="$(cd -- "${script_dir}/../.." && pwd)"
|
|
||||||
|
|
||||||
ts() { date +"%H:%M:%S"; }
|
|
||||||
|
|
||||||
run_with_heartbeat() {
|
|
||||||
local desc="$1"
|
|
||||||
shift
|
|
||||||
|
|
||||||
local start now elapsed hb_pid
|
|
||||||
start="$(date +%s)"
|
|
||||||
|
|
||||||
echo "==> [$(ts)] ${desc}"
|
|
||||||
|
|
||||||
# Heartbeat: print elapsed time periodically in case the underlying command is quiet
|
|
||||||
(
|
|
||||||
while true; do
|
|
||||||
sleep 20
|
|
||||||
now="$(date +%s)"
|
|
||||||
elapsed="$((now - start))"
|
|
||||||
echo " [$(ts)] ... still working (${elapsed}s elapsed) ..."
|
|
||||||
done
|
|
||||||
) &
|
|
||||||
hb_pid="$!"
|
|
||||||
|
|
||||||
set +e
|
|
||||||
"$@"
|
|
||||||
local rc=$?
|
|
||||||
set -e
|
|
||||||
|
|
||||||
kill "$hb_pid" >/dev/null 2>&1 || true
|
|
||||||
wait "$hb_pid" >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
now="$(date +%s)"
|
|
||||||
elapsed="$((now - start))"
|
|
||||||
|
|
||||||
if [[ $rc -ne 0 ]]; then
|
|
||||||
echo "Error: '${desc}' failed after ${elapsed}s (exit $rc)." >&2
|
|
||||||
return "$rc"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Done: ${desc} (${elapsed}s)"
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
||||||
echo "Error: This script is for macOS only." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v brew >/dev/null 2>&1; then
|
|
||||||
echo "Error: Homebrew ('brew') not found in PATH." >&2
|
|
||||||
echo "Install Homebrew first: https://brew.sh/" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Brewfile support (`brew bundle`) is built into modern Homebrew. The historical
|
|
||||||
# tap `homebrew/bundle` has been deprecated and may be empty on newer Homebrew.
|
|
||||||
# If `brew bundle` is missing, the fix is to update Homebrew itself.
|
|
||||||
if ! brew bundle --help >/dev/null 2>&1; then
|
|
||||||
run_with_heartbeat "Updating Homebrew (required for 'brew bundle')" bash -lc "brew update"
|
|
||||||
if ! brew bundle --help >/dev/null 2>&1; then
|
|
||||||
echo "Error: This Homebrew installation does not provide 'brew bundle'." >&2
|
|
||||||
echo "Update Homebrew (e.g. 'brew update') or reinstall Homebrew, then re-run this script." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Homebrew taps are git clones; local formula changes must be committed to be visible.
|
|
||||||
if command -v git >/dev/null 2>&1 && git -C "$repo_root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
||||||
if git -C "$repo_root" status --porcelain Formula/ | grep -q .; then
|
|
||||||
echo "Error: You have uncommitted changes under Formula/." >&2
|
|
||||||
echo "Homebrew taps are git clones, so uncommitted formulae won't be visible to 'brew tap'." >&2
|
|
||||||
echo "Commit your changes, then re-run this script." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Optional: disable auto-update for faster, more predictable runs.
|
|
||||||
export HOMEBREW_NO_AUTO_UPDATE="${HOMEBREW_NO_AUTO_UPDATE:-1}"
|
|
||||||
|
|
||||||
cd "$repo_root"
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Using repo: $repo_root"
|
|
||||||
|
|
||||||
# Strawberry includes local Homebrew formulae under Formula/.
|
|
||||||
# Homebrew requires formulae to be in a tap; we tap this repo via file:// and then
|
|
||||||
# update the tap clone to the latest commit (without untapping, since Homebrew may
|
|
||||||
# refuse to untap when formulae from this tap are installed).
|
|
||||||
run_with_heartbeat "Ensuring local tap exists: strawberry/local" bash -lc \
|
|
||||||
"brew tap | grep -q '^strawberry/local$' || brew tap strawberry/local 'file://$repo_root' >/dev/null"
|
|
||||||
|
|
||||||
run_with_heartbeat "Refreshing strawberry/local tap clone" bash -lc '
|
|
||||||
tap_repo="$(brew --repo strawberry/local)"
|
|
||||||
cd "$tap_repo"
|
|
||||||
# Make sure the remote points at the current local repo path.
|
|
||||||
git remote set-url origin "file://'"$repo_root"'"
|
|
||||||
git fetch -q origin
|
|
||||||
default_ref="$(git symbolic-ref -q --short refs/remotes/origin/HEAD || true)"
|
|
||||||
if [ -z "$default_ref" ]; then
|
|
||||||
default_ref="origin/master"
|
|
||||||
fi
|
|
||||||
git reset --hard -q "$default_ref"
|
|
||||||
|
|
||||||
echo "==> [$(date +\"%H:%M:%S\")] strawberry/local tap repo: $tap_repo"
|
|
||||||
echo " tap HEAD: $(git rev-parse --short HEAD)"
|
|
||||||
echo " origin: $(git remote get-url origin)"
|
|
||||||
# If the source repo is a git repo, also print its HEAD for debugging.
|
|
||||||
if git -C "'"$repo_root"'" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
||||||
echo " src HEAD: $(git -C "'"$repo_root"'" rev-parse --short HEAD)"
|
|
||||||
fi
|
|
||||||
'
|
|
||||||
|
|
||||||
for f in kdsingleapplication-qt6 qtsparkle-qt6 sparkle-framework libgpod macdeploycheck; do
|
|
||||||
if ! info_out="$(brew info "strawberry/local/${f}" 2>&1 >/dev/null)"; then
|
|
||||||
echo "Error: Unable to load formula strawberry/local/${f} from the tapped repo (brew info failed)." >&2
|
|
||||||
echo "Details (brew info):" >&2
|
|
||||||
echo "$info_out" >&2
|
|
||||||
echo "If you recently added/changed formulae, ensure they are committed, then refresh the tap:" >&2
|
|
||||||
echo " git -C \"$(brew --repo strawberry/local)\" pull --ff-only" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
run_with_heartbeat "Installing dependencies from Brewfile" \
|
|
||||||
brew bundle install --file "$repo_root/Brewfile" --verbose
|
|
||||||
|
|
||||||
cat <<EOF
|
|
||||||
|
|
||||||
Done.
|
|
||||||
|
|
||||||
Notes for packaging (optional):
|
|
||||||
- The CMake target 'deploy' expects these env vars for bundling GIO + GStreamer bits:
|
|
||||||
export GIO_EXTRA_MODULES="\$(brew --prefix)/lib/gio/modules"
|
|
||||||
export GST_PLUGIN_SCANNER="\$(brew --prefix gstreamer)/libexec/gstreamer-1.0/gst-plugin-scanner"
|
|
||||||
export GST_PLUGIN_PATH="\$(brew --prefix)/lib/gstreamer-1.0"
|
|
||||||
|
|
||||||
EOF
|
|
||||||
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Create a universal (arm64+x86_64) .app by merging two already-deployed app bundles
|
|
||||||
# that have identical layouts, one built on Apple Silicon and one built on Intel.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./build_tools/macos/make_universal_app.sh \
|
|
||||||
# --arm-app /path/to/arm64/strawberry.app \
|
|
||||||
# --x86-app /path/to/x86_64/strawberry.app \
|
|
||||||
# --out-app /path/to/output/strawberry.app \
|
|
||||||
# [--clean]
|
|
||||||
#
|
|
||||||
# Notes:
|
|
||||||
# - Do NOT sign the per-arch apps first; signatures will be invalidated by lipo anyway.
|
|
||||||
# - Both inputs must be the same app version/config with the same enabled features,
|
|
||||||
# so the file lists match.
|
|
||||||
|
|
||||||
ts() { date +"%H:%M:%S"; }
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage:
|
|
||||||
./build_tools/macos/make_universal_app.sh --arm-app <path> --x86-app <path> --out-app <path> [--clean]
|
|
||||||
|
|
||||||
What it does:
|
|
||||||
- Copies the arm64 app to --out-app
|
|
||||||
- For every Mach-O file in the copied app, finds the corresponding file in the x86_64 app
|
|
||||||
- Uses lipo to combine the two slices into a universal binary at the same relative path
|
|
||||||
|
|
||||||
Required:
|
|
||||||
--arm-app <path> Path to arm64 Strawberry.app (built+deployed on Apple Silicon)
|
|
||||||
--x86-app <path> Path to x86_64 Strawberry.app (built+deployed on Intel)
|
|
||||||
--out-app <path> Output path for universal Strawberry.app
|
|
||||||
|
|
||||||
Optional:
|
|
||||||
--clean Delete --out-app if it already exists
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
||||||
echo "Error: This script is for macOS only." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
arm_app=""
|
|
||||||
x86_app=""
|
|
||||||
out_app=""
|
|
||||||
do_clean=0
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--arm-app) arm_app="${2:-}"; shift 2 ;;
|
|
||||||
--x86-app) x86_app="${2:-}"; shift 2 ;;
|
|
||||||
--out-app) out_app="${2:-}"; shift 2 ;;
|
|
||||||
--clean) do_clean=1; shift ;;
|
|
||||||
-h|--help) usage; exit 0 ;;
|
|
||||||
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ -z "$arm_app" || -z "$x86_app" || -z "$out_app" ]]; then
|
|
||||||
echo "Error: missing required args." >&2
|
|
||||||
usage
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
if [[ ! -d "$arm_app" ]]; then
|
|
||||||
echo "Error: --arm-app not found: $arm_app" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
if [[ ! -d "$x86_app" ]]; then
|
|
||||||
echo "Error: --x86-app not found: $x86_app" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
for cmd in /usr/bin/file /usr/bin/lipo /usr/bin/ditto; do
|
|
||||||
if [[ ! -x "$cmd" ]]; then
|
|
||||||
echo "Error: required tool not found: $cmd" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
out_parent="$(cd -- "$(dirname -- "$out_app")" && pwd)"
|
|
||||||
out_name="$(basename -- "$out_app")"
|
|
||||||
out_app="${out_parent}/${out_name}"
|
|
||||||
|
|
||||||
if [[ -e "$out_app" && "$do_clean" -eq 1 ]]; then
|
|
||||||
echo "==> [$(ts)] Removing existing output app: $out_app"
|
|
||||||
rm -rf "$out_app"
|
|
||||||
fi
|
|
||||||
if [[ -e "$out_app" ]]; then
|
|
||||||
echo "Error: output already exists: $out_app (use --clean to overwrite)" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Copying arm64 app to output"
|
|
||||||
/usr/bin/ditto "$arm_app" "$out_app"
|
|
||||||
|
|
||||||
# Remove any existing signatures in the copied app; we'll re-sign after creating universal binaries.
|
|
||||||
echo "==> [$(ts)] Removing existing code signature metadata (will be re-signed later)"
|
|
||||||
find "$out_app" -type d -name "_CodeSignature" -print0 2>/dev/null | while IFS= read -r -d '' d; do
|
|
||||||
rm -rf "$d" || true
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Merging Mach-O files with lipo"
|
|
||||||
|
|
||||||
merged=0
|
|
||||||
skipped=0
|
|
||||||
|
|
||||||
# Traverse output app and lipo-merge any Mach-O file with its counterpart in the x86 app.
|
|
||||||
while IFS= read -r -d '' f; do
|
|
||||||
# Only operate on regular files.
|
|
||||||
if [[ ! -f "$f" ]]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
ft="$(/usr/bin/file -b "$f" 2>/dev/null || true)"
|
|
||||||
if [[ "$ft" != *"Mach-O"* ]]; then
|
|
||||||
skipped=$((skipped + 1))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
rel="${f#"$out_app"/}"
|
|
||||||
other="${x86_app}/${rel}"
|
|
||||||
if [[ ! -f "$other" ]]; then
|
|
||||||
echo "Error: missing matching file in x86 app for:" >&2
|
|
||||||
echo " $rel" >&2
|
|
||||||
echo "Expected at:" >&2
|
|
||||||
echo " $other" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
other_ft="$(/usr/bin/file -b "$other" 2>/dev/null || true)"
|
|
||||||
if [[ "$other_ft" != *"Mach-O"* ]]; then
|
|
||||||
echo "Error: file is Mach-O in arm app but not Mach-O in x86 app:" >&2
|
|
||||||
echo " $rel" >&2
|
|
||||||
echo "arm64: $ft" >&2
|
|
||||||
echo "x86_64: $other_ft" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Validate architectures.
|
|
||||||
arm_archs="$(/usr/bin/lipo -archs "$f" 2>/dev/null || true)"
|
|
||||||
x86_archs="$(/usr/bin/lipo -archs "$other" 2>/dev/null || true)"
|
|
||||||
if [[ "$arm_archs" != *"arm64"* ]]; then
|
|
||||||
echo "Error: expected arm64 slice in arm app file:" >&2
|
|
||||||
echo " $rel" >&2
|
|
||||||
echo " archs: $arm_archs" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ "$x86_archs" != *"x86_64"* ]]; then
|
|
||||||
echo "Error: expected x86_64 slice in x86 app file:" >&2
|
|
||||||
echo " $rel" >&2
|
|
||||||
echo " archs: $x86_archs" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
tmp="$(mktemp -t strawberry-universal.XXXXXX)"
|
|
||||||
/usr/bin/lipo -create "$f" "$other" -output "$tmp"
|
|
||||||
chmod --reference="$f" "$tmp" 2>/dev/null || true
|
|
||||||
mv -f "$tmp" "$f"
|
|
||||||
merged=$((merged + 1))
|
|
||||||
done < <(find "$out_app" -type f -print0 2>/dev/null)
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Done"
|
|
||||||
echo "Merged Mach-O files: $merged"
|
|
||||||
echo "Non-Mach-O files skipped: $skipped"
|
|
||||||
echo "Output app:"
|
|
||||||
echo " $out_app"
|
|
||||||
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ts() { date +"%H:%M:%S"; }
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage:
|
|
||||||
./build_tools/macos/print_mas_build_cmd.sh [--bundle-id com.dryark.strawberry] [--profile <path>]
|
|
||||||
|
|
||||||
What it does:
|
|
||||||
- Tries to auto-pick a provisioning profile for the bundle id
|
|
||||||
- Prints an exact build command you can copy/paste for build_mas_pkg.sh
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- This helper intentionally does NOT try to auto-pick signing identities by parsing Apple tool output.
|
|
||||||
Use SHA-1 identities from:
|
|
||||||
./build_tools/macos/check_signing_identities.sh
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
||||||
echo "Error: This script is for macOS only." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
repo_root="$(cd -- "${script_dir}/../.." && pwd)"
|
|
||||||
|
|
||||||
bundle_id="com.dryark.strawberry"
|
|
||||||
profile_path=""
|
|
||||||
codesign_identity=""
|
|
||||||
installer_identity=""
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--bundle-id) bundle_id="${2:-}"; shift 2 ;;
|
|
||||||
--profile) profile_path="${2:-}"; shift 2 ;;
|
|
||||||
--codesign-identity) codesign_identity="${2:-}"; shift 2 ;;
|
|
||||||
--installer-identity) installer_identity="${2:-}"; shift 2 ;;
|
|
||||||
-h|--help) usage; exit 0 ;;
|
|
||||||
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ -z "$profile_path" ]]; then
|
|
||||||
# Attempt to auto-select profile using the finder script.
|
|
||||||
finder="${repo_root}/build_tools/macos/find_mas_provisioning_profile.sh"
|
|
||||||
if [[ -x "$finder" ]]; then
|
|
||||||
out="$("$finder" --bundle-id "$bundle_id" 2>/dev/null || true)"
|
|
||||||
# Parse the line after "Recommended profile:"
|
|
||||||
profile_path="$(printf '%s\n' "$out" | awk 'found{print $1; exit} /^Recommended profile:/{found=1} found && $0 ~ /^ \\// {print $1; exit}' | sed 's/^[[:space:]]*//')"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Recommended build command:"
|
|
||||||
echo
|
|
||||||
echo "./build_tools/macos/build_mas_pkg.sh --run --release --clean \\"
|
|
||||||
echo " --codesign-identity \"${codesign_identity:-<SHA1 from check_signing_identities.sh>}\" \\"
|
|
||||||
echo " --installer-identity \"${installer_identity:-<SHA1 from check_signing_identities.sh>}\" \\"
|
|
||||||
if [[ -n "$profile_path" ]]; then
|
|
||||||
echo " --provisionprofile \"${profile_path}\""
|
|
||||||
else
|
|
||||||
echo " --provisionprofile \"</path/to/profile.provisionprofile>\""
|
|
||||||
echo
|
|
||||||
echo "Note: could not auto-pick a provisioning profile for bundle id '${bundle_id}'."
|
|
||||||
echo "Run:"
|
|
||||||
echo " ./build_tools/macos/find_mas_provisioning_profile.sh --bundle-id ${bundle_id}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
@@ -1,104 +1,43 @@
|
|||||||
# NOTE: Packaging helpers should not be REQUIRED at configure time.
|
find_program(MACDEPLOYQT_EXECUTABLE NAMES macdeployqt PATHS /usr/bin /usr/local/bin /opt/local/bin /usr/local/opt/qt6/bin REQUIRED)
|
||||||
# Missing tools should simply disable the related custom targets.
|
|
||||||
find_program(MACDEPLOYQT_EXECUTABLE NAMES macdeployqt PATHS /usr/bin /usr/local/bin /opt/local/bin /usr/local/opt/qt6/bin)
|
|
||||||
if(MACDEPLOYQT_EXECUTABLE)
|
if(MACDEPLOYQT_EXECUTABLE)
|
||||||
message(STATUS "Found macdeployqt: ${MACDEPLOYQT_EXECUTABLE}")
|
message(STATUS "Found macdeployqt: ${MACDEPLOYQT_EXECUTABLE}")
|
||||||
else()
|
else()
|
||||||
message(WARNING "Missing macdeployqt executable.")
|
message(WARNING "Missing macdeployqt executable.")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
find_program(MACDEPLOYCHECK_EXECUTABLE NAMES macdeploycheck PATHS /usr/bin /usr/local/bin /opt/local/bin /usr/local/opt/qt6/bin)
|
find_program(MACDEPLOYCHECK_EXECUTABLE NAMES macdeploycheck PATHS /usr/bin /usr/local/bin /opt/local/bin /usr/local/opt/qt6/bin REQUIRED)
|
||||||
if(MACDEPLOYCHECK_EXECUTABLE)
|
if(MACDEPLOYCHECK_EXECUTABLE)
|
||||||
message(STATUS "Found macdeploycheck: ${MACDEPLOYCHECK_EXECUTABLE}")
|
message(STATUS "Found macdeploycheck: ${MACDEPLOYCHECK_EXECUTABLE}")
|
||||||
else()
|
else()
|
||||||
message(STATUS "macdeploycheck not found (optional): 'deploycheck' target will be unavailable.")
|
message(WARNING "Missing macdeploycheck executable.")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
find_program(CREATEDMG_EXECUTABLE NAMES create-dmg)
|
find_program(CREATEDMG_EXECUTABLE NAMES create-dmg REQUIRED)
|
||||||
if(CREATEDMG_EXECUTABLE)
|
if(CREATEDMG_EXECUTABLE)
|
||||||
message(STATUS "Found create-dmg: ${CREATEDMG_EXECUTABLE}")
|
message(STATUS "Found create-dmg: ${CREATEDMG_EXECUTABLE}")
|
||||||
else()
|
else()
|
||||||
message(WARNING "Missing create-dmg executable.")
|
message(WARNING "Missing create-dmg executable.")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
set(_SPARKLE_FRAMEWORK_DIR "")
|
|
||||||
set(_SPARKLE_ORIGINAL_BIN_LINK "")
|
|
||||||
set(_SPARKLE_ORIGINAL_BIN_REAL "")
|
|
||||||
if(SPARKLE)
|
|
||||||
# SPARKLE may be either the framework directory or the framework binary path.
|
|
||||||
get_filename_component(_sparkle_link "${SPARKLE}" ABSOLUTE)
|
|
||||||
get_filename_component(_sparkle_real "${SPARKLE}" REALPATH)
|
|
||||||
if(_sparkle_link MATCHES "Sparkle\\.framework$")
|
|
||||||
set(_SPARKLE_FRAMEWORK_DIR "${_sparkle_real}")
|
|
||||||
set(_SPARKLE_ORIGINAL_BIN_LINK "${_sparkle_link}/Versions/B/Sparkle")
|
|
||||||
set(_SPARKLE_ORIGINAL_BIN_REAL "${_sparkle_real}/Versions/B/Sparkle")
|
|
||||||
else()
|
|
||||||
# Assume it's the framework binary path:
|
|
||||||
# .../Sparkle.framework/Versions/B/Sparkle
|
|
||||||
set(_SPARKLE_ORIGINAL_BIN_LINK "${_sparkle_link}")
|
|
||||||
set(_SPARKLE_ORIGINAL_BIN_REAL "${_sparkle_real}")
|
|
||||||
get_filename_component(_sparkle_b_dir "${_SPARKLE_ORIGINAL_BIN_REAL}" DIRECTORY) # .../Versions/B
|
|
||||||
get_filename_component(_sparkle_versions_dir "${_sparkle_b_dir}" DIRECTORY) # .../Versions
|
|
||||||
get_filename_component(_SPARKLE_FRAMEWORK_DIR "${_sparkle_versions_dir}" DIRECTORY) # .../Sparkle.framework
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(NOT EXISTS "${_SPARKLE_FRAMEWORK_DIR}" OR NOT EXISTS "${_SPARKLE_ORIGINAL_BIN_REAL}")
|
|
||||||
set(_SPARKLE_FRAMEWORK_DIR "")
|
|
||||||
set(_SPARKLE_ORIGINAL_BIN_LINK "")
|
|
||||||
set(_SPARKLE_ORIGINAL_BIN_REAL "")
|
|
||||||
else()
|
|
||||||
message(STATUS "Sparkle.framework found: ${_SPARKLE_FRAMEWORK_DIR}")
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(MACDEPLOYQT_EXECUTABLE)
|
if(MACDEPLOYQT_EXECUTABLE)
|
||||||
|
|
||||||
# Note: We intentionally do NOT codesign during the CMake 'deploy'/'dmg' targets.
|
if(APPLE_DEVELOPER_ID)
|
||||||
# macdeployqt can optionally sign, but passing identities safely through Ninja's /bin/sh wrapper is brittle.
|
set(MACDEPLOYQT_CODESIGN -codesign=${APPLE_DEVELOPER_ID})
|
||||||
# This repo's signing/notarization pipeline is handled in build_tools/macos/build_sign_notarize.sh instead.
|
set(CREATEDMG_CODESIGN --codesign ${APPLE_DEVELOPER_ID})
|
||||||
|
endif()
|
||||||
if(CREATEDMG_SKIP_JENKINS)
|
if(CREATEDMG_SKIP_JENKINS)
|
||||||
set(CREATEDMG_SKIP_JENKINS_ARG "--skip-jenkins")
|
set(CREATEDMG_SKIP_JENKINS_ARG "--skip-jenkins")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
set(_deploy_commands
|
add_custom_target(deploy
|
||||||
COMMAND mkdir -p ${CMAKE_BINARY_DIR}/strawberry.app/Contents/Frameworks
|
COMMAND mkdir -p ${CMAKE_BINARY_DIR}/strawberry.app/Contents/{Frameworks,Resources}
|
||||||
COMMAND mkdir -p ${CMAKE_BINARY_DIR}/strawberry.app/Contents/Resources
|
|
||||||
COMMAND cp -v ${CMAKE_BINARY_DIR}/dist/macos/Info.plist ${CMAKE_BINARY_DIR}/strawberry.app/Contents/
|
COMMAND cp -v ${CMAKE_BINARY_DIR}/dist/macos/Info.plist ${CMAKE_BINARY_DIR}/strawberry.app/Contents/
|
||||||
COMMAND cp -v ${CMAKE_SOURCE_DIR}/dist/macos/strawberry.icns ${CMAKE_BINARY_DIR}/strawberry.app/Contents/Resources/
|
COMMAND cp -v ${CMAKE_SOURCE_DIR}/dist/macos/strawberry.icns ${CMAKE_BINARY_DIR}/strawberry.app/Contents/Resources/
|
||||||
)
|
|
||||||
|
|
||||||
if(_SPARKLE_FRAMEWORK_DIR)
|
|
||||||
list(APPEND _deploy_commands
|
|
||||||
COMMAND ${CMAKE_SOURCE_DIR}/dist/macos/bundle_sparkle.sh ${CMAKE_BINARY_DIR}/strawberry.app ${_SPARKLE_FRAMEWORK_DIR} ${_SPARKLE_ORIGINAL_BIN_LINK} ${_SPARKLE_ORIGINAL_BIN_REAL}
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
list(APPEND _deploy_commands
|
|
||||||
COMMAND ${CMAKE_SOURCE_DIR}/dist/macos/macgstcopy.sh ${CMAKE_BINARY_DIR}/strawberry.app
|
COMMAND ${CMAKE_SOURCE_DIR}/dist/macos/macgstcopy.sh ${CMAKE_BINARY_DIR}/strawberry.app
|
||||||
COMMAND ${MACDEPLOYQT_EXECUTABLE} strawberry.app -verbose=3 -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/gst-plugin-scanner
|
COMMAND ${MACDEPLOYQT_EXECUTABLE} strawberry.app -verbose=3 -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/gst-plugin-scanner ${MACDEPLOYQT_CODESIGN}
|
||||||
)
|
|
||||||
|
|
||||||
# Make 'deploy' incremental:
|
|
||||||
# - add_custom_target() is always out-of-date, so it reruns every time.
|
|
||||||
# - using a stamp file makes Ninja/Make skip deploy when inputs haven't changed.
|
|
||||||
set(_deploy_stamp "${CMAKE_BINARY_DIR}/deploy_app_bundle.stamp")
|
|
||||||
|
|
||||||
add_custom_command(
|
|
||||||
OUTPUT "${_deploy_stamp}"
|
|
||||||
${_deploy_commands}
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E touch "${_deploy_stamp}"
|
|
||||||
COMMENT "Deploying app bundle (bundling Sparkle/GStreamer + macdeployqt)"
|
|
||||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||||
DEPENDS
|
DEPENDS strawberry
|
||||||
strawberry
|
|
||||||
"${CMAKE_BINARY_DIR}/dist/macos/Info.plist"
|
|
||||||
"${CMAKE_SOURCE_DIR}/dist/macos/strawberry.icns"
|
|
||||||
"${CMAKE_SOURCE_DIR}/dist/macos/macgstcopy.sh"
|
|
||||||
"${CMAKE_SOURCE_DIR}/dist/macos/bundle_sparkle.sh"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
add_custom_target(deploy DEPENDS "${_deploy_stamp}")
|
|
||||||
|
|
||||||
if(MACDEPLOYCHECK_EXECUTABLE)
|
if(MACDEPLOYCHECK_EXECUTABLE)
|
||||||
add_custom_target(deploycheck
|
add_custom_target(deploycheck
|
||||||
COMMAND ${MACDEPLOYCHECK_EXECUTABLE} strawberry.app
|
COMMAND ${MACDEPLOYCHECK_EXECUTABLE} strawberry.app
|
||||||
@@ -106,9 +45,8 @@ if(MACDEPLOYQT_EXECUTABLE)
|
|||||||
endif()
|
endif()
|
||||||
if(CREATEDMG_EXECUTABLE)
|
if(CREATEDMG_EXECUTABLE)
|
||||||
add_custom_target(dmg
|
add_custom_target(dmg
|
||||||
COMMAND ${CREATEDMG_EXECUTABLE} --volname strawberry --background "${CMAKE_SOURCE_DIR}/dist/macos/dmg_background.png" --app-drop-link 450 218 --icon strawberry.app 150 218 --window-size 600 450 ${CREATEDMG_SKIP_JENKINS_ARG} strawberry-${STRAWBERRY_VERSION_PACKAGE}-${CMAKE_HOST_SYSTEM_PROCESSOR}.dmg strawberry.app
|
COMMAND ${CREATEDMG_EXECUTABLE} --volname strawberry --background "${CMAKE_SOURCE_DIR}/dist/macos/dmg_background.png" --app-drop-link 450 218 --icon strawberry.app 150 218 --window-size 600 450 ${CREATEDMG_CODESIGN} ${CREATEDMG_SKIP_JENKINS_ARG} strawberry-${STRAWBERRY_VERSION_PACKAGE}-${CMAKE_HOST_SYSTEM_PROCESSOR}.dmg strawberry.app
|
||||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||||
DEPENDS deploy
|
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
# Try to find RapidJSON (header-only).
|
|
||||||
#
|
|
||||||
# This project uses `find_package(RapidJSON)` and expects:
|
|
||||||
# - RapidJSON_FOUND
|
|
||||||
# - RapidJSON_INCLUDE_DIRS
|
|
||||||
#
|
|
||||||
# Homebrew's `rapidjson` formula commonly installs headers to:
|
|
||||||
# /opt/homebrew/include/rapidjson
|
|
||||||
# but does not always ship a `RapidJSONConfig.cmake`, so we provide this
|
|
||||||
# Find-module fallback.
|
|
||||||
|
|
||||||
find_path(RapidJSON_INCLUDE_DIR
|
|
||||||
NAMES rapidjson/document.h
|
|
||||||
)
|
|
||||||
|
|
||||||
include(FindPackageHandleStandardArgs)
|
|
||||||
find_package_handle_standard_args(RapidJSON
|
|
||||||
REQUIRED_VARS RapidJSON_INCLUDE_DIR
|
|
||||||
)
|
|
||||||
|
|
||||||
if(RapidJSON_FOUND)
|
|
||||||
set(RapidJSON_INCLUDE_DIRS "${RapidJSON_INCLUDE_DIR}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(RapidJSON_FOUND AND NOT TARGET RapidJSON::RapidJSON)
|
|
||||||
add_library(RapidJSON::RapidJSON INTERFACE IMPORTED)
|
|
||||||
set_target_properties(RapidJSON::RapidJSON PROPERTIES
|
|
||||||
INTERFACE_INCLUDE_DIRECTORIES "${RapidJSON_INCLUDE_DIR}"
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
@@ -1,23 +1,6 @@
|
|||||||
set(summary_willbuild "")
|
set(summary_willbuild "")
|
||||||
set(summary_willnotbuild "")
|
set(summary_willnotbuild "")
|
||||||
|
|
||||||
# On some platforms (notably macOS via Homebrew), many "optional" dependencies are
|
|
||||||
# not installed by default. Historically, Strawberry treated missing optional deps
|
|
||||||
# as a hard error when the option defaulted to ON, which makes first-time builds
|
|
||||||
# frustrating.
|
|
||||||
#
|
|
||||||
# This toggle controls that behavior:
|
|
||||||
# - ON => missing optional deps abort the configure (packager/CI-friendly)
|
|
||||||
# - OFF => missing optional deps auto-disable the component (dev-friendly)
|
|
||||||
set(_optional_components_fatal_default ON)
|
|
||||||
if(APPLE)
|
|
||||||
set(_optional_components_fatal_default OFF)
|
|
||||||
endif()
|
|
||||||
option(OPTIONAL_COMPONENTS_MISSING_DEPS_ARE_FATAL
|
|
||||||
"If ON, missing optional component dependencies are fatal (otherwise components auto-disable)"
|
|
||||||
${_optional_components_fatal_default}
|
|
||||||
)
|
|
||||||
|
|
||||||
macro(optional_component_summary_add name test)
|
macro(optional_component_summary_add name test)
|
||||||
if (${test})
|
if (${test})
|
||||||
list(APPEND summary_willbuild ${name})
|
list(APPEND summary_willbuild ${name})
|
||||||
@@ -97,13 +80,8 @@ function(optional_component name default description)
|
|||||||
set(text "${description} (missing ${deplist_text})")
|
set(text "${description} (missing ${deplist_text})")
|
||||||
|
|
||||||
set(summary_willnotbuild "${summary_willnotbuild};${text}" PARENT_SCOPE)
|
set(summary_willnotbuild "${summary_willnotbuild};${text}" PARENT_SCOPE)
|
||||||
if(OPTIONAL_COMPONENTS_MISSING_DEPS_ARE_FATAL)
|
|
||||||
message(FATAL_ERROR "${text}, to disable this optional feature, pass -D${option_variable}=OFF to CMake")
|
message(FATAL_ERROR "${text}, to disable this optional feature, pass -D${option_variable}=OFF to CMake")
|
||||||
else()
|
|
||||||
message(STATUS "${text} - disabling ${option_variable}")
|
|
||||||
set(${option_variable} OFF CACHE BOOL "${description}" FORCE)
|
|
||||||
return()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
else()
|
else()
|
||||||
set(${have_variable} ON PARENT_SCOPE)
|
set(${have_variable} ON PARENT_SCOPE)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
set(STRAWBERRY_VERSION_MAJOR 1)
|
set(STRAWBERRY_VERSION_MAJOR 1)
|
||||||
set(STRAWBERRY_VERSION_MINOR 2)
|
set(STRAWBERRY_VERSION_MINOR 2)
|
||||||
set(STRAWBERRY_VERSION_PATCH 17)
|
set(STRAWBERRY_VERSION_PATCH 16)
|
||||||
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
|
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
|
||||||
|
|
||||||
set(INCLUDE_GIT_REVISION ON)
|
set(INCLUDE_GIT_REVISION ON)
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
tool="${1:-}"
|
|
||||||
shift || true
|
|
||||||
|
|
||||||
if [[ -z "$tool" ]]; then
|
|
||||||
echo "qt_tool_wrapper.sh: missing tool argument" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
base="$(basename "$tool")"
|
|
||||||
|
|
||||||
# Qt LinguistTools (lrelease) prints some noisy informational lines to stderr that
|
|
||||||
# are not actionable during normal builds (e.g. "Removed plural forms...").
|
|
||||||
# We filter only those specific messages.
|
|
||||||
if [[ "$base" == "lrelease" ]]; then
|
|
||||||
"$tool" "$@" 2>&1 | sed \
|
|
||||||
-e '/^Removed plural forms as the target language has less forms\.$/d' \
|
|
||||||
-e '/^If this sounds wrong, possibly the target language is not set or recognized\.$/d'
|
|
||||||
exit "${PIPESTATUS[0]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec "$tool" "$@"
|
|
||||||
|
|
||||||
20
dist/CMakeLists.txt
vendored
20
dist/CMakeLists.txt
vendored
@@ -9,28 +9,8 @@ if(APPLE)
|
|||||||
else()
|
else()
|
||||||
set(LSMinimumSystemVersion 12.0)
|
set(LSMinimumSystemVersion 12.0)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(BUILD_FOR_MAC_APP_STORE)
|
|
||||||
# MAS builds must not embed Sparkle update configuration in Info.plist.
|
|
||||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.mas.plist.in ${CMAKE_CURRENT_BINARY_DIR}/macos/Info.plist)
|
|
||||||
else()
|
|
||||||
# Sparkle (macOS updates)
|
|
||||||
# These values are embedded into Info.plist and control where the app checks for updates.
|
|
||||||
# Downstream builders can override on the CMake command line:
|
|
||||||
# -DSPARKLE_FEED_URL="https://example.com/appcast.xml"
|
|
||||||
# -DSPARKLE_PUBLIC_ED25519_KEY="base64=="
|
|
||||||
#
|
|
||||||
# Defaults preserve upstream behavior, but are intentionally configurable for third-party builds.
|
|
||||||
if(NOT DEFINED SPARKLE_FEED_URL)
|
|
||||||
set(SPARKLE_FEED_URL "https://www.strawberrymusicplayer.org/sparkle-macos-@ARCH@")
|
|
||||||
endif()
|
|
||||||
if(NOT DEFINED SPARKLE_PUBLIC_ED25519_KEY)
|
|
||||||
set(SPARKLE_PUBLIC_ED25519_KEY "/OydhYVfypuO2Mf7G6DUqVZWW9G19eFV74qaDCBTOUk=")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist.in ${CMAKE_CURRENT_BINARY_DIR}/macos/Info.plist)
|
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist.in ${CMAKE_CURRENT_BINARY_DIR}/macos/Info.plist)
|
||||||
endif()
|
endif()
|
||||||
endif()
|
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/windows/windres.rc.in ${CMAKE_BINARY_DIR}/windres.rc)
|
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/windows/windres.rc.in ${CMAKE_BINARY_DIR}/windres.rc)
|
||||||
|
|||||||
253
dist/macos/Info.mas.plist.in
vendored
253
dist/macos/Info.mas.plist.in
vendored
@@ -1,253 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>NSPrincipalClass</key>
|
|
||||||
<string>NSApplication</string>
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
|
||||||
<string>English</string>
|
|
||||||
<key>CFBundleExecutable</key>
|
|
||||||
<string>strawberry</string>
|
|
||||||
<key>CFBundleGetInfoString</key>
|
|
||||||
<string>Strawberry ${STRAWBERRY_VERSION_DISPLAY}</string>
|
|
||||||
<key>CFBundleIconFile</key>
|
|
||||||
<string>strawberry.icns</string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>@MACOS_BUNDLE_ID@</string>
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
|
||||||
<string>6.0</string>
|
|
||||||
<key>CFBundleLongVersionString</key>
|
|
||||||
<string>${STRAWBERRY_VERSION_DISPLAY}</string>
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>Strawberry</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>APPL</string>
|
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>${STRAWBERRY_VERSION_DISPLAY}</string>
|
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>${STRAWBERRY_VERSION_PACKAGE}</string>
|
|
||||||
<key>CSResourcesFileMapped</key>
|
|
||||||
<true/>
|
|
||||||
<key>LSRequiresCarbon</key>
|
|
||||||
<true/>
|
|
||||||
<key>LSApplicationCategoryType</key>
|
|
||||||
<string>public.app-category.music</string>
|
|
||||||
<key>LSMinimumSystemVersion</key>
|
|
||||||
<string>@LSMinimumSystemVersion@</string>
|
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
|
||||||
<false/>
|
|
||||||
|
|
||||||
<key>CFBundleURLTypes</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
<key>CFBundleURLName</key>
|
|
||||||
<string>@MACOS_BUNDLE_ID@</string>
|
|
||||||
<key>CFBundleURLSchemes</key>
|
|
||||||
<array>
|
|
||||||
<string>tidal</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
|
|
||||||
<key>CFBundleDocumentTypes</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeOSTypes</key>
|
|
||||||
<array>
|
|
||||||
<string>****</string>
|
|
||||||
<string>fold</string>
|
|
||||||
<string>disk</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeExtensions</key>
|
|
||||||
<array>
|
|
||||||
<string>xspf</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeIconFile</key>
|
|
||||||
<string>Generic.icns</string>
|
|
||||||
<key>CFBundleTypeMIMETypes</key>
|
|
||||||
<array>
|
|
||||||
<string>application/xspf+xml</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeName</key>
|
|
||||||
<string>XSPF Playlist</string>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeExtensions</key>
|
|
||||||
<array>
|
|
||||||
<string>wav</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeMIMETypes</key>
|
|
||||||
<array>
|
|
||||||
<string>audio/x-wav</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeName</key>
|
|
||||||
<string>WAVE Audio File</string>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeExtensions</key>
|
|
||||||
<array>
|
|
||||||
<string>pls</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeIconFile</key>
|
|
||||||
<string>pls.icns</string>
|
|
||||||
<key>CFBundleTypeName</key>
|
|
||||||
<string>Shoutcast playlist</string>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeExtensions</key>
|
|
||||||
<array>
|
|
||||||
<string>m3u</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeIconFile</key>
|
|
||||||
<string>m3u.icns</string>
|
|
||||||
<key>CFBundleTypeMIMETypes</key>
|
|
||||||
<array>
|
|
||||||
<string>audio/x-mpegurl</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeName</key>
|
|
||||||
<string>Playlist file</string>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeExtensions</key>
|
|
||||||
<array>
|
|
||||||
<string>aac</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeIconFile</key>
|
|
||||||
<string>mpeg4.icns</string>
|
|
||||||
<key>CFBundleTypeName</key>
|
|
||||||
<string>AAC file</string>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeExtensions</key>
|
|
||||||
<array>
|
|
||||||
<string>ogg</string>
|
|
||||||
<string>ogx</string>
|
|
||||||
<string>ogm</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeIconFile</key>
|
|
||||||
<string>ogg.icns</string>
|
|
||||||
<key>CFBundleTypeMIMETypes</key>
|
|
||||||
<array>
|
|
||||||
<string>audio/ogg</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeName</key>
|
|
||||||
<string>Ogg Vorbis File</string>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeExtensions</key>
|
|
||||||
<array>
|
|
||||||
<string>oga</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeIconFile</key>
|
|
||||||
<string>ogg.icns</string>
|
|
||||||
<key>CFBundleTypeMIMETypes</key>
|
|
||||||
<array>
|
|
||||||
<string>audio/ogg</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeName</key>
|
|
||||||
<string>Ogg Audio File</string>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeExtensions</key>
|
|
||||||
<array>
|
|
||||||
<string>wma</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeIconFile</key>
|
|
||||||
<string>wma.icns</string>
|
|
||||||
<key>CFBundleTypeName</key>
|
|
||||||
<string>WIndows Media Audio</string>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeExtensions</key>
|
|
||||||
<array>
|
|
||||||
<string>mp3</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeIconFile</key>
|
|
||||||
<string>mp3.icns</string>
|
|
||||||
<key>CFBundleTypeMIMETypes</key>
|
|
||||||
<array>
|
|
||||||
<string>audio/mpeg</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeName</key>
|
|
||||||
<string>MPEG Audio Layer 3</string>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeExtensions</key>
|
|
||||||
<array>
|
|
||||||
<string>3gp</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeIconFile</key>
|
|
||||||
<string>generic.icns</string>
|
|
||||||
<key>CFBundleTypeName</key>
|
|
||||||
<string>3GPP File</string>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeExtensions</key>
|
|
||||||
<array>
|
|
||||||
<string>m4a</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeIconFile</key>
|
|
||||||
<string>mpeg4.icns</string>
|
|
||||||
<key>CFBundleTypeName</key>
|
|
||||||
<string>MPEG-4 Audio File</string>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeExtensions</key>
|
|
||||||
<array>
|
|
||||||
<string>mpc</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeIconFile</key>
|
|
||||||
<string>generic.icns</string>
|
|
||||||
<key>CFBundleTypeName</key>
|
|
||||||
<string>Musepack Audio File</string>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeExtensions</key>
|
|
||||||
<array>
|
|
||||||
<string>flac</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeIconFile</key>
|
|
||||||
<string>generic.icns</string>
|
|
||||||
<key>CFBundleTypeMIMETypes</key>
|
|
||||||
<array>
|
|
||||||
<string>audio/flac</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeName</key>
|
|
||||||
<string>FLAC Audio File</string>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
|
|
||||||
15
dist/macos/Info.plist.in
vendored
15
dist/macos/Info.plist.in
vendored
@@ -13,7 +13,7 @@
|
|||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>strawberry.icns</string>
|
<string>strawberry.icns</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>@MACOS_BUNDLE_ID@</string>
|
<string>org.strawberrymusicplayer.strawberry</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleLongVersionString</key>
|
<key>CFBundleLongVersionString</key>
|
||||||
@@ -34,24 +34,17 @@
|
|||||||
<string>public.app-category.music</string>
|
<string>public.app-category.music</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>@LSMinimumSystemVersion@</string>
|
<string>@LSMinimumSystemVersion@</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
|
||||||
<false/>
|
|
||||||
<!-- Default to manual update checks unless the user explicitly enables automatic checking. -->
|
|
||||||
<key>SUEnableAutomaticChecks</key>
|
|
||||||
<false/>
|
|
||||||
<key>SUAutomaticallyUpdate</key>
|
|
||||||
<false/>
|
|
||||||
<key>SUFeedURL</key>
|
<key>SUFeedURL</key>
|
||||||
<string>@SPARKLE_FEED_URL@</string>
|
<string>https://www.strawberrymusicplayer.org/sparkle-macos-@ARCH@</string>
|
||||||
<key>SUPublicEDKey</key>
|
<key>SUPublicEDKey</key>
|
||||||
<string>@SPARKLE_PUBLIC_ED25519_KEY@</string>
|
<string>/OydhYVfypuO2Mf7G6DUqVZWW9G19eFV74qaDCBTOUk=</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleTypeRole</key>
|
<key>CFBundleTypeRole</key>
|
||||||
<string>Viewer</string>
|
<string>Viewer</string>
|
||||||
<key>CFBundleURLName</key>
|
<key>CFBundleURLName</key>
|
||||||
<string>@MACOS_BUNDLE_ID@</string>
|
<string>org.strawberrymusicplayer.strawberry</string>
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>tidal</string>
|
<string>tidal</string>
|
||||||
|
|||||||
85
dist/macos/bundle_sparkle.sh
vendored
85
dist/macos/bundle_sparkle.sh
vendored
@@ -1,85 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
bundledir="${1:-}"
|
|
||||||
sparkle_framework_dir="${2:-}"
|
|
||||||
sparkle_bin_link="${3:-}"
|
|
||||||
sparkle_bin_real="${4:-}"
|
|
||||||
|
|
||||||
if [[ -z "$bundledir" || -z "$sparkle_framework_dir" ]]; then
|
|
||||||
echo "Usage: $0 <bundledir> <sparkle_framework_dir> [sparkle_bin_link] [sparkle_bin_real]" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -d "$sparkle_framework_dir" ]]; then
|
|
||||||
echo "Sparkle.framework dir not found: $sparkle_framework_dir" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
src_framework_dir="$sparkle_framework_dir"
|
|
||||||
|
|
||||||
# Homebrew often provides /opt/homebrew/Frameworks/Sparkle.framework where Versions/* are symlinks
|
|
||||||
# pointing back into the Cellar. Copying that verbatim breaks inside an app bundle.
|
|
||||||
# Resolve to the real Cellar framework root via Versions/Current.
|
|
||||||
if [[ -e "${sparkle_framework_dir}/Versions/Current" ]]; then
|
|
||||||
current_real="$(cd "${sparkle_framework_dir}/Versions/Current" && pwd -P)"
|
|
||||||
# current_real is .../Sparkle.framework/Versions/B (or similar)
|
|
||||||
src_framework_dir="$(cd "${current_real}/../.." && pwd -P)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
dst_framework="${bundledir}/Contents/Frameworks/Sparkle.framework"
|
|
||||||
main_bin="${bundledir}/Contents/MacOS/strawberry"
|
|
||||||
qtsparkle_dylib="${bundledir}/Contents/Frameworks/libqtsparkle-qt6.dylib"
|
|
||||||
|
|
||||||
mkdir -p "${bundledir}/Contents/Frameworks"
|
|
||||||
|
|
||||||
echo "Bundling Sparkle.framework -> ${dst_framework}"
|
|
||||||
rm -rf "${dst_framework}"
|
|
||||||
# Use ditto to preserve the framework's internal symlinks/structure.
|
|
||||||
ditto "${src_framework_dir}" "${dst_framework}"
|
|
||||||
|
|
||||||
# Prefer the canonical framework binary path.
|
|
||||||
dst_bin="${dst_framework}/Versions/Current/Sparkle"
|
|
||||||
if [[ ! -e "${dst_bin}" ]]; then
|
|
||||||
echo "Error: Sparkle binary missing at ${dst_bin}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
sparkle_rpath="@rpath/Sparkle.framework/Versions/Current/Sparkle"
|
|
||||||
|
|
||||||
# Sanity check: top-level Sparkle entry should be a symlink (not a copied Mach-O file).
|
|
||||||
if [[ -e "${dst_framework}/Sparkle" && ! -L "${dst_framework}/Sparkle" ]]; then
|
|
||||||
echo "Warning: ${dst_framework}/Sparkle is not a symlink (unexpected). This can confuse codesign." >&2
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Fixing Sparkle.framework install name"
|
|
||||||
install_name_tool -id "${sparkle_rpath}" "${dst_bin}"
|
|
||||||
|
|
||||||
echo "Ensuring main binary has Frameworks rpath"
|
|
||||||
install_name_tool -add_rpath "@executable_path/../Frameworks" "${main_bin}" || true
|
|
||||||
|
|
||||||
echo "Rewriting Sparkle.framework references to @rpath"
|
|
||||||
# Try to rewrite a few common Homebrew Sparkle install names as well, because the
|
|
||||||
# recorded install name may differ from the path returned by CMake's find_library.
|
|
||||||
old_candidates=(
|
|
||||||
"${sparkle_bin_link}"
|
|
||||||
"${sparkle_bin_real}"
|
|
||||||
"/opt/homebrew/opt/sparkle-framework/Frameworks/Sparkle.framework/Versions/A/Sparkle"
|
|
||||||
"/opt/homebrew/opt/sparkle-framework/Frameworks/Sparkle.framework/Versions/B/Sparkle"
|
|
||||||
"/opt/homebrew/Frameworks/Sparkle.framework/Versions/A/Sparkle"
|
|
||||||
"/opt/homebrew/Frameworks/Sparkle.framework/Versions/B/Sparkle"
|
|
||||||
"/usr/local/opt/sparkle-framework/Frameworks/Sparkle.framework/Versions/A/Sparkle"
|
|
||||||
"/usr/local/opt/sparkle-framework/Frameworks/Sparkle.framework/Versions/B/Sparkle"
|
|
||||||
"/usr/local/Frameworks/Sparkle.framework/Versions/A/Sparkle"
|
|
||||||
"/usr/local/Frameworks/Sparkle.framework/Versions/B/Sparkle"
|
|
||||||
)
|
|
||||||
|
|
||||||
for old in "${old_candidates[@]}"; do
|
|
||||||
if [[ -n "${old}" ]]; then
|
|
||||||
install_name_tool -change "${old}" "${sparkle_rpath}" "${main_bin}" || true
|
|
||||||
if [[ -f "${qtsparkle_dylib}" ]]; then
|
|
||||||
install_name_tool -change "${old}" "${sparkle_rpath}" "${qtsparkle_dylib}" || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
20
dist/macos/entitlements.mas.plist
vendored
20
dist/macos/entitlements.mas.plist
vendored
@@ -1,20 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<!-- Enable the App Sandbox (required for Mac App Store). -->
|
|
||||||
<key>com.apple.security.app-sandbox</key>
|
|
||||||
<true/>
|
|
||||||
|
|
||||||
<!-- Strawberry is a client app that needs outbound network access (streaming/scrobbling/etc). -->
|
|
||||||
<key>com.apple.security.network.client</key>
|
|
||||||
<true/>
|
|
||||||
|
|
||||||
<!-- Allow access to user-selected music folders/files (via NSOpenPanel security-scoped bookmarks). -->
|
|
||||||
<key>com.apple.security.files.user-selected.read-write</key>
|
|
||||||
<true/>
|
|
||||||
|
|
||||||
<!-- If iPod classic / other device access is rejected, we'll adjust entitlements after App Review feedback. -->
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
|
|
||||||
73
dist/macos/macdeploycheck.sh
vendored
73
dist/macos/macdeploycheck.sh
vendored
@@ -1,73 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# macdeploycheck: sanity check a deployed macOS .app bundle for accidental runtime deps
|
|
||||||
# on Homebrew/MacPorts paths (which break distribution / App Store / notarization).
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# macdeploycheck /path/to/App.app
|
|
||||||
|
|
||||||
app="${1:-}"
|
|
||||||
if [[ -z "$app" ]]; then
|
|
||||||
echo "Usage: macdeploycheck <path/to/App.app>" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
if [[ ! -d "$app" ]]; then
|
|
||||||
echo "Error: app bundle not found: $app" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
if [[ ! -d "$app/Contents" ]]; then
|
|
||||||
echo "Error: not a macOS app bundle (missing Contents/): $app" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
fail=0
|
|
||||||
tmp="$(mktemp -t macdeploycheck.XXXXXX)"
|
|
||||||
trap 'rm -f "$tmp"' EXIT
|
|
||||||
|
|
||||||
# Collect Mach-O files (executables + dylibs) inside the bundle.
|
|
||||||
while IFS= read -r -d '' f; do
|
|
||||||
if file "$f" | grep -q "Mach-O"; then
|
|
||||||
echo "$f" >>"$tmp"
|
|
||||||
fi
|
|
||||||
done < <(find "$app/Contents" -type f \( -perm -111 -o -name "*.dylib" -o -name "*.so" \) -print0 2>/dev/null)
|
|
||||||
|
|
||||||
if [[ ! -s "$tmp" ]]; then
|
|
||||||
echo "Warning: no Mach-O files found under $app/Contents" >&2
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "macdeploycheck: scanning for external (Homebrew/MacPorts) runtime deps..."
|
|
||||||
while IFS= read -r f; do
|
|
||||||
deps="$(otool -L "$f" 2>/dev/null | tail -n +2 | awk '{print $1}' || true)"
|
|
||||||
while IFS= read -r dep; do
|
|
||||||
[[ -z "$dep" ]] && continue
|
|
||||||
|
|
||||||
# Ignore system and rpath/loader/executable paths.
|
|
||||||
case "$dep" in
|
|
||||||
/System/*|/usr/lib/*|@rpath/*|@loader_path/*|@executable_path/*) continue ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Common accidental runtime deps that will break distribution.
|
|
||||||
if [[ "$dep" == /opt/homebrew/* || "$dep" == /usr/local/* || "$dep" == /opt/local/* ]]; then
|
|
||||||
echo "ERROR: $f links to external path: $dep" >&2
|
|
||||||
fail=1
|
|
||||||
fi
|
|
||||||
done <<<"$deps"
|
|
||||||
done <"$tmp"
|
|
||||||
|
|
||||||
if [[ "$fail" -ne 0 ]]; then
|
|
||||||
cat >&2 <<'EOM'
|
|
||||||
|
|
||||||
One or more binaries in your .app link to a Homebrew (or MacPorts) path.
|
|
||||||
That usually means the bundle is not self-contained and will fail on other machines.
|
|
||||||
|
|
||||||
Fix: re-run your deploy step (e.g. macdeployqt) so frameworks/dylibs are bundled and
|
|
||||||
their install names are rewritten to @rpath/@loader_path.
|
|
||||||
EOM
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "OK: no external Homebrew/MacPorts runtime deps detected."
|
|
||||||
exit 0
|
|
||||||
|
|
||||||
80
dist/macos/privacy_policy.html
vendored
80
dist/macos/privacy_policy.html
vendored
@@ -1,80 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<title>Strawberry Music Player — Privacy Policy</title>
|
|
||||||
<style>
|
|
||||||
:root { color-scheme: light dark; }
|
|
||||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; margin: 24px; line-height: 1.45; }
|
|
||||||
main { max-width: 900px; margin: 0 auto; }
|
|
||||||
code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
|
||||||
h1,h2 { line-height: 1.15; }
|
|
||||||
.muted { opacity: 0.75; }
|
|
||||||
ul { padding-left: 20px; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<main>
|
|
||||||
<h1>Privacy Policy</h1>
|
|
||||||
<p class="muted">Last updated: 2026-01-22</p>
|
|
||||||
|
|
||||||
<h2>Summary</h2>
|
|
||||||
<ul>
|
|
||||||
<li><strong>No analytics / tracking</strong>: This app does not include advertising SDKs, analytics SDKs, or tracking pixels.</li>
|
|
||||||
<li><strong>No data selling</strong>: We do not sell personal data.</li>
|
|
||||||
<li><strong>Optional online features</strong>: If you enable online features (lyrics lookup, cover art search, scrobbling, streaming services, radio, update checks), the app will contact third-party services and send the minimum data needed to provide the feature.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>What data is stored on your device</h2>
|
|
||||||
<p>Strawberry stores data locally on your device, such as:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Library database and playlists (file paths, track metadata, play counts, ratings).</li>
|
|
||||||
<li>App settings and preferences.</li>
|
|
||||||
<li>Optional service credentials/tokens you configure (for example scrobbling or streaming accounts), stored locally.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>What data is sent over the network (and when)</h2>
|
|
||||||
<p>Strawberry does not “phone home” just to run, but it will make network requests when you use or enable specific features. When the app contacts a third-party service, that service will receive standard network information such as your IP address, user-agent, and the request data described below.</p>
|
|
||||||
|
|
||||||
<h3>Album cover art search (optional)</h3>
|
|
||||||
<p>If you use album cover search (or enable “search automatically”), the app may send artist/album/track metadata to configured cover providers to find images.</p>
|
|
||||||
|
|
||||||
<h3>Lyrics lookup (optional)</h3>
|
|
||||||
<p>If you search for lyrics (or enable “search automatically”), the app may send artist/title/album/duration to configured lyrics providers to retrieve lyrics.</p>
|
|
||||||
|
|
||||||
<h3>Scrobbling (optional)</h3>
|
|
||||||
<p>If you enable scrobbling (for example Last.fm or ListenBrainz) the app sends “now playing” and/or listen history data to the configured scrobbling service, including track/artist/album metadata and timestamps. You can disable scrobbling at any time in Settings.</p>
|
|
||||||
|
|
||||||
<h3>Streaming services (optional)</h3>
|
|
||||||
<p>If you enable and sign into a streaming service (for example Tidal, Spotify, Qobuz, Subsonic-compatible servers), the app will communicate with that service to authenticate, browse, and play music. Requests may include account identifiers/tokens and media metadata required by the service.</p>
|
|
||||||
|
|
||||||
<h3>Internet radio (optional)</h3>
|
|
||||||
<p>If you use internet radio features, the app will contact the selected station/provider to retrieve station lists and stream audio.</p>
|
|
||||||
|
|
||||||
<h3>Discord Rich Presence (optional)</h3>
|
|
||||||
<p>If you enable Discord Rich Presence, the app shares currently playing track/artist/album information with the locally-running Discord client so it can be displayed on your Discord profile. You can disable this in Settings.</p>
|
|
||||||
|
|
||||||
<h3>Software updates</h3>
|
|
||||||
<p>The Mac App Store version of Strawberry is updated through Apple’s App Store.</p>
|
|
||||||
|
|
||||||
<h3>OAuth / local redirect server (optional)</h3>
|
|
||||||
<p>Some providers use an OAuth login flow that may open your browser and (in some cases) start a temporary local <code>http://localhost</code> redirect listener to complete authentication. This listener is local-only (not exposed on the internet) and only used during authentication. (Mac App Store builds may disable this flow.)</p>
|
|
||||||
|
|
||||||
<h2>Data sharing</h2>
|
|
||||||
<p>We do not share your personal data with third parties except as necessary to provide features you explicitly use or enable (for example, sending track metadata to a lyrics provider when you request lyrics).</p>
|
|
||||||
|
|
||||||
<h2>Your choices</h2>
|
|
||||||
<ul>
|
|
||||||
<li>Disable Discord Rich Presence in Settings.</li>
|
|
||||||
<li>Disable scrobbling services in Settings.</li>
|
|
||||||
<li>Disable automatic cover/lyrics searching in Settings.</li>
|
|
||||||
<li>Avoid signing into streaming services if you don’t want those network requests.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>Contact</h2>
|
|
||||||
<p>If you have questions about this policy, contact: <strong>privacy@dryark.com</strong></p>
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
@@ -51,7 +51,6 @@
|
|||||||
</screenshots>
|
</screenshots>
|
||||||
<update_contact>eclipseo@fedoraproject.org</update_contact>
|
<update_contact>eclipseo@fedoraproject.org</update_contact>
|
||||||
<releases>
|
<releases>
|
||||||
<release version="1.2.17" date="2026-01-18"/>
|
|
||||||
<release version="1.2.16" date="2025-12-16"/>
|
<release version="1.2.16" date="2025-12-16"/>
|
||||||
<release version="1.2.15" date="2025-11-25"/>
|
<release version="1.2.15" date="2025-11-25"/>
|
||||||
<release version="1.2.14" date="2025-10-25"/>
|
<release version="1.2.14" date="2025-10-25"/>
|
||||||
|
|||||||
101
org.freedesktop.Avahi.EntryGroup.xml
Normal file
101
org.freedesktop.Avahi.EntryGroup.xml
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<?xml version="1.0" standalone='no'?><!--*-nxml-*-->
|
||||||
|
<?xml-stylesheet type="text/xsl" href="introspect.xsl"?>
|
||||||
|
<!DOCTYPE node SYSTEM "introspect.dtd">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
This file is part of avahi.
|
||||||
|
|
||||||
|
avahi is free software; you can redistribute it and/or modify it
|
||||||
|
under the terms of the GNU Lesser General Public License as
|
||||||
|
published by the Free Software Foundation; either version 2 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
avahi is distributed in the hope that it will be useful, but
|
||||||
|
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with avahi; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||||
|
02111-1307 USA.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<node>
|
||||||
|
|
||||||
|
<interface name="org.freedesktop.DBus.Introspectable">
|
||||||
|
<method name="Introspect">
|
||||||
|
<arg name="data" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
</interface>
|
||||||
|
|
||||||
|
<interface name="org.freedesktop.Avahi.EntryGroup">
|
||||||
|
<method name="Free"/>
|
||||||
|
<method name="Commit"/>
|
||||||
|
<method name="Reset"/>
|
||||||
|
|
||||||
|
<method name="GetState">
|
||||||
|
<arg name="state" type="i" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<signal name="StateChanged">
|
||||||
|
<arg name="state" type="i"/>
|
||||||
|
<arg name="error" type="s"/>
|
||||||
|
</signal>
|
||||||
|
|
||||||
|
<method name="IsEmpty">
|
||||||
|
<arg name="empty" type="b" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="AddService">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="type" type="s" direction="in"/>
|
||||||
|
<arg name="domain" type="s" direction="in"/>
|
||||||
|
<arg name="host" type="s" direction="in"/>
|
||||||
|
<arg name="port" type="q" direction="in"/>
|
||||||
|
<arg name="txt" type="aay" direction="in"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="AddServiceSubtype">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="type" type="s" direction="in"/>
|
||||||
|
<arg name="domain" type="s" direction="in"/>
|
||||||
|
<arg name="subtype" type="s" direction="in"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="UpdateServiceTxt">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="type" type="s" direction="in"/>
|
||||||
|
<arg name="domain" type="s" direction="in"/>
|
||||||
|
<arg name="txt" type="aay" direction="in"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="AddAddress">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="address" type="s" direction="in"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="AddRecord">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="clazz" type="q" direction="in"/>
|
||||||
|
<arg name="type" type="q" direction="in"/>
|
||||||
|
<arg name="ttl" type="u" direction="in"/>
|
||||||
|
<arg name="rdata" type="ay" direction="in"/>
|
||||||
|
</method>
|
||||||
|
</interface>
|
||||||
|
</node>
|
||||||
405
org.freedesktop.Avahi.Server.xml
Normal file
405
org.freedesktop.Avahi.Server.xml
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
<?xml version="1.0" standalone='no'?><!--*-nxml-*-->
|
||||||
|
<?xml-stylesheet type="text/xsl" href="introspect.xsl"?>
|
||||||
|
<!DOCTYPE node SYSTEM "introspect.dtd">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
This file is part of avahi.
|
||||||
|
|
||||||
|
avahi is free software; you can redistribute it and/or modify it
|
||||||
|
under the terms of the GNU Lesser General Public License as
|
||||||
|
published by the Free Software Foundation; either version 2 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
avahi is distributed in the hope that it will be useful, but
|
||||||
|
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with avahi; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||||
|
02111-1307 USA.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<node>
|
||||||
|
|
||||||
|
<interface name="org.freedesktop.DBus.Introspectable">
|
||||||
|
<method name="Introspect">
|
||||||
|
<arg name="data" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
</interface>
|
||||||
|
|
||||||
|
<interface name="org.freedesktop.Avahi.Server">
|
||||||
|
|
||||||
|
<method name="GetVersionString">
|
||||||
|
<arg name="version" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="GetAPIVersion">
|
||||||
|
<arg name="version" type="u" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="GetHostName">
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
<method name="SetHostName">
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
</method>
|
||||||
|
<method name="GetHostNameFqdn">
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
<method name="GetDomainName">
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="IsNSSSupportAvailable">
|
||||||
|
<arg name="yes" type="b" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="GetState">
|
||||||
|
<arg name="state" type="i" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<signal name="StateChanged">
|
||||||
|
<arg name="state" type="i"/>
|
||||||
|
<arg name="error" type="s"/>
|
||||||
|
</signal>
|
||||||
|
|
||||||
|
<method name="GetLocalServiceCookie">
|
||||||
|
<arg name="cookie" type="u" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="GetAlternativeHostName">
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="GetAlternativeServiceName">
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="GetNetworkInterfaceNameByIndex">
|
||||||
|
<arg name="index" type="i" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
<method name="GetNetworkInterfaceIndexByName">
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="index" type="i" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="ResolveHostName">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="interface" type="i" direction="out"/>
|
||||||
|
<arg name="protocol" type="i" direction="out"/>
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="out"/>
|
||||||
|
<arg name="address" type="s" direction="out"/>
|
||||||
|
<arg name="flags" type="u" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="ResolveAddress">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="address" type="s" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="interface" type="i" direction="out"/>
|
||||||
|
<arg name="protocol" type="i" direction="out"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="out"/>
|
||||||
|
<arg name="address" type="s" direction="out"/>
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
<arg name="flags" type="u" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="ResolveService">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="type" type="s" direction="in"/>
|
||||||
|
<arg name="domain" type="s" direction="in"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="interface" type="i" direction="out"/>
|
||||||
|
<arg name="protocol" type="i" direction="out"/>
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
<arg name="type" type="s" direction="out"/>
|
||||||
|
<arg name="domain" type="s" direction="out"/>
|
||||||
|
<arg name="host" type="s" direction="out"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="out"/>
|
||||||
|
<arg name="address" type="s" direction="out"/>
|
||||||
|
<arg name="port" type="q" direction="out"/>
|
||||||
|
<arg name="txt" type="aay" direction="out"/>
|
||||||
|
<arg name="flags" type="u" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="EntryGroupNew">
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="DomainBrowserNew">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="domain" type="s" direction="in"/>
|
||||||
|
<arg name="btype" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="ServiceTypeBrowserNew">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="domain" type="s" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="ServiceBrowserNew">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="type" type="s" direction="in"/>
|
||||||
|
<arg name="domain" type="s" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="ServiceResolverNew">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="type" type="s" direction="in"/>
|
||||||
|
<arg name="domain" type="s" direction="in"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="HostNameResolverNew">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="AddressResolverNew">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="address" type="s" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="RecordBrowserNew">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="clazz" type="q" direction="in"/>
|
||||||
|
<arg name="type" type="q" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
</interface>
|
||||||
|
|
||||||
|
<interface name="org.freedesktop.Avahi.Server2">
|
||||||
|
|
||||||
|
<method name="GetVersionString">
|
||||||
|
<arg name="version" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="GetAPIVersion">
|
||||||
|
<arg name="version" type="u" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="GetHostName">
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
<method name="SetHostName">
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
</method>
|
||||||
|
<method name="GetHostNameFqdn">
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
<method name="GetDomainName">
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="IsNSSSupportAvailable">
|
||||||
|
<arg name="yes" type="b" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="GetState">
|
||||||
|
<arg name="state" type="i" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<signal name="StateChanged">
|
||||||
|
<arg name="state" type="i"/>
|
||||||
|
<arg name="error" type="s"/>
|
||||||
|
</signal>
|
||||||
|
|
||||||
|
<method name="GetLocalServiceCookie">
|
||||||
|
<arg name="cookie" type="u" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="GetAlternativeHostName">
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="GetAlternativeServiceName">
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="GetNetworkInterfaceNameByIndex">
|
||||||
|
<arg name="index" type="i" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
<method name="GetNetworkInterfaceIndexByName">
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="index" type="i" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="ResolveHostName">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="interface" type="i" direction="out"/>
|
||||||
|
<arg name="protocol" type="i" direction="out"/>
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="out"/>
|
||||||
|
<arg name="address" type="s" direction="out"/>
|
||||||
|
<arg name="flags" type="u" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="ResolveAddress">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="address" type="s" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="interface" type="i" direction="out"/>
|
||||||
|
<arg name="protocol" type="i" direction="out"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="out"/>
|
||||||
|
<arg name="address" type="s" direction="out"/>
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
<arg name="flags" type="u" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="ResolveService">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="type" type="s" direction="in"/>
|
||||||
|
<arg name="domain" type="s" direction="in"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="interface" type="i" direction="out"/>
|
||||||
|
<arg name="protocol" type="i" direction="out"/>
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
<arg name="type" type="s" direction="out"/>
|
||||||
|
<arg name="domain" type="s" direction="out"/>
|
||||||
|
<arg name="host" type="s" direction="out"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="out"/>
|
||||||
|
<arg name="address" type="s" direction="out"/>
|
||||||
|
<arg name="port" type="q" direction="out"/>
|
||||||
|
<arg name="txt" type="aay" direction="out"/>
|
||||||
|
<arg name="flags" type="u" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="EntryGroupNew">
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="DomainBrowserPrepare">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="domain" type="s" direction="in"/>
|
||||||
|
<arg name="btype" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="ServiceTypeBrowserPrepare">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="domain" type="s" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="ServiceBrowserPrepare">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="type" type="s" direction="in"/>
|
||||||
|
<arg name="domain" type="s" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="ServiceResolverPrepare">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="type" type="s" direction="in"/>
|
||||||
|
<arg name="domain" type="s" direction="in"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="HostNameResolverPrepare">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="AddressResolverPrepare">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="address" type="s" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="RecordBrowserPrepare">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="clazz" type="q" direction="in"/>
|
||||||
|
<arg name="type" type="q" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
</interface>
|
||||||
|
</node>
|
||||||
114
src/avahi/avahi.cpp
Normal file
114
src/avahi/avahi.cpp
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
* Strawberry Music Player
|
||||||
|
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
|
*
|
||||||
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Strawberry is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QString>
|
||||||
|
#include <QDBusConnection>
|
||||||
|
#include <QDBusPendingReply>
|
||||||
|
#include <QDBusPendingCallWatcher>
|
||||||
|
|
||||||
|
#include "core/logging.h"
|
||||||
|
|
||||||
|
#include "avahi.h"
|
||||||
|
#include "avahi/avahiserver.h"
|
||||||
|
#include "avahi/avahientrygroup.h"
|
||||||
|
|
||||||
|
using namespace Qt::StringLiterals;
|
||||||
|
|
||||||
|
Avahi::Avahi(QObject *parent) : Zeroconf(parent), port_(0), entry_group_interface_(nullptr) {}
|
||||||
|
|
||||||
|
void Avahi::PublishInternal(const QString &domain, const QString &type, const QByteArray &name, quint16 port) {
|
||||||
|
|
||||||
|
domain_ = domain;
|
||||||
|
type_ = type;
|
||||||
|
name_ = name;
|
||||||
|
port_ = port;
|
||||||
|
|
||||||
|
OrgFreedesktopAvahiServerInterface server_interface(u"org.freedesktop.Avahi"_s, u"/"_s, QDBusConnection::systemBus());
|
||||||
|
QDBusPendingReply<QDBusObjectPath> reply = server_interface.EntryGroupNew();
|
||||||
|
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply);
|
||||||
|
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, &Avahi::PublishInternalFinished);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void Avahi::PublishInternalFinished(QDBusPendingCallWatcher *watcher) {
|
||||||
|
|
||||||
|
const QDBusPendingReply<QDBusObjectPath> path_reply = watcher->reply();
|
||||||
|
|
||||||
|
watcher->deleteLater();
|
||||||
|
|
||||||
|
if (path_reply.isError()) {
|
||||||
|
qLog(Error) << "Failed to create Avahi entry group:" << path_reply.error();
|
||||||
|
qLog(Info) << "This might be because 'disable-user-service-publishing'" << "is set to 'yes' in avahi-daemon.conf";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddService(path_reply.reply().path());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void Avahi::AddService(const QString &path) {
|
||||||
|
|
||||||
|
entry_group_interface_ = new OrgFreedesktopAvahiEntryGroupInterface(u"org.freedesktop.Avahi"_s, path, QDBusConnection::systemBus());
|
||||||
|
QDBusPendingReply<> reply = entry_group_interface_->AddService(-1, -1, 0, QString::fromUtf8(name_.constData(), name_.size()), type_, domain_, QString(), port_);
|
||||||
|
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply);
|
||||||
|
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, &Avahi::AddServiceFinished);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void Avahi::AddServiceFinished(QDBusPendingCallWatcher *watcher) {
|
||||||
|
|
||||||
|
const QDBusPendingReply<QDBusObjectPath> path_reply = watcher->reply();
|
||||||
|
|
||||||
|
watcher->deleteLater();
|
||||||
|
|
||||||
|
if (path_reply.isError()) {
|
||||||
|
qLog(Error) << "Failed to add Avahi service:" << path_reply.error();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Commit();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void Avahi::Commit() {
|
||||||
|
|
||||||
|
QDBusPendingReply<> reply = entry_group_interface_->Commit();
|
||||||
|
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply);
|
||||||
|
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, &Avahi::CommitFinished);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void Avahi::CommitFinished(QDBusPendingCallWatcher *watcher) {
|
||||||
|
|
||||||
|
const QDBusPendingReply<QDBusObjectPath> path_reply = watcher->reply();
|
||||||
|
|
||||||
|
watcher->deleteLater();
|
||||||
|
|
||||||
|
entry_group_interface_->deleteLater();
|
||||||
|
entry_group_interface_ = nullptr;
|
||||||
|
|
||||||
|
if (path_reply.isError()) {
|
||||||
|
qLog(Debug) << "Commit error:" << path_reply.error();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qLog(Debug) << "Remote interface published on Avahi";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
58
src/avahi/avahi.h
Normal file
58
src/avahi/avahi.h
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Strawberry Music Player
|
||||||
|
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
|
*
|
||||||
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Strawberry is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef AVAHI_H
|
||||||
|
#define AVAHI_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "core/zeroconf.h"
|
||||||
|
|
||||||
|
class QDBusPendingCallWatcher;
|
||||||
|
class OrgFreedesktopAvahiEntryGroupInterface;
|
||||||
|
|
||||||
|
class Avahi : public Zeroconf {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Avahi(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void AddService(const QString &path);
|
||||||
|
void Commit();
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void PublishInternalFinished(QDBusPendingCallWatcher *watcher);
|
||||||
|
void AddServiceFinished(QDBusPendingCallWatcher *watcher);
|
||||||
|
void CommitFinished(QDBusPendingCallWatcher *watcher);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void PublishInternal(const QString &domain, const QString &type, const QByteArray &name, quint16 port) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString domain_;
|
||||||
|
QString type_;
|
||||||
|
QByteArray name_;
|
||||||
|
quint16 port_;
|
||||||
|
OrgFreedesktopAvahiEntryGroupInterface *entry_group_interface_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // AVAHI_H
|
||||||
98
src/avahi/org.freedesktop.Avahi.EntryGroup.xml
Normal file
98
src/avahi/org.freedesktop.Avahi.EntryGroup.xml
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<?xml version="1.0" standalone='no'?><!--*-nxml-*-->
|
||||||
|
<!DOCTYPE node SYSTEM "introspect.dtd">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
This file is part of avahi.
|
||||||
|
|
||||||
|
avahi is free software; you can redistribute it and/or modify it
|
||||||
|
under the terms of the GNU Lesser General Public License as
|
||||||
|
published by the Free Software Foundation; either version 2 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
avahi is distributed in the hope that it will be useful, but
|
||||||
|
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with avahi; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||||
|
02111-1307 USA.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<node>
|
||||||
|
|
||||||
|
<interface name="org.freedesktop.DBus.Introspectable">
|
||||||
|
<method name="Introspect">
|
||||||
|
<arg name="data" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
</interface>
|
||||||
|
|
||||||
|
<interface name="org.freedesktop.Avahi.EntryGroup">
|
||||||
|
<method name="Free"/>
|
||||||
|
<method name="Commit"/>
|
||||||
|
<method name="Reset"/>
|
||||||
|
|
||||||
|
<method name="GetState">
|
||||||
|
<arg name="state" type="i" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<signal name="StateChanged">
|
||||||
|
<arg name="state" type="i"/>
|
||||||
|
<arg name="error" type="s"/>
|
||||||
|
</signal>
|
||||||
|
|
||||||
|
<method name="IsEmpty">
|
||||||
|
<arg name="empty" type="b" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="AddService">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="type" type="s" direction="in"/>
|
||||||
|
<arg name="domain" type="s" direction="in"/>
|
||||||
|
<arg name="host" type="s" direction="in"/>
|
||||||
|
<arg name="port" type="q" direction="in"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="AddServiceSubtype">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="type" type="s" direction="in"/>
|
||||||
|
<arg name="domain" type="s" direction="in"/>
|
||||||
|
<arg name="subtype" type="s" direction="in"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="UpdateServiceTxt">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="type" type="s" direction="in"/>
|
||||||
|
<arg name="domain" type="s" direction="in"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="AddAddress">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="address" type="s" direction="in"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="AddRecord">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="clazz" type="q" direction="in"/>
|
||||||
|
<arg name="type" type="q" direction="in"/>
|
||||||
|
<arg name="ttl" type="u" direction="in"/>
|
||||||
|
<arg name="rdata" type="ay" direction="in"/>
|
||||||
|
</method>
|
||||||
|
</interface>
|
||||||
|
</node>
|
||||||
396
src/avahi/org.freedesktop.Avahi.Server.xml
Normal file
396
src/avahi/org.freedesktop.Avahi.Server.xml
Normal file
@@ -0,0 +1,396 @@
|
|||||||
|
<?xml version="1.0" standalone='no'?><!--*-nxml-*-->
|
||||||
|
<!DOCTYPE node SYSTEM "introspect.dtd">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
This file is part of avahi.
|
||||||
|
|
||||||
|
avahi is free software; you can redistribute it and/or modify it
|
||||||
|
under the terms of the GNU Lesser General Public License as
|
||||||
|
published by the Free Software Foundation; either version 2 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
avahi is distributed in the hope that it will be useful, but
|
||||||
|
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with avahi; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||||
|
02111-1307 USA.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<node>
|
||||||
|
|
||||||
|
<interface name="org.freedesktop.Avahi.Server">
|
||||||
|
|
||||||
|
<method name="GetVersionString">
|
||||||
|
<arg name="version" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="GetAPIVersion">
|
||||||
|
<arg name="version" type="u" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="GetHostName">
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
<method name="SetHostName">
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
</method>
|
||||||
|
<method name="GetHostNameFqdn">
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
<method name="GetDomainName">
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="IsNSSSupportAvailable">
|
||||||
|
<arg name="yes" type="b" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="GetState">
|
||||||
|
<arg name="state" type="i" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<signal name="StateChanged">
|
||||||
|
<arg name="state" type="i"/>
|
||||||
|
<arg name="error" type="s"/>
|
||||||
|
</signal>
|
||||||
|
|
||||||
|
<method name="GetLocalServiceCookie">
|
||||||
|
<arg name="cookie" type="u" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="GetAlternativeHostName">
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="GetAlternativeServiceName">
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="GetNetworkInterfaceNameByIndex">
|
||||||
|
<arg name="index" type="i" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
<method name="GetNetworkInterfaceIndexByName">
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="index" type="i" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="ResolveHostName">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="interface" type="i" direction="out"/>
|
||||||
|
<arg name="protocol" type="i" direction="out"/>
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="out"/>
|
||||||
|
<arg name="address" type="s" direction="out"/>
|
||||||
|
<arg name="flags" type="u" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="ResolveAddress">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="address" type="s" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="interface" type="i" direction="out"/>
|
||||||
|
<arg name="protocol" type="i" direction="out"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="out"/>
|
||||||
|
<arg name="address" type="s" direction="out"/>
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
<arg name="flags" type="u" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="ResolveService">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="type" type="s" direction="in"/>
|
||||||
|
<arg name="domain" type="s" direction="in"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="interface" type="i" direction="out"/>
|
||||||
|
<arg name="protocol" type="i" direction="out"/>
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
<arg name="type" type="s" direction="out"/>
|
||||||
|
<arg name="domain" type="s" direction="out"/>
|
||||||
|
<arg name="host" type="s" direction="out"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="out"/>
|
||||||
|
<arg name="address" type="s" direction="out"/>
|
||||||
|
<arg name="port" type="q" direction="out"/>
|
||||||
|
<arg name="flags" type="u" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="EntryGroupNew">
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="DomainBrowserNew">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="domain" type="s" direction="in"/>
|
||||||
|
<arg name="btype" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="ServiceTypeBrowserNew">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="domain" type="s" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="ServiceBrowserNew">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="type" type="s" direction="in"/>
|
||||||
|
<arg name="domain" type="s" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="ServiceResolverNew">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="type" type="s" direction="in"/>
|
||||||
|
<arg name="domain" type="s" direction="in"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="HostNameResolverNew">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="AddressResolverNew">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="address" type="s" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="RecordBrowserNew">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="clazz" type="q" direction="in"/>
|
||||||
|
<arg name="type" type="q" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
</interface>
|
||||||
|
|
||||||
|
<interface name="org.freedesktop.Avahi.Server2">
|
||||||
|
|
||||||
|
<method name="GetVersionString">
|
||||||
|
<arg name="version" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="GetAPIVersion">
|
||||||
|
<arg name="version" type="u" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="GetHostName">
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
<method name="SetHostName">
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
</method>
|
||||||
|
<method name="GetHostNameFqdn">
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
<method name="GetDomainName">
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="IsNSSSupportAvailable">
|
||||||
|
<arg name="yes" type="b" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="GetState">
|
||||||
|
<arg name="state" type="i" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<signal name="StateChanged">
|
||||||
|
<arg name="state" type="i"/>
|
||||||
|
<arg name="error" type="s"/>
|
||||||
|
</signal>
|
||||||
|
|
||||||
|
<method name="GetLocalServiceCookie">
|
||||||
|
<arg name="cookie" type="u" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="GetAlternativeHostName">
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="GetAlternativeServiceName">
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="GetNetworkInterfaceNameByIndex">
|
||||||
|
<arg name="index" type="i" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
</method>
|
||||||
|
<method name="GetNetworkInterfaceIndexByName">
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="index" type="i" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="ResolveHostName">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="interface" type="i" direction="out"/>
|
||||||
|
<arg name="protocol" type="i" direction="out"/>
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="out"/>
|
||||||
|
<arg name="address" type="s" direction="out"/>
|
||||||
|
<arg name="flags" type="u" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="ResolveAddress">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="address" type="s" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="interface" type="i" direction="out"/>
|
||||||
|
<arg name="protocol" type="i" direction="out"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="out"/>
|
||||||
|
<arg name="address" type="s" direction="out"/>
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
<arg name="flags" type="u" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="ResolveService">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="type" type="s" direction="in"/>
|
||||||
|
<arg name="domain" type="s" direction="in"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="interface" type="i" direction="out"/>
|
||||||
|
<arg name="protocol" type="i" direction="out"/>
|
||||||
|
<arg name="name" type="s" direction="out"/>
|
||||||
|
<arg name="type" type="s" direction="out"/>
|
||||||
|
<arg name="domain" type="s" direction="out"/>
|
||||||
|
<arg name="host" type="s" direction="out"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="out"/>
|
||||||
|
<arg name="address" type="s" direction="out"/>
|
||||||
|
<arg name="port" type="q" direction="out"/>
|
||||||
|
<arg name="flags" type="u" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="EntryGroupNew">
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="DomainBrowserPrepare">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="domain" type="s" direction="in"/>
|
||||||
|
<arg name="btype" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="ServiceTypeBrowserPrepare">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="domain" type="s" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="ServiceBrowserPrepare">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="type" type="s" direction="in"/>
|
||||||
|
<arg name="domain" type="s" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="ServiceResolverPrepare">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="type" type="s" direction="in"/>
|
||||||
|
<arg name="domain" type="s" direction="in"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="HostNameResolverPrepare">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="aprotocol" type="i" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="AddressResolverPrepare">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="address" type="s" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="RecordBrowserPrepare">
|
||||||
|
<arg name="interface" type="i" direction="in"/>
|
||||||
|
<arg name="protocol" type="i" direction="in"/>
|
||||||
|
<arg name="name" type="s" direction="in"/>
|
||||||
|
<arg name="clazz" type="q" direction="in"/>
|
||||||
|
<arg name="type" type="q" direction="in"/>
|
||||||
|
<arg name="flags" type="u" direction="in"/>
|
||||||
|
|
||||||
|
<arg name="path" type="o" direction="out"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
</interface>
|
||||||
|
</node>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
* Strawberry Music Player
|
* Strawberry Music Player
|
||||||
* This file was part of Clementine.
|
* This file was part of Clementine.
|
||||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||||
* Copyright 2018-2026, Jonas Kvinge <jonas@jkvinge.net>
|
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
*
|
*
|
||||||
* Strawberry is free software: you can redistribute it and/or modify
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -537,6 +537,18 @@ void CollectionBackend::AddOrUpdateSubdirs(const CollectionSubdirectoryList &sub
|
|||||||
|
|
||||||
ScopedTransaction transaction(&db);
|
ScopedTransaction transaction(&db);
|
||||||
for (const CollectionSubdirectory &subdir : subdirs) {
|
for (const CollectionSubdirectory &subdir : subdirs) {
|
||||||
|
if (subdir.mtime == 0) {
|
||||||
|
// Delete the subdirectory
|
||||||
|
SqlQuery q(db);
|
||||||
|
q.prepare(QStringLiteral("DELETE FROM %1 WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
|
||||||
|
q.BindValue(u":id"_s, subdir.directory_id);
|
||||||
|
q.BindValue(u":path"_s, subdir.path);
|
||||||
|
if (!q.Exec()) {
|
||||||
|
db_->ReportErrors(q);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
// See if this subdirectory already exists in the database
|
// See if this subdirectory already exists in the database
|
||||||
bool exists = false;
|
bool exists = false;
|
||||||
{
|
{
|
||||||
@@ -574,26 +586,6 @@ void CollectionBackend::AddOrUpdateSubdirs(const CollectionSubdirectoryList &sub
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction.Commit();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectionBackend::DeleteSubdirs(const CollectionSubdirectoryList &subdirs) {
|
|
||||||
|
|
||||||
QMutexLocker l(db_->Mutex());
|
|
||||||
QSqlDatabase db(db_->Connect());
|
|
||||||
|
|
||||||
ScopedTransaction transaction(&db);
|
|
||||||
for (const CollectionSubdirectory &subdir : subdirs) {
|
|
||||||
SqlQuery q(db);
|
|
||||||
q.prepare(QStringLiteral("DELETE FROM %1 WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
|
|
||||||
q.BindValue(u":id"_s, subdir.directory_id);
|
|
||||||
q.BindValue(u":path"_s, subdir.path);
|
|
||||||
if (!q.Exec()) {
|
|
||||||
db_->ReportErrors(q);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Strawberry Music Player
|
* Strawberry Music Player
|
||||||
* This file was part of Clementine.
|
* This file was part of Clementine.
|
||||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||||
* Copyright 2018-2026, Jonas Kvinge <jonas@jkvinge.net>
|
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
*
|
*
|
||||||
* Strawberry is free software: you can redistribute it and/or modify
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -252,7 +252,6 @@ class CollectionBackend : public CollectionBackendInterface {
|
|||||||
void DeleteSongsByUrls(const QList<QUrl> &url);
|
void DeleteSongsByUrls(const QList<QUrl> &url);
|
||||||
void MarkSongsUnavailable(const SongList &songs, const bool unavailable = true);
|
void MarkSongsUnavailable(const SongList &songs, const bool unavailable = true);
|
||||||
void AddOrUpdateSubdirs(const CollectionSubdirectoryList &subdirs);
|
void AddOrUpdateSubdirs(const CollectionSubdirectoryList &subdirs);
|
||||||
void DeleteSubdirs(const CollectionSubdirectoryList &subdirs);
|
|
||||||
void CompilationsNeedUpdating();
|
void CompilationsNeedUpdating();
|
||||||
void UpdateEmbeddedAlbumArt(const QString &effective_albumartist, const QString &album, const bool art_embedded);
|
void UpdateEmbeddedAlbumArt(const QString &effective_albumartist, const QString &album, const bool art_embedded);
|
||||||
void UpdateManualAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &art_manual);
|
void UpdateManualAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &art_manual);
|
||||||
|
|||||||
@@ -124,7 +124,6 @@ void CollectionLibrary::Init() {
|
|||||||
QObject::connect(watcher_, &CollectionWatcher::SongsReadded, &*backend_, &CollectionBackend::MarkSongsUnavailable);
|
QObject::connect(watcher_, &CollectionWatcher::SongsReadded, &*backend_, &CollectionBackend::MarkSongsUnavailable);
|
||||||
QObject::connect(watcher_, &CollectionWatcher::SubdirsDiscovered, &*backend_, &CollectionBackend::AddOrUpdateSubdirs);
|
QObject::connect(watcher_, &CollectionWatcher::SubdirsDiscovered, &*backend_, &CollectionBackend::AddOrUpdateSubdirs);
|
||||||
QObject::connect(watcher_, &CollectionWatcher::SubdirsMTimeUpdated, &*backend_, &CollectionBackend::AddOrUpdateSubdirs);
|
QObject::connect(watcher_, &CollectionWatcher::SubdirsMTimeUpdated, &*backend_, &CollectionBackend::AddOrUpdateSubdirs);
|
||||||
QObject::connect(watcher_, &CollectionWatcher::SubdirsDeleted, &*backend_, &CollectionBackend::DeleteSubdirs);
|
|
||||||
QObject::connect(watcher_, &CollectionWatcher::CompilationsNeedUpdating, &*backend_, &CollectionBackend::CompilationsNeedUpdating);
|
QObject::connect(watcher_, &CollectionWatcher::CompilationsNeedUpdating, &*backend_, &CollectionBackend::CompilationsNeedUpdating);
|
||||||
QObject::connect(watcher_, &CollectionWatcher::UpdateLastSeen, &*backend_, &CollectionBackend::UpdateLastSeen);
|
QObject::connect(watcher_, &CollectionWatcher::UpdateLastSeen, &*backend_, &CollectionBackend::UpdateLastSeen);
|
||||||
|
|
||||||
|
|||||||
@@ -383,7 +383,7 @@ void CollectionView::keyPressEvent(QKeyEvent *e) {
|
|||||||
case Qt::Key_Enter:
|
case Qt::Key_Enter:
|
||||||
case Qt::Key_Return:
|
case Qt::Key_Return:
|
||||||
if (currentIndex().isValid()) {
|
if (currentIndex().isValid()) {
|
||||||
Q_EMIT doubleClicked(currentIndex());
|
AddToPlaylist();
|
||||||
}
|
}
|
||||||
e->accept();
|
e->accept();
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Strawberry Music Player
|
* Strawberry Music Player
|
||||||
* This file was part of Clementine.
|
* This file was part of Clementine.
|
||||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||||
* Copyright 2018-2026, Jonas Kvinge <jonas@jkvinge.net>
|
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
*
|
*
|
||||||
* Strawberry is free software: you can redistribute it and/or modify
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
using namespace std::chrono_literals;
|
using namespace std::chrono_literals;
|
||||||
using namespace Qt::Literals::StringLiterals;
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
|
||||||
QStringList CollectionWatcher::sValidImages = QStringList() << u"jpg"_s << u"jpeg"_s << u"jp2"_s << u"png"_s << u"gif"_s << u"tiff"_s << u"tif"_s << u"webp"_s;
|
QStringList CollectionWatcher::sValidImages = QStringList() << u"jpg"_s << u"png"_s << u"gif"_s << u"jpeg"_s;
|
||||||
|
|
||||||
CollectionWatcher::CollectionWatcher(const Song::Source source,
|
CollectionWatcher::CollectionWatcher(const Song::Source source,
|
||||||
const SharedPtr<TaskManager> task_manager,
|
const SharedPtr<TaskManager> task_manager,
|
||||||
@@ -261,7 +261,7 @@ void CollectionWatcher::ReloadSettings() {
|
|||||||
CollectionWatcher::ScanTransaction::ScanTransaction(CollectionWatcher *watcher, const int dir, const bool incremental, const bool ignores_mtime, const bool mark_songs_unavailable)
|
CollectionWatcher::ScanTransaction::ScanTransaction(CollectionWatcher *watcher, const int dir, const bool incremental, const bool ignores_mtime, const bool mark_songs_unavailable)
|
||||||
: progress_(0),
|
: progress_(0),
|
||||||
progress_max_(0),
|
progress_max_(0),
|
||||||
dir_id_(dir),
|
dir_(dir),
|
||||||
incremental_(incremental),
|
incremental_(incremental),
|
||||||
ignores_mtime_(ignores_mtime),
|
ignores_mtime_(ignores_mtime),
|
||||||
mark_songs_unavailable_(mark_songs_unavailable),
|
mark_songs_unavailable_(mark_songs_unavailable),
|
||||||
@@ -313,19 +313,6 @@ void CollectionWatcher::ScanTransaction::AddToProgressMax(const quint64 n) {
|
|||||||
|
|
||||||
void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
|
void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
|
||||||
|
|
||||||
if (!deleted_subdirs.isEmpty()) {
|
|
||||||
Q_EMIT watcher_->SubdirsDeleted(deleted_subdirs);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!new_subdirs.isEmpty()) {
|
|
||||||
Q_EMIT watcher_->SubdirsDiscovered(new_subdirs);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!touched_subdirs.isEmpty()) {
|
|
||||||
Q_EMIT watcher_->SubdirsMTimeUpdated(touched_subdirs);
|
|
||||||
touched_subdirs.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!deleted_songs.isEmpty()) {
|
if (!deleted_songs.isEmpty()) {
|
||||||
if (mark_songs_unavailable_ && watcher_->source() == Song::Source::Collection) {
|
if (mark_songs_unavailable_ && watcher_->source() == Song::Source::Collection) {
|
||||||
Q_EMIT watcher_->SongsUnavailable(deleted_songs);
|
Q_EMIT watcher_->SongsUnavailable(deleted_songs);
|
||||||
@@ -351,24 +338,34 @@ void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
|
|||||||
readded_songs.clear();
|
readded_songs.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!new_subdirs.isEmpty()) {
|
||||||
|
Q_EMIT watcher_->SubdirsDiscovered(new_subdirs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!touched_subdirs.isEmpty()) {
|
||||||
|
Q_EMIT watcher_->SubdirsMTimeUpdated(touched_subdirs);
|
||||||
|
touched_subdirs.clear();
|
||||||
|
}
|
||||||
|
|
||||||
for (const CollectionSubdirectory &subdir : std::as_const(deleted_subdirs)) {
|
for (const CollectionSubdirectory &subdir : std::as_const(deleted_subdirs)) {
|
||||||
if (watcher_->watched_dirs_.contains(dir_id_)) {
|
if (watcher_->watched_dirs_.contains(dir_)) {
|
||||||
watcher_->RemoveWatch(watcher_->watched_dirs_[dir_id_], subdir);
|
watcher_->RemoveWatch(watcher_->watched_dirs_[dir_], subdir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
deleted_subdirs.clear();
|
deleted_subdirs.clear();
|
||||||
|
|
||||||
if (watcher_->monitor_) {
|
if (watcher_->monitor_) {
|
||||||
|
// Watch the new subdirectories
|
||||||
for (const CollectionSubdirectory &subdir : std::as_const(new_subdirs)) {
|
for (const CollectionSubdirectory &subdir : std::as_const(new_subdirs)) {
|
||||||
if (watcher_->watched_dirs_.contains(dir_id_)) {
|
if (watcher_->watched_dirs_.contains(dir_)) {
|
||||||
watcher_->AddWatch(watcher_->watched_dirs_[dir_id_], subdir.path);
|
watcher_->AddWatch(watcher_->watched_dirs_[dir_], subdir.path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
new_subdirs.clear();
|
new_subdirs.clear();
|
||||||
|
|
||||||
if (incremental_ || ignores_mtime_) {
|
if (incremental_ || ignores_mtime_) {
|
||||||
Q_EMIT watcher_->UpdateLastSeen(dir_id_, expire_unavailable_songs_days_);
|
Q_EMIT watcher_->UpdateLastSeen(dir_, expire_unavailable_songs_days_);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -377,7 +374,7 @@ void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
|
|||||||
SongList CollectionWatcher::ScanTransaction::FindSongsInSubdirectory(const QString &path) {
|
SongList CollectionWatcher::ScanTransaction::FindSongsInSubdirectory(const QString &path) {
|
||||||
|
|
||||||
if (cached_songs_dirty_) {
|
if (cached_songs_dirty_) {
|
||||||
const SongList songs = watcher_->backend_->FindSongsInDirectory(dir_id_);
|
const SongList songs = watcher_->backend_->FindSongsInDirectory(dir_);
|
||||||
for (const Song &song : songs) {
|
for (const Song &song : songs) {
|
||||||
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
|
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
|
||||||
cached_songs_.insert(p, song);
|
cached_songs_.insert(p, song);
|
||||||
@@ -396,7 +393,7 @@ SongList CollectionWatcher::ScanTransaction::FindSongsInSubdirectory(const QStri
|
|||||||
bool CollectionWatcher::ScanTransaction::HasSongsWithMissingFingerprint(const QString &path) {
|
bool CollectionWatcher::ScanTransaction::HasSongsWithMissingFingerprint(const QString &path) {
|
||||||
|
|
||||||
if (cached_songs_missing_fingerprint_dirty_) {
|
if (cached_songs_missing_fingerprint_dirty_) {
|
||||||
const SongList songs = watcher_->backend_->SongsWithMissingFingerprint(dir_id_);
|
const SongList songs = watcher_->backend_->SongsWithMissingFingerprint(dir_);
|
||||||
for (const Song &song : songs) {
|
for (const Song &song : songs) {
|
||||||
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
|
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
|
||||||
cached_songs_missing_fingerprint_.insert(p, song);
|
cached_songs_missing_fingerprint_.insert(p, song);
|
||||||
@@ -411,7 +408,7 @@ bool CollectionWatcher::ScanTransaction::HasSongsWithMissingFingerprint(const QS
|
|||||||
bool CollectionWatcher::ScanTransaction::HasSongsWithMissingLoudnessCharacteristics(const QString &path) {
|
bool CollectionWatcher::ScanTransaction::HasSongsWithMissingLoudnessCharacteristics(const QString &path) {
|
||||||
|
|
||||||
if (cached_songs_missing_loudness_characteristics_dirty_) {
|
if (cached_songs_missing_loudness_characteristics_dirty_) {
|
||||||
const SongList songs = watcher_->backend_->SongsWithMissingLoudnessCharacteristics(dir_id_);
|
const SongList songs = watcher_->backend_->SongsWithMissingLoudnessCharacteristics(dir_);
|
||||||
for (const Song &song : songs) {
|
for (const Song &song : songs) {
|
||||||
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
|
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
|
||||||
cached_songs_missing_loudness_characteristics_.insert(p, song);
|
cached_songs_missing_loudness_characteristics_.insert(p, song);
|
||||||
@@ -433,7 +430,7 @@ void CollectionWatcher::ScanTransaction::SetKnownSubdirs(const CollectionSubdire
|
|||||||
bool CollectionWatcher::ScanTransaction::HasSeenSubdir(const QString &path) {
|
bool CollectionWatcher::ScanTransaction::HasSeenSubdir(const QString &path) {
|
||||||
|
|
||||||
if (known_subdirs_dirty_) {
|
if (known_subdirs_dirty_) {
|
||||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_id_));
|
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
|
||||||
}
|
}
|
||||||
|
|
||||||
return std::any_of(known_subdirs_.begin(), known_subdirs_.end(), [path](const CollectionSubdirectory &subdir) { return subdir.path == path && subdir.mtime != 0; });
|
return std::any_of(known_subdirs_.begin(), known_subdirs_.end(), [path](const CollectionSubdirectory &subdir) { return subdir.path == path && subdir.mtime != 0; });
|
||||||
@@ -443,7 +440,7 @@ bool CollectionWatcher::ScanTransaction::HasSeenSubdir(const QString &path) {
|
|||||||
CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdirs(const QString &path) {
|
CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdirs(const QString &path) {
|
||||||
|
|
||||||
if (known_subdirs_dirty_) {
|
if (known_subdirs_dirty_) {
|
||||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_id_));
|
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
|
||||||
}
|
}
|
||||||
|
|
||||||
CollectionSubdirectoryList ret;
|
CollectionSubdirectoryList ret;
|
||||||
@@ -460,7 +457,7 @@ CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdi
|
|||||||
CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetAllSubdirs() {
|
CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetAllSubdirs() {
|
||||||
|
|
||||||
if (known_subdirs_dirty_) {
|
if (known_subdirs_dirty_) {
|
||||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_id_));
|
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
|
||||||
}
|
}
|
||||||
|
|
||||||
return known_subdirs_;
|
return known_subdirs_;
|
||||||
@@ -497,7 +494,7 @@ void CollectionWatcher::AddDirectory(const CollectionDirectory &dir, const Colle
|
|||||||
const quint64 files_count = FilesCountForPath(&transaction, dir.path);
|
const quint64 files_count = FilesCountForPath(&transaction, dir.path);
|
||||||
transaction.SetKnownSubdirs(subdirs);
|
transaction.SetKnownSubdirs(subdirs);
|
||||||
transaction.AddToProgressMax(files_count);
|
transaction.AddToProgressMax(files_count);
|
||||||
ScanSubdirectory(dir, dir.path, CollectionSubdirectory(), files_count, &transaction);
|
ScanSubdirectory(dir.path, CollectionSubdirectory(), files_count, &transaction);
|
||||||
last_scan_time_ = QDateTime::currentSecsSinceEpoch();
|
last_scan_time_ = QDateTime::currentSecsSinceEpoch();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -515,7 +512,7 @@ void CollectionWatcher::AddDirectory(const CollectionDirectory &dir, const Colle
|
|||||||
transaction.AddToProgressMax(files_count);
|
transaction.AddToProgressMax(files_count);
|
||||||
for (const CollectionSubdirectory &subdir : subdirs) {
|
for (const CollectionSubdirectory &subdir : subdirs) {
|
||||||
if (stop_or_abort_requested()) break;
|
if (stop_or_abort_requested()) break;
|
||||||
ScanSubdirectory(dir, subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
|
ScanSubdirectory(subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
|
||||||
}
|
}
|
||||||
if (!stop_or_abort_requested()) {
|
if (!stop_or_abort_requested()) {
|
||||||
last_scan_time_ = QDateTime::currentSecsSinceEpoch();
|
last_scan_time_ = QDateTime::currentSecsSinceEpoch();
|
||||||
@@ -527,10 +524,9 @@ void CollectionWatcher::AddDirectory(const CollectionDirectory &dir, const Colle
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, ScanTransaction *t, const bool force_noincremental) {
|
void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, ScanTransaction *t, const bool force_noincremental) {
|
||||||
|
|
||||||
const QFileInfo path_info(path);
|
const QFileInfo path_info(path);
|
||||||
const qint64 path_mtime = path_info.exists() && path_info.lastModified().isValid() ? path_info.lastModified().toSecsSinceEpoch() : 0;
|
|
||||||
|
|
||||||
if (path_info.isSymLink()) {
|
if (path_info.isSymLink()) {
|
||||||
const QString real_path = path_info.symLinkTarget();
|
const QString real_path = path_info.symLinkTarget();
|
||||||
@@ -540,8 +536,8 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Do not scan symlinked dirs that are already in collection
|
// Do not scan symlinked dirs that are already in collection
|
||||||
for (const CollectionDirectory &i : std::as_const(watched_dirs_)) {
|
for (const CollectionDirectory &dir : std::as_const(watched_dirs_)) {
|
||||||
if (real_path.startsWith(i.path)) {
|
if (real_path.startsWith(dir.path)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -567,7 +563,7 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (!t->ignores_mtime() && !force_noincremental && t->is_incremental() && path_mtime != 0 && subdir.mtime == path_mtime && !songs_missing_fingerprint && !songs_missing_loudness_characteristics) {
|
if (!t->ignores_mtime() && !force_noincremental && t->is_incremental() && subdir.mtime == path_info.lastModified().toSecsSinceEpoch() && !songs_missing_fingerprint && !songs_missing_loudness_characteristics) {
|
||||||
// The directory hasn't changed since last time
|
// The directory hasn't changed since last time
|
||||||
t->AddToProgress(files_count);
|
t->AddToProgress(files_count);
|
||||||
return;
|
return;
|
||||||
@@ -582,12 +578,11 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
|
|||||||
const CollectionSubdirectoryList previous_subdirs = t->GetImmediateSubdirs(path);
|
const CollectionSubdirectoryList previous_subdirs = t->GetImmediateSubdirs(path);
|
||||||
for (const CollectionSubdirectory &prev_subdir : previous_subdirs) {
|
for (const CollectionSubdirectory &prev_subdir : previous_subdirs) {
|
||||||
if (!QFile::exists(prev_subdir.path) && prev_subdir.path != path) {
|
if (!QFile::exists(prev_subdir.path) && prev_subdir.path != path) {
|
||||||
ScanSubdirectory(dir, prev_subdir.path, prev_subdir, 0, t, true);
|
ScanSubdirectory(prev_subdir.path, prev_subdir, 0, t, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// First we "quickly" get a list of the files in the directory that we think might be music. While we're here, we also look for new subdirectories and possible album artwork.
|
// First we "quickly" get a list of the files in the directory that we think might be music. While we're here, we also look for new subdirectories and possible album artwork.
|
||||||
if (path_info.exists()) {
|
|
||||||
QDirIterator it(path, QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
|
QDirIterator it(path, QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
|
||||||
while (it.hasNext()) {
|
while (it.hasNext()) {
|
||||||
|
|
||||||
@@ -597,7 +592,7 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
|
|||||||
const QFileInfo child_fileinfo(child_filepath);
|
const QFileInfo child_fileinfo(child_filepath);
|
||||||
|
|
||||||
if (child_fileinfo.isSymLink()) {
|
if (child_fileinfo.isSymLink()) {
|
||||||
const QStorageInfo storage_info(child_fileinfo.symLinkTarget());
|
QStorageInfo storage_info(child_fileinfo.symLinkTarget());
|
||||||
if (kRejectedFileSystems.contains(storage_info.fileSystemType())) {
|
if (kRejectedFileSystems.contains(storage_info.fileSystemType())) {
|
||||||
qLog(Warning) << "Ignoring symbolic link" << child_filepath << "which links to" << child_fileinfo.symLinkTarget() << "with rejected filesystem type" << storage_info.fileSystemType();
|
qLog(Warning) << "Ignoring symbolic link" << child_filepath << "which links to" << child_fileinfo.symLinkTarget() << "with rejected filesystem type" << storage_info.fileSystemType();
|
||||||
continue;
|
continue;
|
||||||
@@ -610,14 +605,14 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
|
|||||||
CollectionSubdirectory new_subdir;
|
CollectionSubdirectory new_subdir;
|
||||||
new_subdir.directory_id = -1;
|
new_subdir.directory_id = -1;
|
||||||
new_subdir.path = child_filepath;
|
new_subdir.path = child_filepath;
|
||||||
new_subdir.mtime = child_fileinfo.exists() && child_fileinfo.lastModified().isValid() ? child_fileinfo.lastModified().toSecsSinceEpoch() : 0;
|
new_subdir.mtime = child_fileinfo.lastModified().toSecsSinceEpoch();
|
||||||
my_new_subdirs << new_subdir;
|
my_new_subdirs << new_subdir;
|
||||||
}
|
}
|
||||||
t->AddToProgress(1);
|
t->AddToProgress(1);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const QString ext_part = ExtensionPart(child_filepath);
|
QString ext_part(ExtensionPart(child_filepath));
|
||||||
const QString dir_part = DirectoryPart(child_filepath);
|
QString dir_part(DirectoryPart(child_filepath));
|
||||||
if (Song::kRejectedExtensions.contains(child_fileinfo.suffix(), Qt::CaseInsensitive) || child_fileinfo.baseName() == "qt_temp"_L1) {
|
if (Song::kRejectedExtensions.contains(child_fileinfo.suffix(), Qt::CaseInsensitive) || child_fileinfo.baseName() == "qt_temp"_L1) {
|
||||||
t->AddToProgress(1);
|
t->AddToProgress(1);
|
||||||
}
|
}
|
||||||
@@ -625,9 +620,11 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
|
|||||||
album_art[dir_part] << child_filepath;
|
album_art[dir_part] << child_filepath;
|
||||||
t->AddToProgress(1);
|
t->AddToProgress(1);
|
||||||
}
|
}
|
||||||
else {
|
else if (tagreader_client_->IsMediaFileBlocking(child_filepath)) {
|
||||||
files_on_disk << child_filepath;
|
files_on_disk << child_filepath;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
t->AddToProgress(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -635,27 +632,27 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
|
|||||||
if (stop_or_abort_requested()) return;
|
if (stop_or_abort_requested()) return;
|
||||||
|
|
||||||
// Ask the database for a list of files in this directory
|
// Ask the database for a list of files in this directory
|
||||||
const SongList songs_in_db = t->FindSongsInSubdirectory(path);
|
SongList songs_in_db = t->FindSongsInSubdirectory(path);
|
||||||
|
|
||||||
QSet<QString> cues_processed;
|
QSet<QString> cues_processed;
|
||||||
|
|
||||||
// Now compare the list from the database with the list of files on disk
|
// Now compare the list from the database with the list of files on disk
|
||||||
const QStringList files_on_disk_copy = files_on_disk;
|
QStringList files_on_disk_copy = files_on_disk;
|
||||||
for (const QString &file : files_on_disk_copy) {
|
for (const QString &file : files_on_disk_copy) {
|
||||||
|
|
||||||
if (stop_or_abort_requested()) return;
|
if (stop_or_abort_requested()) return;
|
||||||
|
|
||||||
// Associated CUE
|
// Associated CUE
|
||||||
const QString new_cue = CueParser::FindCueFilename(file);
|
QString new_cue = CueParser::FindCueFilename(file);
|
||||||
|
|
||||||
SongList matching_songs;
|
SongList matching_songs;
|
||||||
if (FindSongsByPath(songs_in_db, file, &matching_songs)) { // Found matching song in DB by path.
|
if (FindSongsByPath(songs_in_db, file, &matching_songs)) { // Found matching song in DB by path.
|
||||||
|
|
||||||
const Song matching_song = matching_songs.first();
|
Song matching_song = matching_songs.first();
|
||||||
|
|
||||||
// The song is in the database and still on disk.
|
// The song is in the database and still on disk.
|
||||||
// Check the mtime to see if it's been changed since it was added.
|
// Check the mtime to see if it's been changed since it was added.
|
||||||
const QFileInfo fileinfo(file);
|
QFileInfo fileinfo(file);
|
||||||
|
|
||||||
if (!fileinfo.exists()) {
|
if (!fileinfo.exists()) {
|
||||||
// Partially fixes race condition - if file was removed between being added to the list and now.
|
// Partially fixes race condition - if file was removed between being added to the list and now.
|
||||||
@@ -730,9 +727,7 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (new_cue.isEmpty() || new_cue_mtime == 0) { // If no CUE or it's about to lose it.
|
if (new_cue.isEmpty() || new_cue_mtime == 0) { // If no CUE or it's about to lose it.
|
||||||
if (!UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, cue_deleted, t)) {
|
UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, cue_deleted, t);
|
||||||
files_on_disk.removeAll(file);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else { // If CUE associated.
|
else { // If CUE associated.
|
||||||
UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, art_automatic, matching_songs, t);
|
UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, art_automatic, matching_songs, t);
|
||||||
@@ -755,7 +750,7 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
|
|||||||
|
|
||||||
// The song is in the database and still on disk.
|
// The song is in the database and still on disk.
|
||||||
// Check the mtime to see if it's been changed since it was added.
|
// Check the mtime to see if it's been changed since it was added.
|
||||||
const QFileInfo fileinfo(file);
|
QFileInfo fileinfo(file);
|
||||||
if (!fileinfo.exists()) {
|
if (!fileinfo.exists()) {
|
||||||
// Partially fixes race condition - if file was removed between being added to the list and now.
|
// Partially fixes race condition - if file was removed between being added to the list and now.
|
||||||
files_on_disk.removeAll(file);
|
files_on_disk.removeAll(file);
|
||||||
@@ -766,7 +761,7 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
|
|||||||
// Make sure the songs aren't deleted, as they still exist elsewhere with a different file path.
|
// Make sure the songs aren't deleted, as they still exist elsewhere with a different file path.
|
||||||
bool matching_songs_has_cue = false;
|
bool matching_songs_has_cue = false;
|
||||||
for (const Song &matching_song : std::as_const(matching_songs)) {
|
for (const Song &matching_song : std::as_const(matching_songs)) {
|
||||||
const QString matching_filename = matching_song.url().toLocalFile();
|
QString matching_filename = matching_song.url().toLocalFile();
|
||||||
if (!t->files_changed_path_.contains(matching_filename)) {
|
if (!t->files_changed_path_.contains(matching_filename)) {
|
||||||
t->files_changed_path_ << matching_filename;
|
t->files_changed_path_ << matching_filename;
|
||||||
qLog(Debug) << matching_filename << "has changed path to" << file;
|
qLog(Debug) << matching_filename << "has changed path to" << file;
|
||||||
@@ -789,9 +784,7 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
|
|||||||
const QUrl art_automatic = ArtForSong(file, album_art);
|
const QUrl art_automatic = ArtForSong(file, album_art);
|
||||||
|
|
||||||
if (new_cue.isEmpty() || new_cue_mtime == 0) { // If no CUE or it's about to lose it.
|
if (new_cue.isEmpty() || new_cue_mtime == 0) { // If no CUE or it's about to lose it.
|
||||||
if (!UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, matching_songs_has_cue && new_cue_mtime == 0, t)) {
|
UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, matching_songs_has_cue && new_cue_mtime == 0, t);
|
||||||
files_on_disk.removeAll(file);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else { // If CUE associated.
|
else { // If CUE associated.
|
||||||
UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, art_automatic, matching_songs, t);
|
UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, art_automatic, matching_songs, t);
|
||||||
@@ -802,7 +795,6 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
|
|||||||
|
|
||||||
const SongList songs = ScanNewFile(file, path, fingerprint, new_cue, &cues_processed);
|
const SongList songs = ScanNewFile(file, path, fingerprint, new_cue, &cues_processed);
|
||||||
if (songs.isEmpty()) {
|
if (songs.isEmpty()) {
|
||||||
files_on_disk.removeAll(file);
|
|
||||||
t->AddToProgress(1);
|
t->AddToProgress(1);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -813,7 +805,7 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
|
|||||||
const QUrl art_automatic = ArtForSong(file, album_art);
|
const QUrl art_automatic = ArtForSong(file, album_art);
|
||||||
|
|
||||||
for (Song song : songs) {
|
for (Song song : songs) {
|
||||||
song.set_directory_id(t->dir_id());
|
song.set_directory_id(t->dir());
|
||||||
if (song.art_automatic().isEmpty()) song.set_art_automatic(art_automatic);
|
if (song.art_automatic().isEmpty()) song.set_art_automatic(art_automatic);
|
||||||
t->new_songs << song;
|
t->new_songs << song;
|
||||||
}
|
}
|
||||||
@@ -831,26 +823,27 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add, update or delete subdir
|
// Add this subdir to the new or touched list
|
||||||
CollectionSubdirectory updated_subdir;
|
CollectionSubdirectory updated_subdir;
|
||||||
updated_subdir.directory_id = t->dir_id();
|
updated_subdir.directory_id = t->dir();
|
||||||
updated_subdir.mtime = path_mtime;
|
updated_subdir.mtime = path_info.exists() ? path_info.lastModified().toSecsSinceEpoch() : 0;
|
||||||
updated_subdir.path = path;
|
updated_subdir.path = path;
|
||||||
|
|
||||||
if (!path_info.exists() && updated_subdir.path != dir.path) {
|
if (subdir.directory_id == -1) {
|
||||||
t->deleted_subdirs << updated_subdir;
|
|
||||||
}
|
|
||||||
else if (subdir.directory_id == -1) {
|
|
||||||
t->new_subdirs << updated_subdir;
|
t->new_subdirs << updated_subdir;
|
||||||
}
|
}
|
||||||
else if (subdir.mtime != updated_subdir.mtime) {
|
else {
|
||||||
t->touched_subdirs << updated_subdir;
|
t->touched_subdirs << updated_subdir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (updated_subdir.mtime == 0) { // CollectionSubdirectory deleted, mark it for removal from the watcher.
|
||||||
|
t->deleted_subdirs << updated_subdir;
|
||||||
|
}
|
||||||
|
|
||||||
// Recurse into the new subdirs that we found
|
// Recurse into the new subdirs that we found
|
||||||
for (const CollectionSubdirectory &my_new_subdir : std::as_const(my_new_subdirs)) {
|
for (const CollectionSubdirectory &my_new_subdir : std::as_const(my_new_subdirs)) {
|
||||||
if (stop_or_abort_requested()) return;
|
if (stop_or_abort_requested()) return;
|
||||||
ScanSubdirectory(dir, my_new_subdir.path, my_new_subdir, 0, t, true);
|
ScanSubdirectory(my_new_subdir.path, my_new_subdir, 0, t, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -882,7 +875,7 @@ void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file,
|
|||||||
QSet<int> used_ids;
|
QSet<int> used_ids;
|
||||||
for (Song new_cue_song : songs) {
|
for (Song new_cue_song : songs) {
|
||||||
new_cue_song.set_source(source_);
|
new_cue_song.set_source(source_);
|
||||||
new_cue_song.set_directory_id(t->dir_id());
|
new_cue_song.set_directory_id(t->dir());
|
||||||
PerformEBUR128Analysis(new_cue_song);
|
PerformEBUR128Analysis(new_cue_song);
|
||||||
new_cue_song.set_fingerprint(fingerprint);
|
new_cue_song.set_fingerprint(fingerprint);
|
||||||
|
|
||||||
@@ -908,7 +901,7 @@ void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file,
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
|
void CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
|
||||||
const QString &fingerprint,
|
const QString &fingerprint,
|
||||||
const SongList &matching_songs,
|
const SongList &matching_songs,
|
||||||
const QUrl &art_automatic,
|
const QUrl &art_automatic,
|
||||||
@@ -929,7 +922,7 @@ bool CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
|
|||||||
const TagReaderResult result = tagreader_client_->ReadFileBlocking(file, &song_on_disk);
|
const TagReaderResult result = tagreader_client_->ReadFileBlocking(file, &song_on_disk);
|
||||||
if (result.success() && song_on_disk.is_valid()) {
|
if (result.success() && song_on_disk.is_valid()) {
|
||||||
song_on_disk.set_source(source_);
|
song_on_disk.set_source(source_);
|
||||||
song_on_disk.set_directory_id(t->dir_id());
|
song_on_disk.set_directory_id(t->dir());
|
||||||
song_on_disk.set_id(matching_song.id());
|
song_on_disk.set_id(matching_song.id());
|
||||||
PerformEBUR128Analysis(song_on_disk);
|
PerformEBUR128Analysis(song_on_disk);
|
||||||
song_on_disk.set_fingerprint(fingerprint);
|
song_on_disk.set_fingerprint(fingerprint);
|
||||||
@@ -938,8 +931,6 @@ bool CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
|
|||||||
AddChangedSong(file, matching_song, song_on_disk, t);
|
AddChangedSong(file, matching_song, song_on_disk, t);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.success() && song_on_disk.is_valid();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SongList CollectionWatcher::ScanNewFile(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, QSet<QString> *cues_processed) const {
|
SongList CollectionWatcher::ScanNewFile(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, QSet<QString> *cues_processed) const {
|
||||||
@@ -1208,13 +1199,12 @@ void CollectionWatcher::DirectoryChanged(const QString &subdir) {
|
|||||||
|
|
||||||
void CollectionWatcher::RescanPathsNow() {
|
void CollectionWatcher::RescanPathsNow() {
|
||||||
|
|
||||||
const QList<int> dir_ids = rescan_queue_.keys();
|
const QList<int> dirs = rescan_queue_.keys();
|
||||||
for (const int dir_id : dir_ids) {
|
for (const int dir : dirs) {
|
||||||
|
|
||||||
if (stop_or_abort_requested()) break;
|
if (stop_or_abort_requested()) break;
|
||||||
ScanTransaction transaction(this, dir_id, false, false, mark_songs_unavailable_);
|
ScanTransaction transaction(this, dir, false, false, mark_songs_unavailable_);
|
||||||
|
|
||||||
const QStringList paths = rescan_queue_.value(dir_id);
|
const QStringList paths = rescan_queue_.value(dir);
|
||||||
|
|
||||||
QMap<QString, quint64> subdir_files_count;
|
QMap<QString, quint64> subdir_files_count;
|
||||||
for (const QString &path : paths) {
|
for (const QString &path : paths) {
|
||||||
@@ -1225,14 +1215,11 @@ void CollectionWatcher::RescanPathsNow() {
|
|||||||
|
|
||||||
for (const QString &path : paths) {
|
for (const QString &path : paths) {
|
||||||
if (stop_or_abort_requested()) break;
|
if (stop_or_abort_requested()) break;
|
||||||
if (!subdir_mapping_.contains(path)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
CollectionSubdirectory subdir;
|
CollectionSubdirectory subdir;
|
||||||
subdir.directory_id = dir_id;
|
subdir.directory_id = dir;
|
||||||
subdir.mtime = 0;
|
subdir.mtime = 0;
|
||||||
subdir.path = path;
|
subdir.path = path;
|
||||||
ScanSubdirectory(subdir_mapping_[path], path, subdir, subdir_files_count[path], &transaction);
|
ScanSubdirectory(path, subdir, subdir_files_count[path], &transaction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1357,13 +1344,11 @@ void CollectionWatcher::PerformScan(const bool incremental, const bool ignore_mt
|
|||||||
ScanTransaction transaction(this, dir.id, incremental, ignore_mtimes, mark_songs_unavailable_);
|
ScanTransaction transaction(this, dir.id, incremental, ignore_mtimes, mark_songs_unavailable_);
|
||||||
CollectionSubdirectoryList subdirs = transaction.GetAllSubdirs();
|
CollectionSubdirectoryList subdirs = transaction.GetAllSubdirs();
|
||||||
|
|
||||||
const bool has_collection_root_dir = std::any_of(subdirs.begin(), subdirs.end(), [&dir](const CollectionSubdirectory &subdir) { return subdir.path == dir.path; });
|
if (subdirs.isEmpty()) {
|
||||||
if (!has_collection_root_dir) {
|
qLog(Debug) << "Collection directory wasn't in subdir list.";
|
||||||
qLog(Debug) << "Collection directory wasn't in subdir list, re-adding";
|
|
||||||
CollectionSubdirectory subdir;
|
CollectionSubdirectory subdir;
|
||||||
subdir.directory_id = dir.id;
|
|
||||||
subdir.path = dir.path;
|
subdir.path = dir.path;
|
||||||
subdir.mtime = 0;
|
subdir.directory_id = dir.id;
|
||||||
subdirs << subdir;
|
subdirs << subdir;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1373,7 +1358,7 @@ void CollectionWatcher::PerformScan(const bool incremental, const bool ignore_mt
|
|||||||
|
|
||||||
for (const CollectionSubdirectory &subdir : std::as_const(subdirs)) {
|
for (const CollectionSubdirectory &subdir : std::as_const(subdirs)) {
|
||||||
if (stop_or_abort_requested()) break;
|
if (stop_or_abort_requested()) break;
|
||||||
ScanSubdirectory(dir, subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
|
ScanSubdirectory(subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1474,8 +1459,6 @@ void CollectionWatcher::RescanSongs(const SongList &songs) {
|
|||||||
QStringList scanned_paths;
|
QStringList scanned_paths;
|
||||||
for (const Song &song : songs) {
|
for (const Song &song : songs) {
|
||||||
if (stop_or_abort_requested()) break;
|
if (stop_or_abort_requested()) break;
|
||||||
if (!watched_dirs_.contains(song.directory_id())) continue;
|
|
||||||
const CollectionDirectory dir = watched_dirs_[song.directory_id()];
|
|
||||||
const QString song_path = song.url().toLocalFile().section(u'/', 0, -2);
|
const QString song_path = song.url().toLocalFile().section(u'/', 0, -2);
|
||||||
if (scanned_paths.contains(song_path)) continue;
|
if (scanned_paths.contains(song_path)) continue;
|
||||||
ScanTransaction transaction(this, song.directory_id(), false, true, mark_songs_unavailable_);
|
ScanTransaction transaction(this, song.directory_id(), false, true, mark_songs_unavailable_);
|
||||||
@@ -1485,7 +1468,7 @@ void CollectionWatcher::RescanSongs(const SongList &songs) {
|
|||||||
if (subdir.path != song_path) continue;
|
if (subdir.path != song_path) continue;
|
||||||
qLog(Debug) << "Rescan for directory ID" << song.directory_id() << "directory" << subdir.path;
|
qLog(Debug) << "Rescan for directory ID" << song.directory_id() << "directory" << subdir.path;
|
||||||
const quint64 files_count = FilesCountForPath(&transaction, subdir.path);
|
const quint64 files_count = FilesCountForPath(&transaction, subdir.path);
|
||||||
ScanSubdirectory(dir, song_path, subdir, files_count, &transaction);
|
ScanSubdirectory(song_path, subdir, files_count, &transaction);
|
||||||
scanned_paths << subdir.path;
|
scanned_paths << subdir.path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Strawberry Music Player
|
* Strawberry Music Player
|
||||||
* This file was part of Clementine.
|
* This file was part of Clementine.
|
||||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||||
* Copyright 2018-2026, Jonas Kvinge <jonas@jkvinge.net>
|
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
*
|
*
|
||||||
* Strawberry is free software: you can redistribute it and/or modify
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -85,7 +85,6 @@ class CollectionWatcher : public QObject {
|
|||||||
void SongsReadded(const SongList &songs, const bool unavailable = false);
|
void SongsReadded(const SongList &songs, const bool unavailable = false);
|
||||||
void SubdirsDiscovered(const CollectionSubdirectoryList &subdirs);
|
void SubdirsDiscovered(const CollectionSubdirectoryList &subdirs);
|
||||||
void SubdirsMTimeUpdated(const CollectionSubdirectoryList &subdirs);
|
void SubdirsMTimeUpdated(const CollectionSubdirectoryList &subdirs);
|
||||||
void SubdirsDeleted(const CollectionSubdirectoryList &subdirs);
|
|
||||||
void CompilationsNeedUpdating();
|
void CompilationsNeedUpdating();
|
||||||
void UpdateLastSeen(const int directory_id, const int expire_unavailable_songs_days);
|
void UpdateLastSeen(const int directory_id, const int expire_unavailable_songs_days);
|
||||||
void ExitFinished();
|
void ExitFinished();
|
||||||
@@ -123,7 +122,7 @@ class CollectionWatcher : public QObject {
|
|||||||
// Emits the signals for new & deleted songs etc and clears the lists. This causes the new stuff to be updated on UI.
|
// Emits the signals for new & deleted songs etc and clears the lists. This causes the new stuff to be updated on UI.
|
||||||
void CommitNewOrUpdatedSongs();
|
void CommitNewOrUpdatedSongs();
|
||||||
|
|
||||||
int dir_id() const { return dir_id_; }
|
int dir() const { return dir_; }
|
||||||
bool is_incremental() const { return incremental_; }
|
bool is_incremental() const { return incremental_; }
|
||||||
bool ignores_mtime() const { return ignores_mtime_; }
|
bool ignores_mtime() const { return ignores_mtime_; }
|
||||||
|
|
||||||
@@ -144,7 +143,7 @@ class CollectionWatcher : public QObject {
|
|||||||
quint64 progress_;
|
quint64 progress_;
|
||||||
quint64 progress_max_;
|
quint64 progress_max_;
|
||||||
|
|
||||||
int dir_id_;
|
int dir_;
|
||||||
// Incremental scan enters a directory only if it has changed since the last scan.
|
// Incremental scan enters a directory only if it has changed since the last scan.
|
||||||
bool incremental_;
|
bool incremental_;
|
||||||
// This type of scan updates every file in a folder that's being scanned.
|
// This type of scan updates every file in a folder that's being scanned.
|
||||||
@@ -180,7 +179,7 @@ class CollectionWatcher : public QObject {
|
|||||||
void IncrementalScanNow();
|
void IncrementalScanNow();
|
||||||
void FullScanNow();
|
void FullScanNow();
|
||||||
void RescanPathsNow();
|
void RescanPathsNow();
|
||||||
void ScanSubdirectory(const CollectionDirectory &dir, const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, CollectionWatcher::ScanTransaction *t, const bool force_noincremental = false);
|
void ScanSubdirectory(const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, CollectionWatcher::ScanTransaction *t, const bool force_noincremental = false);
|
||||||
void RescanSongs(const SongList &songs);
|
void RescanSongs(const SongList &songs);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -203,7 +202,7 @@ class CollectionWatcher : public QObject {
|
|||||||
// Updates the sections of a cue associated and altered (according to mtime) media file during a scan.
|
// Updates the sections of a cue associated and altered (according to mtime) media file during a scan.
|
||||||
void UpdateCueAssociatedSongs(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, const QUrl &art_automatic, const SongList &old_cue_songs, ScanTransaction *t) const;
|
void UpdateCueAssociatedSongs(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, const QUrl &art_automatic, const SongList &old_cue_songs, ScanTransaction *t) const;
|
||||||
// Updates a single non-cue associated and altered (according to mtime) song during a scan.
|
// Updates a single non-cue associated and altered (according to mtime) song during a scan.
|
||||||
bool UpdateNonCueAssociatedSong(const QString &file, const QString &fingerprint, const SongList &matching_songs, const QUrl &art_automatic, const bool cue_deleted, ScanTransaction *t);
|
void UpdateNonCueAssociatedSong(const QString &file, const QString &fingerprint, const SongList &matching_songs, const QUrl &art_automatic, const bool cue_deleted, ScanTransaction *t);
|
||||||
// Scans a single media file that's present on the disk but not yet in the collection.
|
// Scans a single media file that's present on the disk but not yet in the collection.
|
||||||
// It may result in a multiple files added to the collection when the media file has many sections (like a CUE related media file).
|
// It may result in a multiple files added to the collection when the media file has many sections (like a CUE related media file).
|
||||||
SongList ScanNewFile(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, QSet<QString> *cues_processed) const;
|
SongList ScanNewFile(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, QSet<QString> *cues_processed) const;
|
||||||
|
|||||||
@@ -7,8 +7,6 @@
|
|||||||
|
|
||||||
#cmakedefine USE_INSTALL_PREFIX
|
#cmakedefine USE_INSTALL_PREFIX
|
||||||
|
|
||||||
#cmakedefine BUILD_FOR_MAC_APP_STORE
|
|
||||||
|
|
||||||
#cmakedefine HAVE_BACKTRACE
|
#cmakedefine HAVE_BACKTRACE
|
||||||
#cmakedefine HAVE_ALSA
|
#cmakedefine HAVE_ALSA
|
||||||
#cmakedefine HAVE_PULSE
|
#cmakedefine HAVE_PULSE
|
||||||
@@ -35,6 +33,7 @@
|
|||||||
#cmakedefine HAVE_SPOTIFY
|
#cmakedefine HAVE_SPOTIFY
|
||||||
#cmakedefine HAVE_QOBUZ
|
#cmakedefine HAVE_QOBUZ
|
||||||
#cmakedefine HAVE_DISCORD_RPC
|
#cmakedefine HAVE_DISCORD_RPC
|
||||||
|
#cmakedefine HAVE_NETWORKREMOTE
|
||||||
|
|
||||||
#cmakedefine HAVE_TAGLIB_DSFFILE
|
#cmakedefine HAVE_TAGLIB_DSFFILE
|
||||||
#cmakedefine HAVE_TAGLIB_DSDIFFFILE
|
#cmakedefine HAVE_TAGLIB_DSDIFFFILE
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ constexpr char kFileFilter[] =
|
|||||||
"*.mod *.s3m *.xm *.it "
|
"*.mod *.s3m *.xm *.it "
|
||||||
"*.spc *.vgm";
|
"*.spc *.vgm";
|
||||||
|
|
||||||
constexpr char kLoadImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.gif *.xpm *.pbm *.pgm *.ppm *.xbm *.webp)");
|
constexpr char kLoadImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.gif *.xpm *.pbm *.pgm *.ppm *.xbm)");
|
||||||
constexpr char kSaveImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.xpm *.pbm *.ppm *.xbm *.webp)");
|
constexpr char kSaveImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.xpm *.pbm *.ppm *.xbm)");
|
||||||
|
|
||||||
#endif // FILEFILTERCONSTANTS_H
|
#endif // FILEFILTERCONSTANTS_H
|
||||||
|
|||||||
36
src/constants/networkremoteconstants.h
Normal file
36
src/constants/networkremoteconstants.h
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Strawberry Music Player
|
||||||
|
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
|
*
|
||||||
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Strawberry is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef NETWORKREMOTECONSTANTS_H
|
||||||
|
#define NETWORKREMOTECONSTANTS_H
|
||||||
|
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
|
||||||
|
namespace NetworkRemoteConstants {
|
||||||
|
|
||||||
|
const QStringList kDefaultMusicExtensionsAllowedRemotely = { u"aac"_s, u"alac"_s, u"flac"_s, u"m3u"_s, u"m4a"_s, u"mp3"_s, u"ogg"_s, u"wav"_s, u"wmv"_s };
|
||||||
|
constexpr quint16 kDefaultServerPort = 5500;
|
||||||
|
constexpr char kTranscoderSettingPostfix[] = "/NetworkRemote";
|
||||||
|
constexpr quint32 kFileChunkSize = 100000;
|
||||||
|
|
||||||
|
} // namespace NetworkRemoteConstants
|
||||||
|
|
||||||
|
#endif // NETWORKREMOTECONSTANTS_H
|
||||||
35
src/constants/networkremotesettingsconstants.h
Normal file
35
src/constants/networkremotesettingsconstants.h
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Strawberry Music Player
|
||||||
|
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
|
*
|
||||||
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Strawberry is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef NETWORKREMOTESETTINGSCONSTANTS_H
|
||||||
|
#define NETWORKREMOTESETTINGSCONSTANTS_H
|
||||||
|
|
||||||
|
namespace NetworkRemoteSettingsConstants {
|
||||||
|
|
||||||
|
constexpr char kSettingsGroup[] = "NetworkRemote";
|
||||||
|
constexpr char kEnabled[] = "enabled";
|
||||||
|
constexpr char kPort[] = "port";
|
||||||
|
constexpr char kAllowPublicAccess[] = "allow_public_access";
|
||||||
|
constexpr char kUseAuthCode[] = "use_authcode";
|
||||||
|
constexpr char kAuthCode[] = "authcode";
|
||||||
|
constexpr char kFilesRootFolder[] = "files_root_folder";
|
||||||
|
|
||||||
|
} // namespace NetworkRemoteSettingsConstants
|
||||||
|
|
||||||
|
#endif // NETWORKREMOTESETTINGSCONSTANTS_H
|
||||||
@@ -253,7 +253,7 @@ void ContextView::AddActions() {
|
|||||||
|
|
||||||
action_search_lyrics_ = new QAction(tr("Automatically search for song lyrics"), this);
|
action_search_lyrics_ = new QAction(tr("Automatically search for song lyrics"), this);
|
||||||
action_search_lyrics_->setCheckable(true);
|
action_search_lyrics_->setCheckable(true);
|
||||||
action_search_lyrics_->setChecked(false);
|
action_search_lyrics_->setChecked(true);
|
||||||
|
|
||||||
menu_options_->addAction(action_show_album_);
|
menu_options_->addAction(action_show_album_);
|
||||||
menu_options_->addAction(action_show_data_);
|
menu_options_->addAction(action_show_data_);
|
||||||
@@ -287,7 +287,7 @@ void ContextView::ReloadSettings() {
|
|||||||
action_show_album_->setChecked(s.value(ContextSettings::kAlbum, true).toBool());
|
action_show_album_->setChecked(s.value(ContextSettings::kAlbum, true).toBool());
|
||||||
action_show_data_->setChecked(s.value(ContextSettings::kTechnicalData, false).toBool());
|
action_show_data_->setChecked(s.value(ContextSettings::kTechnicalData, false).toBool());
|
||||||
action_show_lyrics_->setChecked(s.value(ContextSettings::kSongLyrics, true).toBool());
|
action_show_lyrics_->setChecked(s.value(ContextSettings::kSongLyrics, true).toBool());
|
||||||
action_search_lyrics_->setChecked(s.value(ContextSettings::kSearchLyrics, false).toBool());
|
action_search_lyrics_->setChecked(s.value(ContextSettings::kSearchLyrics, true).toBool());
|
||||||
font_headline_.setFamily(s.value(ContextSettings::kFontHeadline, default_font).toString());
|
font_headline_.setFamily(s.value(ContextSettings::kFontHeadline, default_font).toString());
|
||||||
font_headline_.setPointSizeF(s.value(ContextSettings::kFontSizeHeadline, ContextSettings::kDefaultFontSizeHeadline).toReal());
|
font_headline_.setPointSizeF(s.value(ContextSettings::kFontSizeHeadline, ContextSettings::kDefaultFontSizeHeadline).toReal());
|
||||||
font_nosong_.setFamily(font_headline_.family());
|
font_nosong_.setFamily(font_headline_.family());
|
||||||
|
|||||||
@@ -110,6 +110,10 @@
|
|||||||
# include "moodbar/moodbarloader.h"
|
# include "moodbar/moodbarloader.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAVE_NETWORKREMOTE
|
||||||
|
# include "networkremote/networkremote.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "radios/radioservices.h"
|
#include "radios/radioservices.h"
|
||||||
#include "radios/radiobackend.h"
|
#include "radios/radiobackend.h"
|
||||||
|
|
||||||
@@ -216,6 +220,13 @@ class ApplicationImpl {
|
|||||||
#ifdef HAVE_MOODBAR
|
#ifdef HAVE_MOODBAR
|
||||||
moodbar_loader_([app]() { return new MoodbarLoader(app); }),
|
moodbar_loader_([app]() { return new MoodbarLoader(app); }),
|
||||||
moodbar_controller_([app]() { return new MoodbarController(app->player(), app->moodbar_loader()); }),
|
moodbar_controller_([app]() { return new MoodbarController(app->player(), app->moodbar_loader()); }),
|
||||||
|
#endif
|
||||||
|
#ifdef HAVE_NETWORKREMOTE
|
||||||
|
network_remote_([app]() {
|
||||||
|
NetworkRemote *networkremote = new NetworkRemote(app->database(), app->player(), app->collection_backend(), app->playlist_manager(), app->playlist_backend(), app->current_albumcover_loader(), app->scrobbler());
|
||||||
|
app->MoveToNewThread(networkremote);
|
||||||
|
return networkremote;
|
||||||
|
}),
|
||||||
#endif
|
#endif
|
||||||
lastfm_import_([app]() { return new LastFMImport(app->network()); })
|
lastfm_import_([app]() { return new LastFMImport(app->network()); })
|
||||||
{}
|
{}
|
||||||
@@ -241,6 +252,9 @@ class ApplicationImpl {
|
|||||||
#ifdef HAVE_MOODBAR
|
#ifdef HAVE_MOODBAR
|
||||||
Lazy<MoodbarLoader> moodbar_loader_;
|
Lazy<MoodbarLoader> moodbar_loader_;
|
||||||
Lazy<MoodbarController> moodbar_controller_;
|
Lazy<MoodbarController> moodbar_controller_;
|
||||||
|
#endif
|
||||||
|
#ifdef HAVE_NETWORKREMOTE
|
||||||
|
Lazy<NetworkRemote> network_remote_;
|
||||||
#endif
|
#endif
|
||||||
Lazy<LastFMImport> lastfm_import_;
|
Lazy<LastFMImport> lastfm_import_;
|
||||||
|
|
||||||
@@ -390,3 +404,6 @@ SharedPtr<LastFMImport> Application::lastfm_import() const { return p_->lastfm_i
|
|||||||
SharedPtr<MoodbarController> Application::moodbar_controller() const { return p_->moodbar_controller_.ptr(); }
|
SharedPtr<MoodbarController> Application::moodbar_controller() const { return p_->moodbar_controller_.ptr(); }
|
||||||
SharedPtr<MoodbarLoader> Application::moodbar_loader() const { return p_->moodbar_loader_.ptr(); }
|
SharedPtr<MoodbarLoader> Application::moodbar_loader() const { return p_->moodbar_loader_.ptr(); }
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef HAVE_NETWORKREMOTE
|
||||||
|
SharedPtr<NetworkRemote> Application::network_remote() const { return p_->network_remote_.ptr(); }
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ class RadioServices;
|
|||||||
class MoodbarController;
|
class MoodbarController;
|
||||||
class MoodbarLoader;
|
class MoodbarLoader;
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef HAVE_NETWORKREMOTE
|
||||||
|
class NetworkRemote;
|
||||||
|
#endif
|
||||||
|
|
||||||
class Application : public QObject {
|
class Application : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -103,6 +106,10 @@ class Application : public QObject {
|
|||||||
SharedPtr<MoodbarLoader> moodbar_loader() const;
|
SharedPtr<MoodbarLoader> moodbar_loader() const;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAVE_NETWORKREMOTE
|
||||||
|
SharedPtr<NetworkRemote> network_remote() const;
|
||||||
|
#endif
|
||||||
|
|
||||||
SharedPtr<LastFMImport> lastfm_import() const;
|
SharedPtr<LastFMImport> lastfm_import() const;
|
||||||
|
|
||||||
void Exit();
|
void Exit();
|
||||||
|
|||||||
24
src/core/bonjour.h
Normal file
24
src/core/bonjour.h
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#ifndef BONJOUR_H
|
||||||
|
#define BONJOUR_H
|
||||||
|
|
||||||
|
#include "zeroconf.h"
|
||||||
|
|
||||||
|
#ifdef __OBJC__
|
||||||
|
@class NetServicePublicationDelegate;
|
||||||
|
#else
|
||||||
|
class NetServicePublicationDelegate;
|
||||||
|
#endif // __OBJC__
|
||||||
|
|
||||||
|
class Bonjour : public Zeroconf {
|
||||||
|
public:
|
||||||
|
explicit Bonjour();
|
||||||
|
virtual ~Bonjour();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void PublishInternal(const QString &domain, const QString &type, const QByteArray &name, const quint16 port);
|
||||||
|
|
||||||
|
private:
|
||||||
|
NetServicePublicationDelegate *delegate_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // BONJOUR_H
|
||||||
57
src/core/bonjour.mm
Normal file
57
src/core/bonjour.mm
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
#include "bonjour.h"
|
||||||
|
|
||||||
|
#import <Foundation/NSNetServices.h>
|
||||||
|
#import <Foundation/NSString.h>
|
||||||
|
|
||||||
|
#include "core/logging.h"
|
||||||
|
#include "core/scoped_nsautorelease_pool.h"
|
||||||
|
|
||||||
|
@interface NetServicePublicationDelegate : NSObject <NSNetServiceDelegate> {}
|
||||||
|
|
||||||
|
- (void)netServiceWillPublish:(NSNetService*)netService;
|
||||||
|
- (void)netService:(NSNetService*)netService didNotPublish:(NSDictionary*)errorDict;
|
||||||
|
- (void)netServiceDidStop:(NSNetService*)netService;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation NetServicePublicationDelegate
|
||||||
|
|
||||||
|
- (void)netServiceWillPublish:(NSNetService*)netService {
|
||||||
|
qLog(Debug) << "Publishing:" << [[netService name] UTF8String];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)netService:(NSNetService*)netServie didNotPublish:(NSDictionary*)errorDict {
|
||||||
|
qLog(Debug) << "Failed to publish remote service with Bonjour";
|
||||||
|
NSLog(@"%@", errorDict);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)netServiceDidStop:(NSNetService*)netService {
|
||||||
|
qLog(Debug) << "Unpublished:" << [[netService name] UTF8String];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
NSString* NSStringFromQString(const QString& s) {
|
||||||
|
return [[NSString alloc] initWithUTF8String:s.toUtf8().constData()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bonjour::Bonjour() : delegate_([[NetServicePublicationDelegate alloc] init]) {}
|
||||||
|
|
||||||
|
Bonjour::~Bonjour() { [delegate_ release]; }
|
||||||
|
|
||||||
|
void Bonjour::PublishInternal(const QString& domain, const QString& type, const QByteArray& name, const quint16 port) {
|
||||||
|
ScopedNSAutoreleasePool pool;
|
||||||
|
NSNetService* service =
|
||||||
|
[[NSNetService alloc] initWithDomain:NSStringFromQString(domain)
|
||||||
|
type:NSStringFromQString(type)
|
||||||
|
name:[NSString stringWithUTF8String:name.constData()]
|
||||||
|
port:port];
|
||||||
|
if (service) {
|
||||||
|
[service setDelegate:delegate_];
|
||||||
|
[service publish];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -52,12 +52,6 @@ LocalRedirectServer::~LocalRedirectServer() {
|
|||||||
|
|
||||||
bool LocalRedirectServer::Listen() {
|
bool LocalRedirectServer::Listen() {
|
||||||
|
|
||||||
#ifdef BUILD_FOR_MAC_APP_STORE
|
|
||||||
success_ = false;
|
|
||||||
error_ = "Local redirect server is disabled in Mac App Store builds."_L1;
|
|
||||||
return false;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (!listen(QHostAddress::LocalHost, static_cast<quint64>(port_))) {
|
if (!listen(QHostAddress::LocalHost, static_cast<quint64>(port_))) {
|
||||||
success_ = false;
|
success_ = false;
|
||||||
error_ = errorString();
|
error_ = errorString();
|
||||||
|
|||||||
@@ -173,12 +173,9 @@
|
|||||||
#endif
|
#endif
|
||||||
#ifdef HAVE_SPOTIFY
|
#ifdef HAVE_SPOTIFY
|
||||||
# include "spotify/spotifyservice.h"
|
# include "spotify/spotifyservice.h"
|
||||||
# include "spotify/spotifymetadatarequest.h"
|
|
||||||
# include "constants/spotifysettings.h"
|
# include "constants/spotifysettings.h"
|
||||||
#endif
|
#endif
|
||||||
#ifdef HAVE_QOBUZ
|
#ifdef HAVE_QOBUZ
|
||||||
# include "qobuz/qobuzservice.h"
|
|
||||||
# include "qobuz/qobuzmetadatarequest.h"
|
|
||||||
# include "constants/qobuzsettings.h"
|
# include "constants/qobuzsettings.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -382,10 +379,8 @@ MainWindow::MainWindow(Application *app,
|
|||||||
playlist_add_to_another_(nullptr),
|
playlist_add_to_another_(nullptr),
|
||||||
playlistitem_actions_separator_(nullptr),
|
playlistitem_actions_separator_(nullptr),
|
||||||
playlist_rescan_songs_(nullptr),
|
playlist_rescan_songs_(nullptr),
|
||||||
playlist_fetch_metadata_(nullptr),
|
|
||||||
track_position_timer_(new QTimer(this)),
|
track_position_timer_(new QTimer(this)),
|
||||||
track_slider_timer_(new QTimer(this)),
|
track_slider_timer_(new QTimer(this)),
|
||||||
metadata_queue_timer_(new QTimer(this)),
|
|
||||||
keep_running_(false),
|
keep_running_(false),
|
||||||
playing_widget_(true),
|
playing_widget_(true),
|
||||||
#ifdef HAVE_DBUS
|
#ifdef HAVE_DBUS
|
||||||
@@ -457,10 +452,6 @@ MainWindow::MainWindow(Application *app,
|
|||||||
track_slider_timer_->setInterval(kTrackSliderUpdateTimeMs);
|
track_slider_timer_->setInterval(kTrackSliderUpdateTimeMs);
|
||||||
QObject::connect(track_slider_timer_, &QTimer::timeout, this, &MainWindow::UpdateTrackSliderPosition);
|
QObject::connect(track_slider_timer_, &QTimer::timeout, this, &MainWindow::UpdateTrackSliderPosition);
|
||||||
|
|
||||||
metadata_queue_timer_->setInterval(200ms); // 200ms between requests to avoid rate limiting
|
|
||||||
metadata_queue_timer_->setSingleShot(true);
|
|
||||||
QObject::connect(metadata_queue_timer_, &QTimer::timeout, this, &MainWindow::ProcessMetadataQueue);
|
|
||||||
|
|
||||||
// Start initializing the player
|
// Start initializing the player
|
||||||
qLog(Debug) << "Initializing player";
|
qLog(Debug) << "Initializing player";
|
||||||
app_->player()->SetAnalyzer(ui_->analyzer);
|
app_->player()->SetAnalyzer(ui_->analyzer);
|
||||||
@@ -821,8 +812,6 @@ MainWindow::MainWindow(Application *app,
|
|||||||
#endif
|
#endif
|
||||||
playlist_rescan_songs_ = playlist_menu_->addAction(IconLoader::Load(u"view-refresh"_s), tr("Rescan song(s)..."), this, &MainWindow::RescanSongs);
|
playlist_rescan_songs_ = playlist_menu_->addAction(IconLoader::Load(u"view-refresh"_s), tr("Rescan song(s)..."), this, &MainWindow::RescanSongs);
|
||||||
playlist_menu_->addAction(playlist_rescan_songs_);
|
playlist_menu_->addAction(playlist_rescan_songs_);
|
||||||
playlist_fetch_metadata_ = playlist_menu_->addAction(IconLoader::Load(u"download"_s), tr("Fetch metadata from service"), this, &MainWindow::FetchStreamingMetadata);
|
|
||||||
playlist_menu_->addAction(playlist_fetch_metadata_);
|
|
||||||
playlist_menu_->addAction(ui_->action_add_files_to_transcoder);
|
playlist_menu_->addAction(ui_->action_add_files_to_transcoder);
|
||||||
playlist_menu_->addSeparator();
|
playlist_menu_->addSeparator();
|
||||||
playlist_copy_url_ = playlist_menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Copy URL(s)..."), this, &MainWindow::PlaylistCopyUrl);
|
playlist_copy_url_ = playlist_menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Copy URL(s)..."), this, &MainWindow::PlaylistCopyUrl);
|
||||||
@@ -988,6 +977,10 @@ MainWindow::MainWindow(Application *app,
|
|||||||
ui_->action_open_cd->setVisible(false);
|
ui_->action_open_cd->setVisible(false);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAVE_NETWORKREMOTE
|
||||||
|
app_->network_remote();
|
||||||
|
#endif
|
||||||
|
|
||||||
// Load settings
|
// Load settings
|
||||||
qLog(Debug) << "Loading settings";
|
qLog(Debug) << "Loading settings";
|
||||||
Settings settings;
|
Settings settings;
|
||||||
@@ -1142,7 +1135,18 @@ MainWindow::MainWindow(Application *app,
|
|||||||
asked_permission = s.value("asked_permission", false).toBool();
|
asked_permission = s.value("asked_permission", false).toBool();
|
||||||
s.endGroup();
|
s.endGroup();
|
||||||
#endif
|
#endif
|
||||||
Q_UNUSED(asked_permission)
|
if (asked_permission) {
|
||||||
|
s.beginGroup(MainWindowSettings::kSettingsGroup);
|
||||||
|
const bool do_not_show_sponsor_message = s.value(MainWindowSettings::kDoNotShowSponsorMessage, false).toBool();
|
||||||
|
s.endGroup();
|
||||||
|
if (!do_not_show_sponsor_message) {
|
||||||
|
MessageDialog *sponsor_message = new MessageDialog(this);
|
||||||
|
sponsor_message->set_settings_group(QLatin1String(MainWindowSettings::kSettingsGroup));
|
||||||
|
sponsor_message->set_do_not_show_message_again(QLatin1String(MainWindowSettings::kDoNotShowSponsorMessage));
|
||||||
|
sponsor_message->setAttribute(Qt::WA_DeleteOnClose);
|
||||||
|
sponsor_message->ShowMessage(tr("Sponsoring Strawberry"), tr("Strawberry is free and open source software. If you like Strawberry, please consider sponsoring the project. For more information about sponsorship see our website %1").arg(u"<a href= \"https://www.strawberrymusicplayer.org/\">www.strawberrymusicplayer.org</a>"_s), IconLoader::Load(u"dialog-information"_s));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
qLog(Debug) << "Started" << QThread::currentThread();
|
qLog(Debug) << "Started" << QThread::currentThread();
|
||||||
@@ -1229,7 +1233,7 @@ void MainWindow::ReloadSettings() {
|
|||||||
osd_->ReloadSettings();
|
osd_->ReloadSettings();
|
||||||
|
|
||||||
s.beginGroup(MainWindowSettings::kSettingsGroup);
|
s.beginGroup(MainWindowSettings::kSettingsGroup);
|
||||||
album_cover_choice_controller_->search_cover_auto_action()->setChecked(s.value(MainWindowSettings::kSearchForCoverAuto, false).toBool());
|
album_cover_choice_controller_->search_cover_auto_action()->setChecked(s.value(MainWindowSettings::kSearchForCoverAuto, true).toBool());
|
||||||
s.endGroup();
|
s.endGroup();
|
||||||
|
|
||||||
#ifdef HAVE_SUBSONIC
|
#ifdef HAVE_SUBSONIC
|
||||||
@@ -1995,7 +1999,6 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
|
|||||||
int in_skipped = 0;
|
int in_skipped = 0;
|
||||||
int not_in_skipped = 0;
|
int not_in_skipped = 0;
|
||||||
int local_songs = 0;
|
int local_songs = 0;
|
||||||
int streaming_songs = 0;
|
|
||||||
|
|
||||||
for (const QModelIndex &idx : selection) {
|
for (const QModelIndex &idx : selection) {
|
||||||
|
|
||||||
@@ -2005,13 +2008,7 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
|
|||||||
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(src_idx.row());
|
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(src_idx.row());
|
||||||
if (!item) continue;
|
if (!item) continue;
|
||||||
|
|
||||||
if (item->EffectiveMetadata().url().isLocalFile()) {
|
if (item->EffectiveMetadata().url().isLocalFile()) ++local_songs;
|
||||||
++local_songs;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item->EffectiveMetadata().is_stream_service()) {
|
|
||||||
++streaming_songs;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item->EffectiveMetadata().has_cue()) {
|
if (item->EffectiveMetadata().has_cue()) {
|
||||||
cue_selected = true;
|
cue_selected = true;
|
||||||
@@ -2039,9 +2036,6 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
|
|||||||
playlist_rescan_songs_->setEnabled(local_songs > 0 && editable > 0);
|
playlist_rescan_songs_->setEnabled(local_songs > 0 && editable > 0);
|
||||||
playlist_rescan_songs_->setVisible(local_songs > 0 && editable > 0);
|
playlist_rescan_songs_->setVisible(local_songs > 0 && editable > 0);
|
||||||
|
|
||||||
playlist_fetch_metadata_->setEnabled(streaming_songs > 0);
|
|
||||||
playlist_fetch_metadata_->setVisible(streaming_songs > 0);
|
|
||||||
|
|
||||||
ui_->action_add_files_to_transcoder->setEnabled(local_songs > 0 && editable > 0);
|
ui_->action_add_files_to_transcoder->setEnabled(local_songs > 0 && editable > 0);
|
||||||
ui_->action_add_files_to_transcoder->setVisible(local_songs > 0 && editable > 0);
|
ui_->action_add_files_to_transcoder->setVisible(local_songs > 0 && editable > 0);
|
||||||
|
|
||||||
@@ -2253,23 +2247,9 @@ void MainWindow::EditTracks() {
|
|||||||
void MainWindow::EditTagDialogAccepted() {
|
void MainWindow::EditTagDialogAccepted() {
|
||||||
|
|
||||||
const PlaylistItemPtrList items = edit_tag_dialog_->playlist_items();
|
const PlaylistItemPtrList items = edit_tag_dialog_->playlist_items();
|
||||||
const SongList songs = edit_tag_dialog_->songs();
|
for (PlaylistItemPtr item : items) {
|
||||||
|
|
||||||
if (items.count() != songs.count()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < items.count(); ++i) {
|
|
||||||
PlaylistItemPtr item = items[i];
|
|
||||||
const Song &updated_song = songs[i];
|
|
||||||
// For stream tracks, apply the metadata directly since there's no file to reload from
|
|
||||||
if (updated_song.is_stream_service()) {
|
|
||||||
item->SetOriginalMetadata(updated_song);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
item->Reload();
|
item->Reload();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: This is really lame but we don't know what rows have changed.
|
// FIXME: This is really lame but we don't know what rows have changed.
|
||||||
ui_->playlist->view()->update();
|
ui_->playlist->view()->update();
|
||||||
@@ -2343,8 +2323,8 @@ void MainWindow::SelectionSetValue() {
|
|||||||
QObject::disconnect(*connection);
|
QObject::disconnect(*connection);
|
||||||
}, Qt::QueuedConnection);
|
}, Qt::QueuedConnection);
|
||||||
}
|
}
|
||||||
else if (song.is_stream()) {
|
else if (song.source() == Song::Source::Stream) {
|
||||||
app_->playlist_manager()->current()->setData(source_index.sibling(source_index.row(), static_cast<int>(column)), column_value, 0);
|
app_->playlist_manager()->current()->setData(source_index, column_value, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3428,172 +3408,3 @@ void MainWindow::FocusSearchField() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::FetchStreamingMetadata() {
|
|
||||||
|
|
||||||
const QModelIndexList proxy_indexes = ui_->playlist->view()->selectionModel()->selectedRows();
|
|
||||||
for (const QModelIndex &proxy_index : proxy_indexes) {
|
|
||||||
const QModelIndex source_index = app_->playlist_manager()->current()->filter()->mapToSource(proxy_index);
|
|
||||||
if (!source_index.isValid()) continue;
|
|
||||||
PlaylistItemPtr item(app_->playlist_manager()->current()->item_at(source_index.row()));
|
|
||||||
if (!item) continue;
|
|
||||||
|
|
||||||
const Song &song = item->EffectiveMetadata();
|
|
||||||
const QPersistentModelIndex persistent_index = QPersistentModelIndex(source_index);
|
|
||||||
QString track_id;
|
|
||||||
|
|
||||||
#ifdef HAVE_QOBUZ
|
|
||||||
if (song.source() == Song::Source::Qobuz) {
|
|
||||||
track_id = song.song_id();
|
|
||||||
// song_id() may be empty if not persisted, fall back to URL path
|
|
||||||
if (track_id.isEmpty()) {
|
|
||||||
track_id = song.url().path();
|
|
||||||
}
|
|
||||||
if (track_id.isEmpty()) {
|
|
||||||
qLog(Error) << "Failed to fetch Qobuz metadata: No track ID";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef HAVE_SPOTIFY
|
|
||||||
if (song.source() == Song::Source::Spotify) {
|
|
||||||
track_id = song.song_id();
|
|
||||||
// song_id() may be empty if not persisted, fall back to parsing URL
|
|
||||||
if (track_id.isEmpty() && song.url().scheme() == "spotify"_L1 && song.url().path().startsWith(u"track:"_s)) {
|
|
||||||
track_id = song.url().path().mid(6);
|
|
||||||
}
|
|
||||||
if (track_id.isEmpty()) {
|
|
||||||
qLog(Error) << "Failed to fetch Spotify metadata: No track ID";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (!track_id.isEmpty()) {
|
|
||||||
metadata_queue_.append({song.source(), track_id, persistent_index});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start processing the queue if it's not already running
|
|
||||||
if (!metadata_queue_.isEmpty() && !metadata_queue_timer_->isActive()) {
|
|
||||||
ProcessMetadataQueue();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::ProcessMetadataQueue() {
|
|
||||||
|
|
||||||
if (metadata_queue_.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MetadataQueueEntry metadata_queue_entry = metadata_queue_.takeFirst();
|
|
||||||
|
|
||||||
#ifdef HAVE_QOBUZ
|
|
||||||
if (metadata_queue_entry.source == Song::Source::Qobuz) {
|
|
||||||
if (QobuzServicePtr qobuz_service = app_->streaming_services()->Service<QobuzService>()) {
|
|
||||||
QobuzMetadataRequest *request = new QobuzMetadataRequest(&*qobuz_service, qobuz_service->network());
|
|
||||||
QObject::connect(request, &QobuzMetadataRequest::MetadataReceived, this, [this, metadata_queue_entry, request](const QString &received_track_id, const Song &fetched_song) {
|
|
||||||
Q_UNUSED(received_track_id);
|
|
||||||
if (metadata_queue_entry.persistent_index.isValid() && fetched_song.is_valid()) {
|
|
||||||
PlaylistItemPtr playlist_item = app_->playlist_manager()->current()->item_at(metadata_queue_entry.persistent_index.row());
|
|
||||||
if (playlist_item) {
|
|
||||||
const Song old_song = playlist_item->OriginalMetadata();
|
|
||||||
Song updated_song = old_song;
|
|
||||||
// Update all metadata fields from the fetched song
|
|
||||||
if (!fetched_song.title().isEmpty()) updated_song.set_title(fetched_song.title());
|
|
||||||
if (!fetched_song.artist().isEmpty()) updated_song.set_artist(fetched_song.artist());
|
|
||||||
if (!fetched_song.album().isEmpty()) updated_song.set_album(fetched_song.album());
|
|
||||||
if (!fetched_song.albumartist().isEmpty()) updated_song.set_albumartist(fetched_song.albumartist());
|
|
||||||
if (!fetched_song.genre().isEmpty()) updated_song.set_genre(fetched_song.genre());
|
|
||||||
if (!fetched_song.composer().isEmpty()) updated_song.set_composer(fetched_song.composer());
|
|
||||||
if (!fetched_song.performer().isEmpty()) updated_song.set_performer(fetched_song.performer());
|
|
||||||
if (!fetched_song.comment().isEmpty()) updated_song.set_comment(fetched_song.comment());
|
|
||||||
if (fetched_song.track() > 0) updated_song.set_track(fetched_song.track());
|
|
||||||
if (fetched_song.disc() > 0) updated_song.set_disc(fetched_song.disc());
|
|
||||||
if (fetched_song.year() > 0) updated_song.set_year(fetched_song.year());
|
|
||||||
if (fetched_song.length_nanosec() > 0) updated_song.set_length_nanosec(fetched_song.length_nanosec());
|
|
||||||
if (fetched_song.art_automatic().isValid()) updated_song.set_art_automatic(fetched_song.art_automatic());
|
|
||||||
playlist_item->SetOriginalMetadata(updated_song);
|
|
||||||
app_->playlist_manager()->current()->ItemReload(metadata_queue_entry.persistent_index, old_song, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
request->deleteLater();
|
|
||||||
// Process next item in queue
|
|
||||||
if (!metadata_queue_.isEmpty()) {
|
|
||||||
metadata_queue_timer_->start();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
QObject::connect(request, &QobuzMetadataRequest::MetadataFailure, this, [this, request](const QString &failed_track_id, const QString &error) {
|
|
||||||
Q_UNUSED(failed_track_id);
|
|
||||||
qLog(Error) << "Failed to fetch Qobuz metadata:" << error;
|
|
||||||
request->deleteLater();
|
|
||||||
// Process next item in queue
|
|
||||||
if (!metadata_queue_.isEmpty()) {
|
|
||||||
metadata_queue_timer_->start();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
request->FetchTrackMetadata(metadata_queue_entry.track_id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef HAVE_SPOTIFY
|
|
||||||
if (metadata_queue_entry.source == Song::Source::Spotify) {
|
|
||||||
if (SpotifyServicePtr spotify_service = app_->streaming_services()->Service<SpotifyService>()) {
|
|
||||||
SpotifyMetadataRequest *request = new SpotifyMetadataRequest(&*spotify_service, app_->network());
|
|
||||||
QObject::connect(request, &SpotifyMetadataRequest::MetadataReceived, this, [this, metadata_queue_entry, request](const QString &received_track_id, const Song &fetched_song) {
|
|
||||||
Q_UNUSED(received_track_id);
|
|
||||||
if (metadata_queue_entry.persistent_index.isValid() && fetched_song.is_valid()) {
|
|
||||||
PlaylistItemPtr playlist_item = app_->playlist_manager()->current()->item_at(metadata_queue_entry.persistent_index.row());
|
|
||||||
if (playlist_item) {
|
|
||||||
const Song old_song = playlist_item->OriginalMetadata();
|
|
||||||
Song updated_song = old_song;
|
|
||||||
// Update all metadata fields from the fetched song
|
|
||||||
if (!fetched_song.title().isEmpty()) updated_song.set_title(fetched_song.title());
|
|
||||||
if (!fetched_song.artist().isEmpty()) updated_song.set_artist(fetched_song.artist());
|
|
||||||
if (!fetched_song.album().isEmpty()) updated_song.set_album(fetched_song.album());
|
|
||||||
if (!fetched_song.albumartist().isEmpty()) updated_song.set_albumartist(fetched_song.albumartist());
|
|
||||||
if (!fetched_song.genre().isEmpty()) updated_song.set_genre(fetched_song.genre());
|
|
||||||
if (!fetched_song.composer().isEmpty()) updated_song.set_composer(fetched_song.composer());
|
|
||||||
if (!fetched_song.performer().isEmpty()) updated_song.set_performer(fetched_song.performer());
|
|
||||||
if (!fetched_song.comment().isEmpty()) updated_song.set_comment(fetched_song.comment());
|
|
||||||
if (fetched_song.track() > 0) updated_song.set_track(fetched_song.track());
|
|
||||||
if (fetched_song.disc() > 0) updated_song.set_disc(fetched_song.disc());
|
|
||||||
if (fetched_song.year() > 0) updated_song.set_year(fetched_song.year());
|
|
||||||
if (fetched_song.length_nanosec() > 0) updated_song.set_length_nanosec(fetched_song.length_nanosec());
|
|
||||||
if (fetched_song.art_automatic().isValid()) updated_song.set_art_automatic(fetched_song.art_automatic());
|
|
||||||
playlist_item->SetOriginalMetadata(updated_song);
|
|
||||||
app_->playlist_manager()->current()->ItemReload(metadata_queue_entry.persistent_index, old_song, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
request->deleteLater();
|
|
||||||
// Process next item in queue
|
|
||||||
if (!metadata_queue_.isEmpty()) {
|
|
||||||
metadata_queue_timer_->start();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
QObject::connect(request, &SpotifyMetadataRequest::MetadataFailure, this, [this, request](const QString &failed_track_id, const QString &error) {
|
|
||||||
Q_UNUSED(failed_track_id);
|
|
||||||
qLog(Error) << "Failed to fetch Spotify metadata:" << error;
|
|
||||||
request->deleteLater();
|
|
||||||
// Process next item in queue
|
|
||||||
if (!metadata_queue_.isEmpty()) {
|
|
||||||
metadata_queue_timer_->start();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
request->FetchTrackMetadata(metadata_queue_entry.track_id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// If we get here, the source wasn't handled - try the next item
|
|
||||||
if (!metadata_queue_.isEmpty()) {
|
|
||||||
metadata_queue_timer_->start();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
|
#include <QPixmap>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QtEvents>
|
#include <QtEvents>
|
||||||
|
|
||||||
@@ -245,6 +246,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
|||||||
void ToggleSearchCoverAuto(const bool checked);
|
void ToggleSearchCoverAuto(const bool checked);
|
||||||
void SaveGeometry();
|
void SaveGeometry();
|
||||||
|
|
||||||
|
void Exit();
|
||||||
void DoExit();
|
void DoExit();
|
||||||
|
|
||||||
void HandleNotificationPreview(const OSDSettings::Type type, const QString &line1, const QString &line2);
|
void HandleNotificationPreview(const OSDSettings::Type type, const QString &line1, const QString &line2);
|
||||||
@@ -276,13 +278,9 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
|||||||
|
|
||||||
void DeleteFilesFinished(const SongList &songs_with_errors);
|
void DeleteFilesFinished(const SongList &songs_with_errors);
|
||||||
|
|
||||||
void FetchStreamingMetadata();
|
|
||||||
void ProcessMetadataQueue();
|
|
||||||
|
|
||||||
public Q_SLOTS:
|
public Q_SLOTS:
|
||||||
void CommandlineOptionsReceived(const QByteArray &string_options);
|
void CommandlineOptionsReceived(const QByteArray &string_options);
|
||||||
void Raise();
|
void Raise();
|
||||||
void Exit();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void SaveSettings();
|
void SaveSettings();
|
||||||
@@ -292,6 +290,9 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
|||||||
|
|
||||||
void CheckFullRescanRevisions();
|
void CheckFullRescanRevisions();
|
||||||
|
|
||||||
|
// creates the icon by painting the full one depending on the current position
|
||||||
|
QPixmap CreateOverlayedIcon(const int position, const int scrobble_point);
|
||||||
|
|
||||||
void GetCoverAutomatically();
|
void GetCoverAutomatically();
|
||||||
|
|
||||||
void SetToggleScrobblingIcon(const bool value);
|
void SetToggleScrobblingIcon(const bool value);
|
||||||
@@ -382,13 +383,11 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
|||||||
QList<QAction*> playlistitem_actions_;
|
QList<QAction*> playlistitem_actions_;
|
||||||
QAction *playlistitem_actions_separator_;
|
QAction *playlistitem_actions_separator_;
|
||||||
QAction *playlist_rescan_songs_;
|
QAction *playlist_rescan_songs_;
|
||||||
QAction *playlist_fetch_metadata_;
|
|
||||||
|
|
||||||
QModelIndex playlist_menu_index_;
|
QModelIndex playlist_menu_index_;
|
||||||
|
|
||||||
QTimer *track_position_timer_;
|
QTimer *track_position_timer_;
|
||||||
QTimer *track_slider_timer_;
|
QTimer *track_slider_timer_;
|
||||||
QTimer *metadata_queue_timer_;
|
|
||||||
|
|
||||||
bool keep_running_;
|
bool keep_running_;
|
||||||
bool playing_widget_;
|
bool playing_widget_;
|
||||||
@@ -412,14 +411,6 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
|||||||
bool playlists_loaded_;
|
bool playlists_loaded_;
|
||||||
bool delete_files_;
|
bool delete_files_;
|
||||||
std::optional<CommandlineOptions> options_;
|
std::optional<CommandlineOptions> options_;
|
||||||
|
|
||||||
class MetadataQueueEntry {
|
|
||||||
public:
|
|
||||||
Song::Source source;
|
|
||||||
QString track_id;
|
|
||||||
QPersistentModelIndex persistent_index;
|
|
||||||
};
|
|
||||||
QList<MetadataQueueEntry> metadata_queue_;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // MAINWINDOW_H
|
#endif // MAINWINDOW_H
|
||||||
|
|||||||
@@ -34,13 +34,6 @@
|
|||||||
|
|
||||||
#include "mergedproxymodel.h"
|
#include "mergedproxymodel.h"
|
||||||
|
|
||||||
#ifdef __GNUC__
|
|
||||||
#pragma GCC diagnostic push
|
|
||||||
#if __GNUC__ >= 16
|
|
||||||
#pragma GCC diagnostic ignored "-Wstringop-overflow"
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include <boost/multi_index/detail/bidir_node_iterator.hpp>
|
#include <boost/multi_index/detail/bidir_node_iterator.hpp>
|
||||||
#include <boost/multi_index/detail/hash_index_iterator.hpp>
|
#include <boost/multi_index/detail/hash_index_iterator.hpp>
|
||||||
#include <boost/multi_index/hashed_index.hpp>
|
#include <boost/multi_index/hashed_index.hpp>
|
||||||
@@ -52,10 +45,6 @@
|
|||||||
#include <boost/multi_index_container.hpp>
|
#include <boost/multi_index_container.hpp>
|
||||||
#include <boost/operators.hpp>
|
#include <boost/operators.hpp>
|
||||||
|
|
||||||
#ifdef __GNUC__
|
|
||||||
#pragma GCC diagnostic pop
|
|
||||||
#endif
|
|
||||||
|
|
||||||
using boost::multi_index::hashed_unique;
|
using boost::multi_index::hashed_unique;
|
||||||
using boost::multi_index::identity;
|
using boost::multi_index::identity;
|
||||||
using boost::multi_index::indexed_by;
|
using boost::multi_index::indexed_by;
|
||||||
|
|||||||
@@ -25,14 +25,15 @@
|
|||||||
|
|
||||||
#include "mimedata.h"
|
#include "mimedata.h"
|
||||||
|
|
||||||
MimeData::MimeData(const bool clear, const bool play_now, const bool enqueue, const bool enqueue_next_now, const bool open_in_new_playlist, QObject *parent)
|
MimeData::MimeData(const bool clear, const bool play_now, const bool enqueue, const bool enqueue_next_now, const bool open_in_new_playlist, const int playlist_id, QObject *parent)
|
||||||
: override_user_settings_(false),
|
: override_user_settings_(false),
|
||||||
clear_first_(clear),
|
clear_first_(clear),
|
||||||
play_now_(play_now),
|
play_now_(play_now),
|
||||||
enqueue_now_(enqueue),
|
enqueue_now_(enqueue),
|
||||||
enqueue_next_now_(enqueue_next_now),
|
enqueue_next_now_(enqueue_next_now),
|
||||||
open_in_new_playlist_(open_in_new_playlist),
|
open_in_new_playlist_(open_in_new_playlist),
|
||||||
from_doubleclick_(false) {
|
from_doubleclick_(false),
|
||||||
|
playlist_id_(playlist_id) {
|
||||||
|
|
||||||
Q_UNUSED(parent);
|
Q_UNUSED(parent);
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class MimeData : public QMimeData {
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit MimeData(const bool clear = false, const bool play_now = false, const bool enqueue = false, const bool enqueue_next_now = false, const bool open_in_new_playlist = false, QObject *parent = nullptr);
|
explicit MimeData(const bool clear = false, const bool play_now = false, const bool enqueue = false, const bool enqueue_next_now = false, const bool open_in_new_playlist = false, const int playlist_id = -1, QObject *parent = nullptr);
|
||||||
|
|
||||||
// If this is set then MainWindow will not touch any of the other flags.
|
// If this is set then MainWindow will not touch any of the other flags.
|
||||||
bool override_user_settings_;
|
bool override_user_settings_;
|
||||||
@@ -57,6 +57,9 @@ class MimeData : public QMimeData {
|
|||||||
// The MainWindow will set the above flags to the defaults set by the user.
|
// The MainWindow will set the above flags to the defaults set by the user.
|
||||||
bool from_doubleclick_;
|
bool from_doubleclick_;
|
||||||
|
|
||||||
|
// The Network Remote can use this MimeData to drop songs on another playlist than the one currently opened on the server
|
||||||
|
int playlist_id_;
|
||||||
|
|
||||||
// Returns a pretty name for a playlist containing songs described by this MimeData object.
|
// Returns a pretty name for a playlist containing songs described by this MimeData object.
|
||||||
// By pretty name we mean the value of 'name_for_new_playlist_' or generic "Playlist" string if the 'name_for_new_playlist_' attribute is empty.
|
// By pretty name we mean the value of 'name_for_new_playlist_' or generic "Playlist" string if the 'name_for_new_playlist_' attribute is empty.
|
||||||
QString get_name_for_new_playlist() const;
|
QString get_name_for_new_playlist() const;
|
||||||
|
|||||||
@@ -236,14 +236,6 @@ void OAuthenticator::Authenticate() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mac App Store builds: do not start any localhost listening redirect server.
|
|
||||||
#ifdef BUILD_FOR_MAC_APP_STORE
|
|
||||||
if (use_local_redirect_server_) {
|
|
||||||
Q_EMIT AuthenticationFinished(false, tr("This authentication flow is disabled in Mac App Store builds."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
QUrl redirect_url(redirect_url_);
|
QUrl redirect_url(redirect_url_);
|
||||||
|
|
||||||
if (use_local_redirect_server_) {
|
if (use_local_redirect_server_) {
|
||||||
|
|||||||
@@ -686,12 +686,11 @@ const QString &Song::playlist_effective_albumartistsort() const { return is_comp
|
|||||||
bool Song::is_metadata_good() const { return !d->url_.isEmpty() && !d->artist_.isEmpty() && !d->title_.isEmpty(); }
|
bool Song::is_metadata_good() const { return !d->url_.isEmpty() && !d->artist_.isEmpty() && !d->title_.isEmpty(); }
|
||||||
bool Song::is_local_collection_song() const { return d->source_ == Source::Collection; }
|
bool Song::is_local_collection_song() const { return d->source_ == Source::Collection; }
|
||||||
bool Song::is_linked_collection_song() const { return IsLinkedCollectionSource(d->source_); }
|
bool Song::is_linked_collection_song() const { return IsLinkedCollectionSource(d->source_); }
|
||||||
|
bool Song::is_stream() const { return is_radio() || d->source_ == Source::Tidal || d->source_ == Source::Subsonic || d->source_ == Source::Qobuz || d->source_ == Source::Spotify; }
|
||||||
bool Song::is_radio() const { return d->source_ == Source::Stream || d->source_ == Source::SomaFM || d->source_ == Source::RadioParadise; }
|
bool Song::is_radio() const { return d->source_ == Source::Stream || d->source_ == Source::SomaFM || d->source_ == Source::RadioParadise; }
|
||||||
bool Song::is_stream_service() const { return d->source_ == Source::Subsonic || d->source_ == Source::Tidal || d->source_ == Source::Qobuz || d->source_ == Source::Spotify; }
|
|
||||||
bool Song::is_stream() const { return is_radio() || is_stream_service(); }
|
|
||||||
bool Song::is_cdda() const { return d->source_ == Source::CDDA; }
|
bool Song::is_cdda() const { return d->source_ == Source::CDDA; }
|
||||||
bool Song::is_compilation() const { return (d->compilation_ || d->compilation_detected_ || d->compilation_on_) && !d->compilation_off_; }
|
bool Song::is_compilation() const { return (d->compilation_ || d->compilation_detected_ || d->compilation_on_) && !d->compilation_off_; }
|
||||||
bool Song::stream_url_can_expire() const { return d->source_ == Source::Tidal || d->source_ == Source::Qobuz; }
|
bool Song::stream_url_can_expire() const { return d->source_ == Source::Tidal || d->source_ == Source::Qobuz || d->source_ == Source::Spotify; }
|
||||||
bool Song::is_module_music() const { return d->filetype_ == FileType::MOD || d->filetype_ == FileType::S3M || d->filetype_ == FileType::XM || d->filetype_ == FileType::IT; }
|
bool Song::is_module_music() const { return d->filetype_ == FileType::MOD || d->filetype_ == FileType::S3M || d->filetype_ == FileType::XM || d->filetype_ == FileType::IT; }
|
||||||
bool Song::has_cue() const { return !d->cue_path_.isEmpty(); }
|
bool Song::has_cue() const { return !d->cue_path_.isEmpty(); }
|
||||||
|
|
||||||
@@ -957,7 +956,7 @@ QString Song::PrettyRating() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Song::IsEditable() const {
|
bool Song::IsEditable() const {
|
||||||
return d->valid_ && d->url_.isValid() && ((d->url_.isLocalFile() && write_tags_supported() && !has_cue()) || is_stream());
|
return d->valid_ && d->url_.isValid() && ((d->url_.isLocalFile() && write_tags_supported() && !has_cue()) || d->source_ == Source::Stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Song::IsFileInfoEqual(const Song &other) const {
|
bool Song::IsFileInfoEqual(const Song &other) const {
|
||||||
@@ -1669,24 +1668,12 @@ void Song::InitArtManual() {
|
|||||||
void Song::InitArtAutomatic() {
|
void Song::InitArtAutomatic() {
|
||||||
|
|
||||||
if (d->art_automatic_.isEmpty() && d->source_ == Source::LocalFile && d->url_.isLocalFile()) {
|
if (d->art_automatic_.isEmpty() && d->source_ == Source::LocalFile && d->url_.isLocalFile()) {
|
||||||
const QFileInfo fileinfo(d->url_.toLocalFile());
|
// Pick the first image file in the album directory.
|
||||||
const QDir dir(fileinfo.path());
|
QFileInfo file(d->url_.toLocalFile());
|
||||||
const QStringList cover_files = dir.entryList(QStringList() << u"*.jpg"_s << u"*.png"_s << u"*.gif"_s << u"*.jpeg"_s, QDir::Files|QDir::Readable, QDir::Name);
|
QDir dir(file.path());
|
||||||
QString best_cover_file;
|
QStringList files = dir.entryList(QStringList() << u"*.jpg"_s << u"*.png"_s << u"*.gif"_s << u"*.jpeg"_s, QDir::Files|QDir::Readable, QDir::Name);
|
||||||
for (const QString &cover_file : cover_files) {
|
if (files.count() > 0) {
|
||||||
if (cover_file.contains("back"_L1, Qt::CaseInsensitive)) {
|
d->art_automatic_ = QUrl::fromLocalFile(file.path() + QDir::separator() + files.first());
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (cover_file.contains("front"_L1, Qt::CaseInsensitive) || cover_file.startsWith("cover"_L1, Qt::CaseInsensitive)) {
|
|
||||||
best_cover_file = cover_file;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (best_cover_file.isEmpty()) {
|
|
||||||
best_cover_file = cover_file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!best_cover_file.isEmpty()) {
|
|
||||||
d->art_automatic_ = QUrl::fromLocalFile(fileinfo.path() + QDir::separator() + best_cover_file);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -407,9 +407,8 @@ class Song {
|
|||||||
bool is_metadata_good() const;
|
bool is_metadata_good() const;
|
||||||
bool is_local_collection_song() const;
|
bool is_local_collection_song() const;
|
||||||
bool is_linked_collection_song() const;
|
bool is_linked_collection_song() const;
|
||||||
bool is_radio() const;
|
|
||||||
bool is_stream_service() const;
|
|
||||||
bool is_stream() const;
|
bool is_stream() const;
|
||||||
|
bool is_radio() const;
|
||||||
bool is_cdda() const;
|
bool is_cdda() const;
|
||||||
bool is_compilation() const;
|
bool is_compilation() const;
|
||||||
bool stream_url_can_expire() const;
|
bool stream_url_can_expire() const;
|
||||||
|
|||||||
86
src/core/tinysvcmdns.cpp
Normal file
86
src/core/tinysvcmdns.cpp
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
extern "C" {
|
||||||
|
#include "mdnsd.h"
|
||||||
|
}
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QList>
|
||||||
|
#include <QString>
|
||||||
|
#include <QHostInfo>
|
||||||
|
#include <QNetworkInterface>
|
||||||
|
#include <QtEndian>
|
||||||
|
|
||||||
|
#include "tinysvcmdns.h"
|
||||||
|
#include "core/logging.h"
|
||||||
|
|
||||||
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
|
||||||
|
TinySVCMDNS::TinySVCMDNS(QObject *parent) : Zeroconf(parent) {
|
||||||
|
|
||||||
|
// Get all network interfaces
|
||||||
|
const QList<QNetworkInterface> network_interfaces = QNetworkInterface::allInterfaces();
|
||||||
|
for (const QNetworkInterface &network_interface : network_interfaces) {
|
||||||
|
// Only use up and non loopback interfaces
|
||||||
|
if (network_interface.flags().testFlag(network_interface.IsUp) && !network_interface.flags().testFlag(network_interface.IsLoopBack)) {
|
||||||
|
|
||||||
|
qLog(Debug) << "Interface" << network_interface.humanReadableName();
|
||||||
|
|
||||||
|
uint32_t ipv4 = 0;
|
||||||
|
QString ipv6;
|
||||||
|
|
||||||
|
// Now check all network addresses for this device
|
||||||
|
QList<QNetworkAddressEntry> network_address_entries = network_interface.addressEntries();
|
||||||
|
|
||||||
|
for (QNetworkAddressEntry network_address_entry : network_address_entries) {
|
||||||
|
QHostAddress host_address = network_address_entry.ip();
|
||||||
|
if (host_address.protocol() == QAbstractSocket::IPv4Protocol) {
|
||||||
|
ipv4 = qToBigEndian(host_address.toIPv4Address());
|
||||||
|
qLog(Debug) << " ipv4:" << host_address.toString();
|
||||||
|
}
|
||||||
|
else if (host_address.protocol() == QAbstractSocket::IPv6Protocol) {
|
||||||
|
ipv6 = host_address.toString();
|
||||||
|
qLog(Debug) << " ipv6:" << host_address.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now start the service
|
||||||
|
CreateMdnsd(ipv4, ipv6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
TinySVCMDNS::~TinySVCMDNS() {
|
||||||
|
|
||||||
|
for (mdnsd *mdnsd : std::as_const(mdnsd_)) {
|
||||||
|
mdnsd_stop(mdnsd);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void TinySVCMDNS::CreateMdnsd(const uint32_t ipv4, const QString &ipv6) {
|
||||||
|
|
||||||
|
const QString host = QHostInfo::localHostName();
|
||||||
|
|
||||||
|
// Start the service
|
||||||
|
mdnsd *mdnsd = mdnsd_start();
|
||||||
|
|
||||||
|
// Set our hostname
|
||||||
|
const QString fullhostname = host + ".local"_L1;
|
||||||
|
mdnsd_set_hostname(mdnsd, fullhostname.toUtf8().constData(), ipv4);
|
||||||
|
|
||||||
|
// Add to the list
|
||||||
|
mdnsd_.append(mdnsd);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void TinySVCMDNS::PublishInternal(const QString &domain, const QString &type, const QByteArray &name, const quint16 port) {
|
||||||
|
|
||||||
|
// Some pointless text, so tinymDNS publishes the service correctly.
|
||||||
|
const char *txt[] = { "cat=nyan", nullptr };
|
||||||
|
|
||||||
|
for (mdnsd *mdnsd : mdnsd_) {
|
||||||
|
const QString fulltype = type + ".local"_L1;
|
||||||
|
mdnsd_register_svc(mdnsd, name.constData(), fulltype.toUtf8().constData(), port, nullptr, txt);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
26
src/core/tinysvcmdns.h
Normal file
26
src/core/tinysvcmdns.h
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#ifndef TINYSVCMDNS_H
|
||||||
|
#define TINYSVCMDNS_H
|
||||||
|
|
||||||
|
#include <QList>
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "zeroconf.h"
|
||||||
|
|
||||||
|
struct mdnsd;
|
||||||
|
|
||||||
|
class TinySVCMDNS : public Zeroconf {
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit TinySVCMDNS(QObject *parent = nullptr);
|
||||||
|
virtual ~TinySVCMDNS();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void PublishInternal(const QString &domain, const QString &type, const QByteArray &name, const quint16 port) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void CreateMdnsd(const uint32_t ipv4, const QString &ipv6);
|
||||||
|
QList<mdnsd*> mdnsd_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // TINYSVCMDNS_H
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
/*
|
|
||||||
* Strawberry Music Player
|
|
||||||
* Copyright 2026, Jonas Kvinge <jonas@jkvinge.net>
|
|
||||||
*
|
|
||||||
* Strawberry is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Strawberry is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <cstring>
|
|
||||||
#include <sys/socket.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
#include <csignal>
|
|
||||||
#include <cerrno>
|
|
||||||
#include <fcntl.h>
|
|
||||||
|
|
||||||
#include <QSocketNotifier>
|
|
||||||
|
|
||||||
#include "core/logging.h"
|
|
||||||
#include "unixsignalwatcher.h"
|
|
||||||
|
|
||||||
UnixSignalWatcher *UnixSignalWatcher::sInstance = nullptr;
|
|
||||||
|
|
||||||
UnixSignalWatcher::UnixSignalWatcher(QObject *parent)
|
|
||||||
: QObject(parent),
|
|
||||||
signal_fd_{-1, -1},
|
|
||||||
socket_notifier_(nullptr) {
|
|
||||||
|
|
||||||
Q_ASSERT(!sInstance);
|
|
||||||
|
|
||||||
// Create a socket pair for the self-pipe trick
|
|
||||||
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, signal_fd_) != 0) {
|
|
||||||
qLog(Error) << "Failed to create socket pair for signal handling:" << ::strerror(errno);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Q_ASSERT(signal_fd_[0] != -1);
|
|
||||||
|
|
||||||
// Set the read end to non-blocking mode
|
|
||||||
// Non-blocking mode is important to prevent HandleSignalNotification from blocking
|
|
||||||
int flags = ::fcntl(signal_fd_[0], F_GETFL, 0);
|
|
||||||
if (flags == -1) {
|
|
||||||
qLog(Error) << "Failed to get socket flags:" << ::strerror(errno);
|
|
||||||
}
|
|
||||||
else if (::fcntl(signal_fd_[0], F_SETFL, flags | O_NONBLOCK) == -1) {
|
|
||||||
qLog(Error) << "Failed to set socket to non-blocking:" << ::strerror(errno);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the write end to non-blocking mode as well (used in signal handler)
|
|
||||||
// Non-blocking mode prevents the signal handler from blocking if buffer is full
|
|
||||||
flags = ::fcntl(signal_fd_[1], F_GETFL, 0);
|
|
||||||
if (flags == -1) {
|
|
||||||
qLog(Error) << "Failed to get socket flags for write end:" << ::strerror(errno);
|
|
||||||
}
|
|
||||||
else if (::fcntl(signal_fd_[1], F_SETFL, flags | O_NONBLOCK) == -1) {
|
|
||||||
qLog(Error) << "Failed to set write end of socket to non-blocking:" << ::strerror(errno);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up QSocketNotifier to monitor the read end of the socket
|
|
||||||
socket_notifier_ = new QSocketNotifier(signal_fd_[0], QSocketNotifier::Read, this);
|
|
||||||
QObject::connect(socket_notifier_, &QSocketNotifier::activated, this, &UnixSignalWatcher::HandleSignalNotification);
|
|
||||||
|
|
||||||
sInstance = this;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
UnixSignalWatcher::~UnixSignalWatcher() {
|
|
||||||
|
|
||||||
if (socket_notifier_) {
|
|
||||||
socket_notifier_->setEnabled(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore original signal handlers
|
|
||||||
for (int i = 0; i < watched_signals_.size(); ++i) {
|
|
||||||
if (::sigaction(watched_signals_[i], &original_signal_actions_[i], nullptr) != 0) {
|
|
||||||
qLog(Error) << "Failed to restore signal handler for signal" << watched_signals_[i] << ":" << ::strerror(errno);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signal_fd_[0] != -1) {
|
|
||||||
::close(signal_fd_[0]);
|
|
||||||
signal_fd_[0] = -1;
|
|
||||||
}
|
|
||||||
if (signal_fd_[1] != -1) {
|
|
||||||
::close(signal_fd_[1]);
|
|
||||||
signal_fd_[1] = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
sInstance = nullptr;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void UnixSignalWatcher::WatchForSignal(const int signal) {
|
|
||||||
|
|
||||||
// Check if socket pair was created successfully
|
|
||||||
if (signal_fd_[0] == -1 || signal_fd_[1] == -1) {
|
|
||||||
qLog(Error) << "Cannot watch for signal: socket pair not initialized";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (watched_signals_.contains(signal)) {
|
|
||||||
qLog(Error) << "Already watching for signal" << signal;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct sigaction signal_action{};
|
|
||||||
::memset(&signal_action, 0, sizeof(signal_action));
|
|
||||||
sigemptyset(&signal_action.sa_mask);
|
|
||||||
signal_action.sa_handler = UnixSignalWatcher::SignalHandler;
|
|
||||||
signal_action.sa_flags = SA_RESTART;
|
|
||||||
|
|
||||||
struct sigaction old_signal_action{};
|
|
||||||
::memset(&old_signal_action, 0, sizeof(old_signal_action));
|
|
||||||
if (::sigaction(signal, &signal_action, &old_signal_action) != 0) {
|
|
||||||
qLog(Error) << "sigaction error:" << ::strerror(errno);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
watched_signals_ << signal;
|
|
||||||
original_signal_actions_ << old_signal_action;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void UnixSignalWatcher::SignalHandler(const int signal) {
|
|
||||||
|
|
||||||
if (!sInstance || sInstance->signal_fd_[1] == -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the signal number to the socket pair (async-signal-safe)
|
|
||||||
// This is the only operation we perform in the signal handler
|
|
||||||
// Ignore errors as there's nothing we can safely do about them in a signal handler
|
|
||||||
#ifdef __GNUC__
|
|
||||||
#pragma GCC diagnostic push
|
|
||||||
#pragma GCC diagnostic ignored "-Wunused-result"
|
|
||||||
#endif
|
|
||||||
(void)::write(sInstance->signal_fd_[1], &signal, sizeof(signal));
|
|
||||||
#ifdef __GNUC__
|
|
||||||
#pragma GCC diagnostic pop
|
|
||||||
#endif
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void UnixSignalWatcher::HandleSignalNotification() {
|
|
||||||
|
|
||||||
// Read all pending signals from the socket
|
|
||||||
// Multiple signals could arrive before the notifier triggers
|
|
||||||
while (true) {
|
|
||||||
int signal = 0;
|
|
||||||
const ssize_t bytes_read = ::read(signal_fd_[0], &signal, sizeof(signal));
|
|
||||||
if (bytes_read == sizeof(signal)) {
|
|
||||||
qLog(Debug) << "Caught signal:" << signal;
|
|
||||||
Q_EMIT UnixSignal(signal);
|
|
||||||
}
|
|
||||||
else if (bytes_read == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
|
|
||||||
// No more data available (expected with non-blocking socket)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Error occurred or partial read
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
/*
|
|
||||||
* Strawberry Music Player
|
|
||||||
* Copyright 2026, Jonas Kvinge <jonas@jkvinge.net>
|
|
||||||
*
|
|
||||||
* Strawberry is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Strawberry is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef UNIXSIGNALWATCHER_H
|
|
||||||
#define UNIXSIGNALWATCHER_H
|
|
||||||
|
|
||||||
#include <csignal>
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QList>
|
|
||||||
|
|
||||||
class QSocketNotifier;
|
|
||||||
|
|
||||||
class UnixSignalWatcher : public QObject {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit UnixSignalWatcher(QObject *parent = nullptr);
|
|
||||||
~UnixSignalWatcher() override;
|
|
||||||
|
|
||||||
void WatchForSignal(const int signal);
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
|
||||||
void UnixSignal(const int signal);
|
|
||||||
|
|
||||||
private:
|
|
||||||
static void SignalHandler(const int signal);
|
|
||||||
void HandleSignalNotification();
|
|
||||||
|
|
||||||
static UnixSignalWatcher *sInstance;
|
|
||||||
int signal_fd_[2];
|
|
||||||
QSocketNotifier *socket_notifier_;
|
|
||||||
QList<int> watched_signals_;
|
|
||||||
QList<struct sigaction> original_signal_actions_;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // UNIXSIGNALWATCHER_H
|
|
||||||
69
src/core/zeroconf.cpp
Normal file
69
src/core/zeroconf.cpp
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#ifdef HAVE_DBUS
|
||||||
|
# include "avahi/avahi.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
# include "bonjour.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
# include "tinysvcmdns.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "zeroconf.h"
|
||||||
|
|
||||||
|
Zeroconf *Zeroconf::sInstance = nullptr;
|
||||||
|
|
||||||
|
Zeroconf::Zeroconf(QObject *parent) : QObject(parent) {}
|
||||||
|
|
||||||
|
Zeroconf::~Zeroconf() = default;
|
||||||
|
|
||||||
|
Zeroconf *Zeroconf::GetZeroconf() {
|
||||||
|
|
||||||
|
if (!sInstance) {
|
||||||
|
#ifdef HAVE_DBUS
|
||||||
|
sInstance = new Avahi;
|
||||||
|
#endif // HAVE_DBUS
|
||||||
|
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
sInstance = new Bonjour;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
sInstance = new TinySVCMDNS;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
return sInstance;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray Zeroconf::TruncateName(const QString &name) {
|
||||||
|
|
||||||
|
QByteArray truncated_utf8;
|
||||||
|
for (const QChar c : name) {
|
||||||
|
if (truncated_utf8.size() + 1 >= 63) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
truncated_utf8 += c.toLatin1();
|
||||||
|
}
|
||||||
|
|
||||||
|
// NULL-terminate the string.
|
||||||
|
truncated_utf8.append('\0');
|
||||||
|
|
||||||
|
return truncated_utf8;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void Zeroconf::Publish(const QString &domain, const QString &type, const QString &name, quint16 port) {
|
||||||
|
|
||||||
|
const QByteArray truncated_name = TruncateName(name);
|
||||||
|
PublishInternal(domain, type, truncated_name, port);
|
||||||
|
|
||||||
|
}
|
||||||
28
src/core/zeroconf.h
Normal file
28
src/core/zeroconf.h
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#ifndef ZEROCONF_H
|
||||||
|
#define ZEROCONF_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
class Zeroconf : public QObject {
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Zeroconf(QObject *parent);
|
||||||
|
virtual ~Zeroconf();
|
||||||
|
|
||||||
|
void Publish(const QString &domain, const QString &type, const QString &name, quint16 port);
|
||||||
|
|
||||||
|
static Zeroconf *GetZeroconf();
|
||||||
|
|
||||||
|
// Truncate a QString to 63 bytes of UTF-8.
|
||||||
|
static QByteArray TruncateName(const QString &name);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void PublishInternal(const QString &domain, const QString &type, const QByteArray &name, quint16 port) = 0;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static Zeroconf *sInstance;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // ZEROCONF_H
|
||||||
@@ -75,7 +75,6 @@ FilesystemDevice::FilesystemDevice(const QUrl &url,
|
|||||||
QObject::connect(collection_watcher_, &CollectionWatcher::SongsReadded, &*collection_backend_, &CollectionBackend::MarkSongsUnavailable);
|
QObject::connect(collection_watcher_, &CollectionWatcher::SongsReadded, &*collection_backend_, &CollectionBackend::MarkSongsUnavailable);
|
||||||
QObject::connect(collection_watcher_, &CollectionWatcher::SubdirsDiscovered, &*collection_backend_, &CollectionBackend::AddOrUpdateSubdirs);
|
QObject::connect(collection_watcher_, &CollectionWatcher::SubdirsDiscovered, &*collection_backend_, &CollectionBackend::AddOrUpdateSubdirs);
|
||||||
QObject::connect(collection_watcher_, &CollectionWatcher::SubdirsMTimeUpdated, &*collection_backend_, &CollectionBackend::AddOrUpdateSubdirs);
|
QObject::connect(collection_watcher_, &CollectionWatcher::SubdirsMTimeUpdated, &*collection_backend_, &CollectionBackend::AddOrUpdateSubdirs);
|
||||||
QObject::connect(collection_watcher_, &CollectionWatcher::SubdirsDeleted, &*collection_backend_, &CollectionBackend::DeleteSubdirs);
|
|
||||||
QObject::connect(collection_watcher_, &CollectionWatcher::CompilationsNeedUpdating, &*collection_backend_, &CollectionBackend::CompilationsNeedUpdating);
|
QObject::connect(collection_watcher_, &CollectionWatcher::CompilationsNeedUpdating, &*collection_backend_, &CollectionBackend::CompilationsNeedUpdating);
|
||||||
QObject::connect(collection_watcher_, &CollectionWatcher::UpdateLastSeen, &*collection_backend_, &CollectionBackend::UpdateLastSeen);
|
QObject::connect(collection_watcher_, &CollectionWatcher::UpdateLastSeen, &*collection_backend_, &CollectionBackend::UpdateLastSeen);
|
||||||
QObject::connect(collection_watcher_, &CollectionWatcher::ScanStarted, this, &FilesystemDevice::TaskStarted);
|
QObject::connect(collection_watcher_, &CollectionWatcher::ScanStarted, this, &FilesystemDevice::TaskStarted);
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QKeySequence>
|
#include <QKeySequence>
|
||||||
|
#include <QTextBrowser>
|
||||||
|
|
||||||
#include "about.h"
|
#include "about.h"
|
||||||
#include "ui_about.h"
|
#include "ui_about.h"
|
||||||
@@ -43,6 +44,52 @@ About::About(QWidget *parent) : QDialog(parent), ui_{} {
|
|||||||
setWindowFlags(windowFlags()|Qt::WindowStaysOnTopHint);
|
setWindowFlags(windowFlags()|Qt::WindowStaysOnTopHint);
|
||||||
setWindowTitle(tr("About Strawberry"));
|
setWindowTitle(tr("About Strawberry"));
|
||||||
|
|
||||||
|
strawberry_authors_ \
|
||||||
|
<< Person(u"Jonas Kvinge"_s);
|
||||||
|
|
||||||
|
strawberry_contributors_ \
|
||||||
|
<< Person(u"Gavin D. Howard"_s)
|
||||||
|
<< Person(u"Martin Delille"_s)
|
||||||
|
<< Person(u"Roman Lebedev"_s)
|
||||||
|
<< Person(u"Daniel Ostertag"_s)
|
||||||
|
<< Person(u"Gustavo L Conte"_s);
|
||||||
|
|
||||||
|
clementine_authors_
|
||||||
|
<< Person(u"David Sansome"_s)
|
||||||
|
<< Person(u"John Maguire"_s)
|
||||||
|
<< Person(u"Paweł Bara"_s)
|
||||||
|
<< Person(u"Arnaud Bienner"_s);
|
||||||
|
|
||||||
|
clementine_contributors_ \
|
||||||
|
<< Person(u"Jakub Stachowski"_s)
|
||||||
|
<< Person(u"Paul Cifarelli"_s)
|
||||||
|
<< Person(u"Felipe Rivera"_s)
|
||||||
|
<< Person(u"Alexander Peitz"_s)
|
||||||
|
<< Person(u"Andreas Muttscheller"_s)
|
||||||
|
<< Person(u"Mark Furneaux"_s)
|
||||||
|
<< Person(u"Florian Bigard"_s)
|
||||||
|
<< Person(u"Alex Bikadorov"_s)
|
||||||
|
<< Person(u"Mattias Andersson"_s)
|
||||||
|
<< Person(u"Alan Briolat"_s)
|
||||||
|
<< Person(u"Arun Narayanankutty"_s)
|
||||||
|
<< Person(u"Bartłomiej Burdukiewicz"_s)
|
||||||
|
<< Person(u"Andre Siviero"_s)
|
||||||
|
<< Person(u"Santiago Gil"_s)
|
||||||
|
<< Person(u"Tyler Rhodes"_s)
|
||||||
|
<< Person(u"Vikram Ambrose"_s)
|
||||||
|
<< Person(u"David Guillen"_s)
|
||||||
|
<< Person(u"Krzysztof Sobiecki"_s)
|
||||||
|
<< Person(u"Valeriy Malov"_s)
|
||||||
|
<< Person(u"Nick Lanham"_s);
|
||||||
|
|
||||||
|
strawberry_thanks_ \
|
||||||
|
<< Person(u"Mark Kretschmann"_s)
|
||||||
|
<< Person(u"Max Howell"_s)
|
||||||
|
<< Person(u"Artur Rona"_s)
|
||||||
|
<< Person(u"Robert-André Mauchin"_s)
|
||||||
|
<< Person(u"Thomas Pierson"_s)
|
||||||
|
<< Person(u"Fabio Loli"_s);
|
||||||
|
|
||||||
QFont title_font;
|
QFont title_font;
|
||||||
title_font.setBold(true);
|
title_font.setBold(true);
|
||||||
title_font.setPointSize(title_font.pointSize() + 4);
|
title_font.setPointSize(title_font.pointSize() + 4);
|
||||||
@@ -50,6 +97,8 @@ About::About(QWidget *parent) : QDialog(parent), ui_{} {
|
|||||||
ui_.label_title->setFont(title_font);
|
ui_.label_title->setFont(title_font);
|
||||||
ui_.label_title->setText(windowTitle());
|
ui_.label_title->setText(windowTitle());
|
||||||
ui_.label_text->setText(MainHtml());
|
ui_.label_text->setText(MainHtml());
|
||||||
|
ui_.text_contributors->document()->setDefaultStyleSheet(QStringLiteral("a {color: %1; }").arg(palette().text().color().name()));
|
||||||
|
ui_.text_contributors->setText(ContributorsHtml());
|
||||||
|
|
||||||
ui_.buttonBox->button(QDialogButtonBox::Close)->setShortcut(QKeySequence::Close);
|
ui_.buttonBox->button(QDialogButtonBox::Close)->setShortcut(QKeySequence::Close);
|
||||||
|
|
||||||
@@ -64,17 +113,94 @@ QString About::MainHtml() const {
|
|||||||
ret += "</p>"_L1;
|
ret += "</p>"_L1;
|
||||||
|
|
||||||
ret += "<p>"_L1;
|
ret += "<p>"_L1;
|
||||||
ret += tr("Fork of %1.").arg(QStringLiteral("<a style=\"color:%1;\" href=\"https://github.com/clementine-player/Clementine\">Clementine</a>").arg(palette().text().color().name()));
|
ret += tr("Strawberry is a music player and music collection organizer.");
|
||||||
|
ret += "<br />"_L1;
|
||||||
|
ret += tr("It is a fork of Clementine released in 2018 aimed at music collectors and audiophiles.");
|
||||||
ret += "</p>"_L1;
|
ret += "</p>"_L1;
|
||||||
|
|
||||||
ret += "<p>"_L1;
|
ret += "<p>"_L1;
|
||||||
ret += tr("Source code: %1").arg(QStringLiteral("<a style=\"color:%1;\" href=\"https://gitea.dryark.com/dryark/strawberry\">gitea.dryark.com/dryark/strawberry</a>").arg(palette().text().color().name()));
|
ret += tr("Strawberry is free software released under GPL. The source code is available on %1").arg(QStringLiteral("<a style=\"color:%1;\" href=\"https://github.com/strawberrymusicplayer/strawberry\">GitHub</a>.").arg(palette().text().color().name()));
|
||||||
|
ret += "<br />"_L1;
|
||||||
|
ret += tr("You should have received a copy of the GNU General Public License along with this program. If not, see %1").arg(QStringLiteral("<a style=\"color:%1;\" href=\"http://www.gnu.org/licenses/\">http://www.gnu.org/licenses/</a>").arg(palette().text().color().name()));
|
||||||
ret += "</p>"_L1;
|
ret += "</p>"_L1;
|
||||||
|
|
||||||
ret += "<p>"_L1;
|
ret += "<p>"_L1;
|
||||||
ret += tr("License: %1").arg(QStringLiteral("<a style=\"color:%1;\" href=\"https://www.gnu.org/licenses/gpl-3.0.html\">GPLv3</a>").arg(palette().text().color().name()));
|
ret += tr("If you like Strawberry and can make use of it, consider sponsoring or donating.");
|
||||||
|
ret += "<br />"_L1;
|
||||||
|
ret += tr("You can sponsor the author on %1 or %2. You can also make a one-time payment through %3.").arg(
|
||||||
|
QStringLiteral("<a style=\"color:%1;\" href=\"https://www.patreon.com/jonaskvinge\">Patreon</a>").arg(palette().text().color().name()),
|
||||||
|
QStringLiteral("<a style=\"color:%1;\" href=\"https://github.com/sponsors/jonaski\">GitHub</a>").arg(palette().text().color().name()),
|
||||||
|
QStringLiteral("<a style=\"color:%1;\" href=\"https://paypal.me/jonaskvinge\">paypal.me/jonaskvinge</a>").arg(palette().text().color().name())
|
||||||
|
);
|
||||||
|
|
||||||
ret += "</p>"_L1;
|
ret += "</p>"_L1;
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString About::ContributorsHtml() const {
|
||||||
|
|
||||||
|
QString ret;
|
||||||
|
|
||||||
|
ret += "<p>"_L1;
|
||||||
|
ret += "<b>"_L1;
|
||||||
|
ret += tr("Author and maintainer");
|
||||||
|
ret += "</b>"_L1;
|
||||||
|
for (const Person &person : strawberry_authors_) {
|
||||||
|
ret += "<br />"_L1 + PersonToHtml(person);
|
||||||
|
}
|
||||||
|
ret += "</p>"_L1;
|
||||||
|
|
||||||
|
ret += "<p>"_L1;
|
||||||
|
ret += "<b>"_L1;
|
||||||
|
ret += tr("Contributors");
|
||||||
|
ret += "</b>"_L1;
|
||||||
|
for (const Person &person : strawberry_contributors_) {
|
||||||
|
ret += "<br />"_L1 + PersonToHtml(person);
|
||||||
|
}
|
||||||
|
ret += "</p>"_L1;
|
||||||
|
|
||||||
|
ret += "<p>"_L1;
|
||||||
|
ret += "<b>"_L1;
|
||||||
|
ret += tr("Clementine authors");
|
||||||
|
ret += "</b>"_L1;
|
||||||
|
for (const Person &person : clementine_authors_) {
|
||||||
|
ret += "<br />"_L1 + PersonToHtml(person);
|
||||||
|
}
|
||||||
|
ret += "</p>"_L1;
|
||||||
|
|
||||||
|
ret += "<p>"_L1;
|
||||||
|
ret += "<b>"_L1;
|
||||||
|
ret += tr("Clementine contributors");
|
||||||
|
ret += "</b>"_L1;
|
||||||
|
for (const Person &person : clementine_contributors_) {
|
||||||
|
ret += "<br />"_L1 + PersonToHtml(person);
|
||||||
|
}
|
||||||
|
ret += "</p>"_L1;
|
||||||
|
|
||||||
|
ret += "<p>"_L1;
|
||||||
|
ret += "<b>"_L1;
|
||||||
|
ret += tr("Thanks to");
|
||||||
|
ret += "</b>"_L1;
|
||||||
|
for (const Person &person : strawberry_thanks_) {
|
||||||
|
ret += "<br />"_L1 + PersonToHtml(person);
|
||||||
|
}
|
||||||
|
ret += "</p>"_L1;
|
||||||
|
|
||||||
|
ret += "<p>"_L1;
|
||||||
|
ret += tr("Thanks to all the other Amarok and Clementine contributors.");
|
||||||
|
ret += "</p>"_L1;
|
||||||
|
return ret;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
QString About::PersonToHtml(const Person &person) {
|
||||||
|
|
||||||
|
if (person.email.isEmpty()) {
|
||||||
|
return person.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return QStringLiteral("%1 <<a href=\"mailto:%2\">%3</a>>").arg(person.name, person.email, person.email);
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
|
#include <QList>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
#include "ui_about.h"
|
#include "ui_about.h"
|
||||||
@@ -39,10 +40,25 @@ class About : public QDialog {
|
|||||||
explicit About(QWidget *parent = nullptr);
|
explicit About(QWidget *parent = nullptr);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
struct Person {
|
||||||
|
explicit Person(const QString &n, const QString &e = QString()) : name(n), email(e) {}
|
||||||
|
bool operator<(const Person &other) const { return name < other.name; }
|
||||||
|
QString name;
|
||||||
|
QString email;
|
||||||
|
};
|
||||||
|
|
||||||
QString MainHtml() const;
|
QString MainHtml() const;
|
||||||
|
QString ContributorsHtml() const;
|
||||||
|
static QString PersonToHtml(const Person &person);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Ui::About ui_;
|
Ui::About ui_;
|
||||||
|
|
||||||
|
QList<Person> strawberry_authors_;
|
||||||
|
QList<Person> strawberry_contributors_;
|
||||||
|
QList<Person> strawberry_thanks_;
|
||||||
|
QList<Person> clementine_authors_;
|
||||||
|
QList<Person> clementine_contributors_;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // ABOUT_H
|
#endif // ABOUT_H
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>500</width>
|
<width>500</width>
|
||||||
<height>320</height>
|
<height>500</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="focusPolicy">
|
<property name="focusPolicy">
|
||||||
@@ -149,6 +149,19 @@
|
|||||||
</property>
|
</property>
|
||||||
</spacer>
|
</spacer>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QTextBrowser" name="text_contributors">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>0</width>
|
||||||
|
<height>200</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="openExternalLinks">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<spacer name="spacer_bottom">
|
<spacer name="spacer_bottom">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
|
|||||||
@@ -411,17 +411,6 @@ bool EditTagDialog::eventFilter(QObject *o, QEvent *e) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SongList EditTagDialog::songs() const {
|
|
||||||
|
|
||||||
SongList result;
|
|
||||||
for (const Data &d : data_) {
|
|
||||||
result << d.current_;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
bool EditTagDialog::SetLoading(const QString &message) {
|
bool EditTagDialog::SetLoading(const QString &message) {
|
||||||
|
|
||||||
const bool loading = !message.isEmpty();
|
const bool loading = !message.isEmpty();
|
||||||
@@ -1410,12 +1399,6 @@ void EditTagDialog::SaveData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (save_tags || save_playcount || save_rating || save_embedded_cover) {
|
if (save_tags || save_playcount || save_rating || save_embedded_cover) {
|
||||||
// For streaming tracks, skip tag writing since there's no local file.
|
|
||||||
// The metadata will be applied directly to the playlist item in MainWindow::EditTagDialogAccepted.
|
|
||||||
if (ref.current_.is_stream()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not to confuse the collection model.
|
// Not to confuse the collection model.
|
||||||
if (ref.current_.track() <= 0) { ref.current_.set_track(-1); }
|
if (ref.current_.track() <= 0) { ref.current_.set_track(-1); }
|
||||||
if (ref.current_.disc() <= 0) { ref.current_.set_disc(-1); }
|
if (ref.current_.disc() <= 0) { ref.current_.set_disc(-1); }
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ class EditTagDialog : public QDialog {
|
|||||||
void SetSongs(const SongList &songs, const PlaylistItemPtrList &items = PlaylistItemPtrList());
|
void SetSongs(const SongList &songs, const PlaylistItemPtrList &items = PlaylistItemPtrList());
|
||||||
|
|
||||||
PlaylistItemPtrList playlist_items() const { return playlist_items_; }
|
PlaylistItemPtrList playlist_items() const { return playlist_items_; }
|
||||||
SongList songs() const;
|
|
||||||
void accept() override;
|
void accept() override;
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
|
|||||||
@@ -29,17 +29,13 @@
|
|||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
#include <QStandardPaths>
|
#include <QSettings>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QScrollBar>
|
#include <QScrollBar>
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
#include <QToolButton>
|
#include <QToolButton>
|
||||||
#include <QFileDialog>
|
|
||||||
#include <QSpacerItem>
|
|
||||||
#include <QtEvents>
|
#include <QtEvents>
|
||||||
|
|
||||||
#include "constants/appearancesettings.h"
|
|
||||||
#include "constants/filefilterconstants.h"
|
|
||||||
#include "includes/shared_ptr.h"
|
#include "includes/shared_ptr.h"
|
||||||
#include "core/deletefiles.h"
|
#include "core/deletefiles.h"
|
||||||
#include "core/filesystemmusicstorage.h"
|
#include "core/filesystemmusicstorage.h"
|
||||||
@@ -49,11 +45,10 @@
|
|||||||
#include "dialogs/deleteconfirmationdialog.h"
|
#include "dialogs/deleteconfirmationdialog.h"
|
||||||
#include "fileview.h"
|
#include "fileview.h"
|
||||||
#include "fileviewlist.h"
|
#include "fileviewlist.h"
|
||||||
#include "fileviewtree.h"
|
|
||||||
#include "fileviewtreemodel.h"
|
|
||||||
#include "fileviewtreeitem.h"
|
|
||||||
#include "ui_fileview.h"
|
#include "ui_fileview.h"
|
||||||
#include "organize/organizeerrordialog.h"
|
#include "organize/organizeerrordialog.h"
|
||||||
|
#include "constants/appearancesettings.h"
|
||||||
|
#include "constants/filefilterconstants.h"
|
||||||
|
|
||||||
using std::make_unique;
|
using std::make_unique;
|
||||||
using namespace Qt::Literals::StringLiterals;
|
using namespace Qt::Literals::StringLiterals;
|
||||||
@@ -62,12 +57,9 @@ FileView::FileView(QWidget *parent)
|
|||||||
: QWidget(parent),
|
: QWidget(parent),
|
||||||
ui_(new Ui_FileView),
|
ui_(new Ui_FileView),
|
||||||
model_(nullptr),
|
model_(nullptr),
|
||||||
tree_model_(nullptr),
|
|
||||||
undo_stack_(new QUndoStack(this)),
|
undo_stack_(new QUndoStack(this)),
|
||||||
task_manager_(nullptr),
|
task_manager_(nullptr),
|
||||||
storage_(new FilesystemMusicStorage(Song::Source::LocalFile, u"/"_s)),
|
storage_(new FilesystemMusicStorage(Song::Source::LocalFile, u"/"_s)) {
|
||||||
tree_view_active_(false),
|
|
||||||
view_mode_spacer_(nullptr) {
|
|
||||||
|
|
||||||
ui_->setupUi(this);
|
ui_->setupUi(this);
|
||||||
|
|
||||||
@@ -76,14 +68,12 @@ FileView::FileView(QWidget *parent)
|
|||||||
ui_->forward->setIcon(IconLoader::Load(u"go-next"_s));
|
ui_->forward->setIcon(IconLoader::Load(u"go-next"_s));
|
||||||
ui_->home->setIcon(IconLoader::Load(u"go-home"_s));
|
ui_->home->setIcon(IconLoader::Load(u"go-home"_s));
|
||||||
ui_->up->setIcon(IconLoader::Load(u"go-up"_s));
|
ui_->up->setIcon(IconLoader::Load(u"go-up"_s));
|
||||||
ui_->toggle_view->setIcon(IconLoader::Load(u"view-choose"_s));
|
|
||||||
|
|
||||||
QObject::connect(ui_->back, &QToolButton::clicked, undo_stack_, &QUndoStack::undo);
|
QObject::connect(ui_->back, &QToolButton::clicked, undo_stack_, &QUndoStack::undo);
|
||||||
QObject::connect(ui_->forward, &QToolButton::clicked, undo_stack_, &QUndoStack::redo);
|
QObject::connect(ui_->forward, &QToolButton::clicked, undo_stack_, &QUndoStack::redo);
|
||||||
QObject::connect(ui_->home, &QToolButton::clicked, this, &FileView::FileHome);
|
QObject::connect(ui_->home, &QToolButton::clicked, this, &FileView::FileHome);
|
||||||
QObject::connect(ui_->up, &QToolButton::clicked, this, &FileView::FileUp);
|
QObject::connect(ui_->up, &QToolButton::clicked, this, &FileView::FileUp);
|
||||||
QObject::connect(ui_->path, &QLineEdit::textChanged, this, &FileView::ChangeFilePath);
|
QObject::connect(ui_->path, &QLineEdit::textChanged, this, &FileView::ChangeFilePath);
|
||||||
QObject::connect(ui_->toggle_view, &QToolButton::clicked, this, &FileView::ToggleViewMode);
|
|
||||||
|
|
||||||
QObject::connect(undo_stack_, &QUndoStack::canUndoChanged, ui_->back, &FileView::setEnabled);
|
QObject::connect(undo_stack_, &QUndoStack::canUndoChanged, ui_->back, &FileView::setEnabled);
|
||||||
QObject::connect(undo_stack_, &QUndoStack::canRedoChanged, ui_->forward, &FileView::setEnabled);
|
QObject::connect(undo_stack_, &QUndoStack::canRedoChanged, ui_->forward, &FileView::setEnabled);
|
||||||
@@ -97,22 +87,6 @@ FileView::FileView(QWidget *parent)
|
|||||||
QObject::connect(ui_->list, &FileViewList::Delete, this, &FileView::Delete);
|
QObject::connect(ui_->list, &FileViewList::Delete, this, &FileView::Delete);
|
||||||
QObject::connect(ui_->list, &FileViewList::EditTags, this, &FileView::EditTags);
|
QObject::connect(ui_->list, &FileViewList::EditTags, this, &FileView::EditTags);
|
||||||
|
|
||||||
// Connect tree view signals
|
|
||||||
QObject::connect(ui_->tree, &FileViewTree::AddToPlaylist, this, &FileView::AddToPlaylist);
|
|
||||||
QObject::connect(ui_->tree, &FileViewTree::CopyToCollection, this, &FileView::CopyToCollection);
|
|
||||||
QObject::connect(ui_->tree, &FileViewTree::MoveToCollection, this, &FileView::MoveToCollection);
|
|
||||||
QObject::connect(ui_->tree, &FileViewTree::CopyToDevice, this, &FileView::CopyToDevice);
|
|
||||||
QObject::connect(ui_->tree, &FileViewTree::Delete, this, &FileView::Delete);
|
|
||||||
QObject::connect(ui_->tree, &FileViewTree::EditTags, this, &FileView::EditTags);
|
|
||||||
QObject::connect(ui_->tree, &FileViewTree::activated, this, &FileView::ItemActivated);
|
|
||||||
QObject::connect(ui_->tree, &FileViewTree::doubleClicked, this, &FileView::ItemDoubleClick);
|
|
||||||
|
|
||||||
// Setup tree root management buttons
|
|
||||||
ui_->add_tree_root->setIcon(IconLoader::Load(u"folder-new"_s));
|
|
||||||
ui_->remove_tree_root->setIcon(IconLoader::Load(u"list-remove"_s));
|
|
||||||
QObject::connect(ui_->add_tree_root, &QToolButton::clicked, this, &FileView::AddRootButtonClicked);
|
|
||||||
QObject::connect(ui_->remove_tree_root, &QToolButton::clicked, this, &FileView::RemoveRootButtonClicked);
|
|
||||||
|
|
||||||
QString filter = QLatin1String(kFileFilter);
|
QString filter = QLatin1String(kFileFilter);
|
||||||
filter_list_ << filter.split(u' ');
|
filter_list_ << filter.split(u' ');
|
||||||
|
|
||||||
@@ -135,19 +109,6 @@ void FileView::ReloadSettings() {
|
|||||||
ui_->forward->setIconSize(QSize(iconsize, iconsize));
|
ui_->forward->setIconSize(QSize(iconsize, iconsize));
|
||||||
ui_->home->setIconSize(QSize(iconsize, iconsize));
|
ui_->home->setIconSize(QSize(iconsize, iconsize));
|
||||||
ui_->up->setIconSize(QSize(iconsize, iconsize));
|
ui_->up->setIconSize(QSize(iconsize, iconsize));
|
||||||
ui_->toggle_view->setIconSize(QSize(iconsize, iconsize));
|
|
||||||
ui_->add_tree_root->setIconSize(QSize(iconsize, iconsize));
|
|
||||||
ui_->remove_tree_root->setIconSize(QSize(iconsize, iconsize));
|
|
||||||
|
|
||||||
// Load tree root paths setting
|
|
||||||
Settings file_settings;
|
|
||||||
file_settings.beginGroup(u"FileView"_s);
|
|
||||||
tree_root_paths_ = file_settings.value(u"tree_root_paths"_s, QStandardPaths::standardLocations(QStandardPaths::StandardLocation::MusicLocation)).toStringList();
|
|
||||||
tree_view_active_ = file_settings.value(u"tree_view_active"_s, false).toBool();
|
|
||||||
file_settings.endGroup();
|
|
||||||
|
|
||||||
// Set initial view mode
|
|
||||||
UpdateViewModeUI();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,46 +180,24 @@ void FileView::ChangeFilePathWithoutUndo(const QString &new_path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void FileView::ItemActivated(const QModelIndex &idx) {
|
void FileView::ItemActivated(const QModelIndex &idx) {
|
||||||
// Only handle activation for list view (not tree view)
|
if (model_->isDir(idx))
|
||||||
if (!tree_view_active_ && model_->isDir(idx)) {
|
|
||||||
ChangeFilePath(model_->filePath(idx));
|
ChangeFilePath(model_->filePath(idx));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void FileView::ItemDoubleClick(const QModelIndex &idx) {
|
void FileView::ItemDoubleClick(const QModelIndex &idx) {
|
||||||
|
|
||||||
QString file_path;
|
if (model_->isDir(idx)) {
|
||||||
bool is_file = false;
|
return;
|
||||||
|
|
||||||
// Handle tree view with virtual roots
|
|
||||||
if (tree_view_active_ && tree_model_) {
|
|
||||||
QVariant type_var = tree_model_->data(idx, FileViewTreeModel::Role_Type);
|
|
||||||
if (type_var.isValid()) {
|
|
||||||
FileViewTreeItem::Type item_type = type_var.value<FileViewTreeItem::Type>();
|
|
||||||
// Only handle files, ignore directories and virtual roots
|
|
||||||
if (item_type == FileViewTreeItem::Type::File) {
|
|
||||||
file_path = tree_model_->data(idx, FileViewTreeModel::Role_FilePath).toString();
|
|
||||||
is_file = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Handle list view with filesystem model
|
|
||||||
else if (!tree_view_active_ && model_) {
|
|
||||||
if (!model_->isDir(idx)) {
|
|
||||||
file_path = model_->filePath(idx);
|
|
||||||
is_file = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add file to playlist if it's a valid file
|
QString file_path = model_->filePath(idx);
|
||||||
if (is_file && !file_path.isEmpty()) {
|
|
||||||
MimeData *mimedata = new MimeData;
|
MimeData *mimedata = new MimeData;
|
||||||
mimedata->from_doubleclick_ = true;
|
mimedata->from_doubleclick_ = true;
|
||||||
mimedata->setUrls(QList<QUrl>() << QUrl::fromLocalFile(file_path));
|
mimedata->setUrls(QList<QUrl>() << QUrl::fromLocalFile(file_path));
|
||||||
mimedata->name_for_new_playlist_ = file_path;
|
mimedata->name_for_new_playlist_ = file_path;
|
||||||
|
|
||||||
Q_EMIT AddToPlaylist(mimedata);
|
Q_EMIT AddToPlaylist(mimedata);
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,156 +272,12 @@ void FileView::showEvent(QShowEvent *e) {
|
|||||||
model_->setNameFilterDisables(false);
|
model_->setNameFilterDisables(false);
|
||||||
|
|
||||||
ui_->list->setModel(model_);
|
ui_->list->setModel(model_);
|
||||||
|
|
||||||
// Create tree model
|
|
||||||
tree_model_ = new FileViewTreeModel(this);
|
|
||||||
tree_model_->SetNameFilters(filter_list_);
|
|
||||||
|
|
||||||
SetupTreeView();
|
|
||||||
|
|
||||||
ChangeFilePathWithoutUndo(QDir::homePath());
|
ChangeFilePathWithoutUndo(QDir::homePath());
|
||||||
|
|
||||||
if (!lazy_set_path_.isEmpty()) ChangeFilePathWithoutUndo(lazy_set_path_);
|
if (!lazy_set_path_.isEmpty()) ChangeFilePathWithoutUndo(lazy_set_path_);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileView::SetupTreeView() {
|
|
||||||
|
|
||||||
// Use the new tree model with virtual roots
|
|
||||||
ui_->tree->setModel(tree_model_);
|
|
||||||
|
|
||||||
// Set the root paths in the model
|
|
||||||
tree_model_->SetRootPaths(tree_root_paths_);
|
|
||||||
|
|
||||||
// No need to set root index - the model handles virtual roots
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileView::ToggleViewMode() {
|
|
||||||
|
|
||||||
tree_view_active_ = !tree_view_active_;
|
|
||||||
UpdateViewModeUI();
|
|
||||||
|
|
||||||
// Save the preference
|
|
||||||
Settings s;
|
|
||||||
s.beginGroup(u"FileView"_s);
|
|
||||||
s.setValue(u"tree_view_active"_s, tree_view_active_);
|
|
||||||
s.endGroup();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileView::UpdateViewModeUI() {
|
|
||||||
|
|
||||||
if (tree_view_active_) {
|
|
||||||
ui_->view_stack->setCurrentWidget(ui_->tree_page);
|
|
||||||
// Hide navigation controls in tree view mode
|
|
||||||
ui_->back->setVisible(false);
|
|
||||||
ui_->forward->setVisible(false);
|
|
||||||
ui_->up->setVisible(false);
|
|
||||||
ui_->home->setVisible(false);
|
|
||||||
ui_->path->setVisible(false);
|
|
||||||
// Show tree root management buttons
|
|
||||||
ui_->add_tree_root->setVisible(true);
|
|
||||||
ui_->remove_tree_root->setVisible(true);
|
|
||||||
// Insert spacer in tree view if not already present
|
|
||||||
if (!view_mode_spacer_) {
|
|
||||||
view_mode_spacer_ = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum);
|
|
||||||
ui_->horizontalLayout->insertSpacerItem(ui_->horizontalLayout->indexOf(ui_->toggle_view), view_mode_spacer_);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
ui_->view_stack->setCurrentWidget(ui_->list_page);
|
|
||||||
// Show navigation controls in list view mode
|
|
||||||
ui_->back->setVisible(true);
|
|
||||||
ui_->forward->setVisible(true);
|
|
||||||
ui_->up->setVisible(true);
|
|
||||||
ui_->home->setVisible(true);
|
|
||||||
ui_->path->setVisible(true);
|
|
||||||
// Hide tree root management buttons in list view
|
|
||||||
ui_->add_tree_root->setVisible(false);
|
|
||||||
ui_->remove_tree_root->setVisible(false);
|
|
||||||
// Remove spacer in list view
|
|
||||||
if (view_mode_spacer_) {
|
|
||||||
ui_->horizontalLayout->removeItem(view_mode_spacer_);
|
|
||||||
delete view_mode_spacer_;
|
|
||||||
view_mode_spacer_ = nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileView::AddTreeRootPath(const QString &path) {
|
|
||||||
|
|
||||||
if (!tree_root_paths_.contains(path)) {
|
|
||||||
tree_root_paths_.append(path);
|
|
||||||
SaveTreeRootPaths();
|
|
||||||
|
|
||||||
// Refresh the tree view to show the new root
|
|
||||||
if (tree_model_) {
|
|
||||||
SetupTreeView();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileView::RemoveTreeRootPath(const QString &path) {
|
|
||||||
|
|
||||||
tree_root_paths_.removeAll(path);
|
|
||||||
SaveTreeRootPaths();
|
|
||||||
|
|
||||||
// Refresh the tree view
|
|
||||||
if (tree_model_) {
|
|
||||||
SetupTreeView();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileView::SaveTreeRootPaths() {
|
|
||||||
|
|
||||||
Settings s;
|
|
||||||
s.beginGroup(u"FileView"_s);
|
|
||||||
s.setValue(u"tree_root_paths"_s, tree_root_paths_);
|
|
||||||
s.endGroup();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileView::AddRootButtonClicked() {
|
|
||||||
|
|
||||||
const QString dir = QFileDialog::getExistingDirectory(this, tr("Select folder to add as tree root"), tree_root_paths_.isEmpty() ? QDir::homePath() : tree_root_paths_.first(), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
|
|
||||||
if (!dir.isEmpty()) {
|
|
||||||
AddTreeRootPath(dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileView::RemoveRootButtonClicked() {
|
|
||||||
|
|
||||||
// Get currently selected item in tree
|
|
||||||
QModelIndex current = ui_->tree->currentIndex();
|
|
||||||
if (!current.isValid()) return;
|
|
||||||
|
|
||||||
QString path;
|
|
||||||
|
|
||||||
// Get the file path from the appropriate model
|
|
||||||
if (tree_model_) {
|
|
||||||
path = tree_model_->data(current, FileViewTreeModel::Role_FilePath).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (path.isEmpty()) return;
|
|
||||||
|
|
||||||
const QString clean_path = QDir::cleanPath(path);
|
|
||||||
|
|
||||||
// Check if this path or any parent is a configured root
|
|
||||||
for (const QString &root : std::as_const(tree_root_paths_)) {
|
|
||||||
const QString clean_root = QDir::cleanPath(root);
|
|
||||||
if (clean_path == clean_root || clean_path.startsWith(clean_root + QDir::separator())) {
|
|
||||||
RemoveTreeRootPath(root);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileView::keyPressEvent(QKeyEvent *e) {
|
void FileView::keyPressEvent(QKeyEvent *e) {
|
||||||
|
|
||||||
switch (e->key()) {
|
switch (e->key()) {
|
||||||
|
|||||||
@@ -40,12 +40,10 @@ class QFileIconProvider;
|
|||||||
class QUndoStack;
|
class QUndoStack;
|
||||||
class QKeyEvent;
|
class QKeyEvent;
|
||||||
class QShowEvent;
|
class QShowEvent;
|
||||||
class QSpacerItem;
|
|
||||||
|
|
||||||
class MusicStorage;
|
class MusicStorage;
|
||||||
class TaskManager;
|
class TaskManager;
|
||||||
class Ui_FileView;
|
class Ui_FileView;
|
||||||
class FileViewTreeModel;
|
|
||||||
|
|
||||||
class FileView : public QWidget {
|
class FileView : public QWidget {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -78,22 +76,12 @@ class FileView : public QWidget {
|
|||||||
void ChangeFilePath(const QString &new_path);
|
void ChangeFilePath(const QString &new_path);
|
||||||
void ItemActivated(const QModelIndex &idx);
|
void ItemActivated(const QModelIndex &idx);
|
||||||
void ItemDoubleClick(const QModelIndex &idx);
|
void ItemDoubleClick(const QModelIndex &idx);
|
||||||
void ToggleViewMode();
|
|
||||||
|
|
||||||
void Delete(const QStringList &filenames);
|
void Delete(const QStringList &filenames);
|
||||||
void DeleteFinished(const SongList &songs_with_errors);
|
void DeleteFinished(const SongList &songs_with_errors);
|
||||||
|
|
||||||
public Q_SLOTS:
|
|
||||||
void AddTreeRootPath(const QString &path);
|
|
||||||
void RemoveTreeRootPath(const QString &path);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void ChangeFilePathWithoutUndo(const QString &new_path);
|
void ChangeFilePathWithoutUndo(const QString &new_path);
|
||||||
void SetupTreeView();
|
|
||||||
void SaveTreeRootPaths();
|
|
||||||
void AddRootButtonClicked();
|
|
||||||
void RemoveRootButtonClicked();
|
|
||||||
void UpdateViewModeUI();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
class UndoCommand : public QUndoCommand {
|
class UndoCommand : public QUndoCommand {
|
||||||
@@ -122,21 +110,16 @@ class FileView : public QWidget {
|
|||||||
Ui_FileView *ui_;
|
Ui_FileView *ui_;
|
||||||
|
|
||||||
QFileSystemModel *model_;
|
QFileSystemModel *model_;
|
||||||
FileViewTreeModel *tree_model_;
|
|
||||||
QUndoStack *undo_stack_;
|
QUndoStack *undo_stack_;
|
||||||
|
|
||||||
SharedPtr<TaskManager> task_manager_;
|
SharedPtr<TaskManager> task_manager_;
|
||||||
SharedPtr<MusicStorage> storage_;
|
SharedPtr<MusicStorage> storage_;
|
||||||
|
|
||||||
QString lazy_set_path_;
|
QString lazy_set_path_;
|
||||||
QStringList tree_root_paths_;
|
|
||||||
|
|
||||||
QStringList filter_list_;
|
QStringList filter_list_;
|
||||||
|
|
||||||
ScopedPtr<QFileIconProvider> file_icon_provider_;
|
ScopedPtr<QFileIconProvider> file_icon_provider_;
|
||||||
|
|
||||||
bool tree_view_active_;
|
|
||||||
QSpacerItem *view_mode_spacer_;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // FILEVIEW_H
|
#endif // FILEVIEW_H
|
||||||
|
|||||||
@@ -95,78 +95,8 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QLineEdit" name="path"/>
|
<widget class="QLineEdit" name="path"/>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
|
||||||
<widget class="QToolButton" name="add_tree_root">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Add root directory</string>
|
|
||||||
</property>
|
|
||||||
<property name="iconSize">
|
|
||||||
<size>
|
|
||||||
<width>22</width>
|
|
||||||
<height>22</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
<property name="autoRaise">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QToolButton" name="remove_tree_root">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Remove selected root directory</string>
|
|
||||||
</property>
|
|
||||||
<property name="iconSize">
|
|
||||||
<size>
|
|
||||||
<width>22</width>
|
|
||||||
<height>22</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
<property name="autoRaise">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QToolButton" name="toggle_view">
|
|
||||||
<property name="iconSize">
|
|
||||||
<size>
|
|
||||||
<width>22</width>
|
|
||||||
<height>22</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
<property name="autoRaise">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Toggle between list and tree view</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
|
||||||
<widget class="QStackedWidget" name="view_stack">
|
|
||||||
<property name="currentIndex">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<widget class="QWidget" name="list_page">
|
|
||||||
<layout class="QVBoxLayout" name="list_layout">
|
|
||||||
<property name="spacing">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<property name="leftMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<property name="topMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<property name="rightMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<property name="bottomMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<item>
|
<item>
|
||||||
<widget class="FileViewList" name="list">
|
<widget class="FileViewList" name="list">
|
||||||
<property name="dragEnabled">
|
<property name="dragEnabled">
|
||||||
@@ -191,62 +121,12 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QWidget" name="tree_page">
|
|
||||||
<layout class="QVBoxLayout" name="tree_layout">
|
|
||||||
<property name="spacing">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<property name="leftMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<property name="topMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<property name="rightMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<property name="bottomMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<item>
|
|
||||||
<widget class="FileViewTree" name="tree">
|
|
||||||
<property name="dragEnabled">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
<property name="dragDropMode">
|
|
||||||
<enum>QAbstractItemView::DragOnly</enum>
|
|
||||||
</property>
|
|
||||||
<property name="selectionMode">
|
|
||||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
|
||||||
</property>
|
|
||||||
<property name="selectionBehavior">
|
|
||||||
<enum>QAbstractItemView::SelectRows</enum>
|
|
||||||
</property>
|
|
||||||
<property name="iconSize">
|
|
||||||
<size>
|
|
||||||
<width>16</width>
|
|
||||||
<height>16</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>FileViewList</class>
|
<class>FileViewList</class>
|
||||||
<extends>QListView</extends>
|
<extends>QListView</extends>
|
||||||
<header>fileview/fileviewlist.h</header>
|
<header>fileview/fileviewlist.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
<customwidget>
|
|
||||||
<class>FileViewTree</class>
|
|
||||||
<extends>QTreeView</extends>
|
|
||||||
<header>fileview/fileviewtree.h</header>
|
|
||||||
</customwidget>
|
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<resources/>
|
<resources/>
|
||||||
<connections/>
|
<connections/>
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ MimeData *FileViewList::MimeDataFromSelection() const {
|
|||||||
|
|
||||||
const QStringList filenames = FilenamesFromSelection();
|
const QStringList filenames = FilenamesFromSelection();
|
||||||
|
|
||||||
// If just one folder selected - use its path as the new playlist's name
|
// if just one folder selected - use its path as the new playlist's name
|
||||||
if (filenames.size() == 1 && QFileInfo(filenames.first()).isDir()) {
|
if (filenames.size() == 1 && QFileInfo(filenames.first()).isDir()) {
|
||||||
if (filenames.first().length() > 20) {
|
if (filenames.first().length() > 20) {
|
||||||
mimedata->name_for_new_playlist_ = QDir(filenames.first()).dirName();
|
mimedata->name_for_new_playlist_ = QDir(filenames.first()).dirName();
|
||||||
@@ -108,7 +108,7 @@ MimeData *FileViewList::MimeDataFromSelection() const {
|
|||||||
mimedata->name_for_new_playlist_ = filenames.first();
|
mimedata->name_for_new_playlist_ = filenames.first();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Otherwise, use the current root path
|
// otherwise, use the current root path
|
||||||
else {
|
else {
|
||||||
QString path = qobject_cast<QFileSystemModel*>(model())->rootPath();
|
QString path = qobject_cast<QFileSystemModel*>(model())->rootPath();
|
||||||
if (path.length() > 20) {
|
if (path.length() > 20) {
|
||||||
@@ -196,11 +196,11 @@ void FileViewList::mousePressEvent(QMouseEvent *e) {
|
|||||||
case Qt::XButton2:
|
case Qt::XButton2:
|
||||||
Q_EMIT Forward();
|
Q_EMIT Forward();
|
||||||
break;
|
break;
|
||||||
// Enqueue to playlist with middleClick
|
// enqueue to playlist with middleClick
|
||||||
case Qt::MiddleButton:{
|
case Qt::MiddleButton:{
|
||||||
QListView::mousePressEvent(e);
|
QListView::mousePressEvent(e);
|
||||||
|
|
||||||
// We need to update the menu selection
|
// we need to update the menu selection
|
||||||
menu_selection_ = selectionModel()->selection();
|
menu_selection_ = selectionModel()->selection();
|
||||||
|
|
||||||
MimeData *mimedata = new MimeData;
|
MimeData *mimedata = new MimeData;
|
||||||
|
|||||||
@@ -1,205 +0,0 @@
|
|||||||
/*
|
|
||||||
* Strawberry Music Player
|
|
||||||
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
|
|
||||||
*
|
|
||||||
* Strawberry is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Strawberry is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
#include <QWidget>
|
|
||||||
#include <QAbstractItemModel>
|
|
||||||
#include <QFileInfo>
|
|
||||||
#include <QDir>
|
|
||||||
#include <QMenu>
|
|
||||||
#include <QUrl>
|
|
||||||
#include <QCollator>
|
|
||||||
#include <QtEvents>
|
|
||||||
|
|
||||||
#include "core/iconloader.h"
|
|
||||||
#include "core/mimedata.h"
|
|
||||||
#include "utilities/filemanagerutils.h"
|
|
||||||
#include "fileviewtree.h"
|
|
||||||
#include "fileviewtreemodel.h"
|
|
||||||
|
|
||||||
using namespace Qt::Literals::StringLiterals;
|
|
||||||
|
|
||||||
FileViewTree::FileViewTree(QWidget *parent)
|
|
||||||
: QTreeView(parent),
|
|
||||||
menu_(new QMenu(this)) {
|
|
||||||
|
|
||||||
menu_->addAction(IconLoader::Load(u"media-playback-start"_s), tr("Append to current playlist"), this, &FileViewTree::AddToPlaylistSlot);
|
|
||||||
menu_->addAction(IconLoader::Load(u"media-playback-start"_s), tr("Replace current playlist"), this, &FileViewTree::LoadSlot);
|
|
||||||
menu_->addAction(IconLoader::Load(u"document-new"_s), tr("Open in new playlist"), this, &FileViewTree::OpenInNewPlaylistSlot);
|
|
||||||
menu_->addSeparator();
|
|
||||||
menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Copy to collection..."), this, &FileViewTree::CopyToCollectionSlot);
|
|
||||||
menu_->addAction(IconLoader::Load(u"go-jump"_s), tr("Move to collection..."), this, &FileViewTree::MoveToCollectionSlot);
|
|
||||||
menu_->addAction(IconLoader::Load(u"device"_s), tr("Copy to device..."), this, &FileViewTree::CopyToDeviceSlot);
|
|
||||||
menu_->addAction(IconLoader::Load(u"edit-delete"_s), tr("Delete from disk..."), this, &FileViewTree::DeleteSlot);
|
|
||||||
|
|
||||||
menu_->addSeparator();
|
|
||||||
menu_->addAction(IconLoader::Load(u"edit-rename"_s), tr("Edit track information..."), this, &FileViewTree::EditTagsSlot);
|
|
||||||
menu_->addAction(IconLoader::Load(u"document-open-folder"_s), tr("Show in file browser..."), this, &FileViewTree::ShowInBrowser);
|
|
||||||
|
|
||||||
setAttribute(Qt::WA_MacShowFocusRect, false);
|
|
||||||
setHeaderHidden(true);
|
|
||||||
setUniformRowHeights(true);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileViewTree::contextMenuEvent(QContextMenuEvent *e) {
|
|
||||||
|
|
||||||
menu_selection_ = selectionModel()->selection();
|
|
||||||
|
|
||||||
menu_->popup(e->globalPos());
|
|
||||||
e->accept();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList FileViewTree::FilenamesFromSelection() const {
|
|
||||||
|
|
||||||
QStringList filenames;
|
|
||||||
const QModelIndexList indexes = menu_selection_.indexes();
|
|
||||||
|
|
||||||
FileViewTreeModel *tree_model = qobject_cast<FileViewTreeModel*>(model());
|
|
||||||
if (tree_model) {
|
|
||||||
for (const QModelIndex &index : indexes) {
|
|
||||||
if (index.column() == 0) {
|
|
||||||
QString path = tree_model->data(index, FileViewTreeModel::Role_FilePath).toString();
|
|
||||||
if (!path.isEmpty()) {
|
|
||||||
filenames << path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QCollator collator;
|
|
||||||
collator.setNumericMode(true);
|
|
||||||
std::sort(filenames.begin(), filenames.end(), collator);
|
|
||||||
|
|
||||||
return filenames;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
QList<QUrl> FileViewTree::UrlListFromSelection() const {
|
|
||||||
|
|
||||||
QList<QUrl> urls;
|
|
||||||
const QStringList filenames = FilenamesFromSelection();
|
|
||||||
urls.reserve(filenames.count());
|
|
||||||
for (const QString &filename : std::as_const(filenames)) {
|
|
||||||
urls << QUrl::fromLocalFile(filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
return urls;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MimeData *FileViewTree::MimeDataFromSelection() const {
|
|
||||||
|
|
||||||
MimeData *mimedata = new MimeData;
|
|
||||||
mimedata->setUrls(UrlListFromSelection());
|
|
||||||
|
|
||||||
const QStringList filenames = FilenamesFromSelection();
|
|
||||||
|
|
||||||
// if just one folder selected - use its path as the new playlist's name
|
|
||||||
if (filenames.size() == 1 && QFileInfo(filenames.first()).isDir()) {
|
|
||||||
if (filenames.first().length() > 20) {
|
|
||||||
mimedata->name_for_new_playlist_ = QDir(filenames.first()).dirName();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
mimedata->name_for_new_playlist_ = filenames.first();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// otherwise, use "Files" as default
|
|
||||||
else {
|
|
||||||
mimedata->name_for_new_playlist_ = tr("Files");
|
|
||||||
}
|
|
||||||
|
|
||||||
return mimedata;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileViewTree::LoadSlot() {
|
|
||||||
|
|
||||||
MimeData *mimedata = MimeDataFromSelection();
|
|
||||||
mimedata->clear_first_ = true;
|
|
||||||
Q_EMIT AddToPlaylist(mimedata);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileViewTree::AddToPlaylistSlot() {
|
|
||||||
Q_EMIT AddToPlaylist(MimeDataFromSelection());
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileViewTree::OpenInNewPlaylistSlot() {
|
|
||||||
|
|
||||||
MimeData *mimedata = MimeDataFromSelection();
|
|
||||||
mimedata->open_in_new_playlist_ = true;
|
|
||||||
Q_EMIT AddToPlaylist(mimedata);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileViewTree::CopyToCollectionSlot() {
|
|
||||||
Q_EMIT CopyToCollection(UrlListFromSelection());
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileViewTree::MoveToCollectionSlot() {
|
|
||||||
Q_EMIT MoveToCollection(UrlListFromSelection());
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileViewTree::CopyToDeviceSlot() {
|
|
||||||
Q_EMIT CopyToDevice(UrlListFromSelection());
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileViewTree::DeleteSlot() {
|
|
||||||
Q_EMIT Delete(FilenamesFromSelection());
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileViewTree::EditTagsSlot() {
|
|
||||||
Q_EMIT EditTags(UrlListFromSelection());
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileViewTree::mousePressEvent(QMouseEvent *e) {
|
|
||||||
|
|
||||||
switch (e->button()) {
|
|
||||||
// Enqueue to playlist with middleClick
|
|
||||||
case Qt::MiddleButton:{
|
|
||||||
QTreeView::mousePressEvent(e);
|
|
||||||
|
|
||||||
// We need to update the menu selection
|
|
||||||
QItemSelectionModel *selection_model = selectionModel();
|
|
||||||
if (!selection_model) {
|
|
||||||
e->ignore();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
menu_selection_ = selection_model->selection();
|
|
||||||
|
|
||||||
MimeData *mimedata = new MimeData;
|
|
||||||
mimedata->setUrls(UrlListFromSelection());
|
|
||||||
mimedata->enqueue_now_ = true;
|
|
||||||
Q_EMIT AddToPlaylist(mimedata);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
QTreeView::mousePressEvent(e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileViewTree::ShowInBrowser() {
|
|
||||||
Utilities::OpenInFileBrowser(UrlListFromSelection());
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
/*
|
|
||||||
* Strawberry Music Player
|
|
||||||
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
|
|
||||||
*
|
|
||||||
* Strawberry is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Strawberry is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef FILEVIEWTREE_H
|
|
||||||
#define FILEVIEWTREE_H
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QTreeView>
|
|
||||||
#include <QList>
|
|
||||||
#include <QUrl>
|
|
||||||
#include <QString>
|
|
||||||
#include <QStringList>
|
|
||||||
|
|
||||||
class QWidget;
|
|
||||||
class QMimeData;
|
|
||||||
class QMenu;
|
|
||||||
class QMouseEvent;
|
|
||||||
class QContextMenuEvent;
|
|
||||||
|
|
||||||
class MimeData;
|
|
||||||
|
|
||||||
class FileViewTree : public QTreeView {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit FileViewTree(QWidget *parent = nullptr);
|
|
||||||
|
|
||||||
void mousePressEvent(QMouseEvent *e) override;
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
|
||||||
void AddToPlaylist(QMimeData *data);
|
|
||||||
void CopyToCollection(const QList<QUrl> &urls);
|
|
||||||
void MoveToCollection(const QList<QUrl> &urls);
|
|
||||||
void CopyToDevice(const QList<QUrl> &urls);
|
|
||||||
void Delete(const QStringList &filenames);
|
|
||||||
void EditTags(const QList<QUrl> &urls);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void contextMenuEvent(QContextMenuEvent *e) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
QStringList FilenamesFromSelection() const;
|
|
||||||
QList<QUrl> UrlListFromSelection() const;
|
|
||||||
MimeData *MimeDataFromSelection() const;
|
|
||||||
|
|
||||||
private Q_SLOTS:
|
|
||||||
void LoadSlot();
|
|
||||||
void AddToPlaylistSlot();
|
|
||||||
void OpenInNewPlaylistSlot();
|
|
||||||
void CopyToCollectionSlot();
|
|
||||||
void MoveToCollectionSlot();
|
|
||||||
void CopyToDeviceSlot();
|
|
||||||
void DeleteSlot();
|
|
||||||
void EditTagsSlot();
|
|
||||||
void ShowInBrowser();
|
|
||||||
|
|
||||||
private:
|
|
||||||
QMenu *menu_;
|
|
||||||
QItemSelection menu_selection_;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // FILEVIEWTREE_H
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
/*
|
|
||||||
* Strawberry Music Player
|
|
||||||
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
|
|
||||||
*
|
|
||||||
* Strawberry is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Strawberry is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef FILEVIEWTREEITEM_H
|
|
||||||
#define FILEVIEWTREEITEM_H
|
|
||||||
|
|
||||||
#include "config.h"
|
|
||||||
|
|
||||||
#include <QFileInfo>
|
|
||||||
|
|
||||||
#include "core/simpletreeitem.h"
|
|
||||||
|
|
||||||
class FileViewTreeItem : public SimpleTreeItem<FileViewTreeItem> {
|
|
||||||
public:
|
|
||||||
enum class Type {
|
|
||||||
Root, // Hidden root
|
|
||||||
VirtualRoot, // User-configured root paths
|
|
||||||
Directory, // File system directory
|
|
||||||
File // File system file
|
|
||||||
};
|
|
||||||
|
|
||||||
explicit FileViewTreeItem(SimpleTreeModel<FileViewTreeItem> *_model) : SimpleTreeItem<FileViewTreeItem>(_model), type(Type::Root), lazy_loaded(false) {}
|
|
||||||
explicit FileViewTreeItem(const Type _type, FileViewTreeItem *_parent = nullptr) : SimpleTreeItem<FileViewTreeItem>(_parent), type(_type), lazy_loaded(false) {}
|
|
||||||
|
|
||||||
Type type;
|
|
||||||
QString file_path; // Absolute file system path
|
|
||||||
QFileInfo file_info; // Cached file info
|
|
||||||
bool lazy_loaded; // Whether children have been loaded
|
|
||||||
|
|
||||||
private:
|
|
||||||
Q_DISABLE_COPY(FileViewTreeItem)
|
|
||||||
};
|
|
||||||
|
|
||||||
Q_DECLARE_METATYPE(FileViewTreeItem::Type)
|
|
||||||
|
|
||||||
#endif // FILEVIEWTREEITEM_H
|
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
/*
|
|
||||||
* Strawberry Music Player
|
|
||||||
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
|
|
||||||
*
|
|
||||||
* Strawberry is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Strawberry is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QVariant>
|
|
||||||
#include <QString>
|
|
||||||
#include <QStringList>
|
|
||||||
#include <QList>
|
|
||||||
#include <QMap>
|
|
||||||
#include <QDir>
|
|
||||||
#include <QFileInfo>
|
|
||||||
#include <QFileIconProvider>
|
|
||||||
#include <QMimeData>
|
|
||||||
#include <QUrl>
|
|
||||||
#include <QIcon>
|
|
||||||
|
|
||||||
#include "core/simpletreemodel.h"
|
|
||||||
#include "core/logging.h"
|
|
||||||
#include "fileviewtreemodel.h"
|
|
||||||
#include "fileviewtreeitem.h"
|
|
||||||
|
|
||||||
using namespace Qt::Literals::StringLiterals;
|
|
||||||
|
|
||||||
FileViewTreeModel::FileViewTreeModel(QObject *parent)
|
|
||||||
: SimpleTreeModel<FileViewTreeItem>(new FileViewTreeItem(this), parent),
|
|
||||||
icon_provider_(new QFileIconProvider()) {
|
|
||||||
}
|
|
||||||
|
|
||||||
FileViewTreeModel::~FileViewTreeModel() {
|
|
||||||
delete root_;
|
|
||||||
delete icon_provider_;
|
|
||||||
}
|
|
||||||
|
|
||||||
Qt::ItemFlags FileViewTreeModel::flags(const QModelIndex &idx) const {
|
|
||||||
|
|
||||||
const FileViewTreeItem *item = IndexToItem(idx);
|
|
||||||
if (!item) return Qt::NoItemFlags;
|
|
||||||
|
|
||||||
switch (item->type) {
|
|
||||||
case FileViewTreeItem::Type::VirtualRoot:
|
|
||||||
case FileViewTreeItem::Type::Directory:
|
|
||||||
case FileViewTreeItem::Type::File:
|
|
||||||
return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled;
|
|
||||||
case FileViewTreeItem::Type::Root:
|
|
||||||
default:
|
|
||||||
return Qt::ItemIsEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant FileViewTreeModel::data(const QModelIndex &idx, const int role) const {
|
|
||||||
|
|
||||||
if (!idx.isValid()) return QVariant();
|
|
||||||
|
|
||||||
const FileViewTreeItem *item = IndexToItem(idx);
|
|
||||||
if (!item) return QVariant();
|
|
||||||
|
|
||||||
switch (role) {
|
|
||||||
case Qt::DisplayRole:
|
|
||||||
if (item->type == FileViewTreeItem::Type::VirtualRoot) {
|
|
||||||
return item->display_text.isEmpty() ? item->file_path : item->display_text;
|
|
||||||
}
|
|
||||||
return item->file_info.fileName();
|
|
||||||
|
|
||||||
case Qt::DecorationRole:
|
|
||||||
return GetIcon(item);
|
|
||||||
|
|
||||||
case Role_Type:
|
|
||||||
return QVariant::fromValue(item->type);
|
|
||||||
|
|
||||||
case Role_FilePath:
|
|
||||||
return item->file_path;
|
|
||||||
|
|
||||||
case Role_FileName:
|
|
||||||
return item->file_info.fileName();
|
|
||||||
|
|
||||||
default:
|
|
||||||
return QVariant();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
bool FileViewTreeModel::hasChildren(const QModelIndex &parent) const {
|
|
||||||
|
|
||||||
const FileViewTreeItem *item = IndexToItem(parent);
|
|
||||||
if (!item) return false;
|
|
||||||
|
|
||||||
// Root and VirtualRoot always have children (or can have them)
|
|
||||||
if (item->type == FileViewTreeItem::Type::Root) return true;
|
|
||||||
if (item->type == FileViewTreeItem::Type::VirtualRoot) return true;
|
|
||||||
|
|
||||||
// Directories can have children
|
|
||||||
if (item->type == FileViewTreeItem::Type::Directory) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Files don't have children
|
|
||||||
return false;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
bool FileViewTreeModel::canFetchMore(const QModelIndex &parent) const {
|
|
||||||
|
|
||||||
const FileViewTreeItem *item = IndexToItem(parent);
|
|
||||||
if (!item) return false;
|
|
||||||
|
|
||||||
// Can fetch more if not yet lazy loaded
|
|
||||||
return !item->lazy_loaded && (item->type == FileViewTreeItem::Type::VirtualRoot || item->type == FileViewTreeItem::Type::Directory);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileViewTreeModel::fetchMore(const QModelIndex &parent) {
|
|
||||||
|
|
||||||
FileViewTreeItem *item = IndexToItem(parent);
|
|
||||||
if (!item || item->lazy_loaded) return;
|
|
||||||
|
|
||||||
LazyLoad(item);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileViewTreeModel::LazyLoad(FileViewTreeItem *item) {
|
|
||||||
|
|
||||||
if (item->lazy_loaded) return;
|
|
||||||
|
|
||||||
QDir dir(item->file_path);
|
|
||||||
if (!dir.exists()) {
|
|
||||||
item->lazy_loaded = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply name filters
|
|
||||||
const QDir::Filters filters = QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot;
|
|
||||||
if (!name_filters_.isEmpty()) {
|
|
||||||
dir.setNameFilters(name_filters_);
|
|
||||||
}
|
|
||||||
|
|
||||||
const QFileInfoList entries = dir.entryInfoList(filters, QDir::Name | QDir::DirsFirst);
|
|
||||||
if (!entries.isEmpty()) {
|
|
||||||
BeginInsert(item, 0, static_cast<int>(entries.count()) - 1);
|
|
||||||
|
|
||||||
for (const QFileInfo &entry : entries) {
|
|
||||||
FileViewTreeItem *child = new FileViewTreeItem(
|
|
||||||
entry.isDir() ? FileViewTreeItem::Type::Directory : FileViewTreeItem::Type::File,
|
|
||||||
item
|
|
||||||
);
|
|
||||||
child->file_path = entry.absoluteFilePath();
|
|
||||||
child->file_info = entry;
|
|
||||||
child->lazy_loaded = false;
|
|
||||||
child->display_text = entry.fileName();
|
|
||||||
}
|
|
||||||
|
|
||||||
EndInsert();
|
|
||||||
}
|
|
||||||
|
|
||||||
item->lazy_loaded = true;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
QIcon FileViewTreeModel::GetIcon(const FileViewTreeItem *item) const {
|
|
||||||
|
|
||||||
if (!item) return QIcon();
|
|
||||||
|
|
||||||
switch (item->type) {
|
|
||||||
case FileViewTreeItem::Type::VirtualRoot:
|
|
||||||
case FileViewTreeItem::Type::Directory:
|
|
||||||
return icon_provider_->icon(QFileIconProvider::Folder);
|
|
||||||
case FileViewTreeItem::Type::File:
|
|
||||||
return icon_provider_->icon(item->file_info);
|
|
||||||
default:
|
|
||||||
return QIcon();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList FileViewTreeModel::mimeTypes() const {
|
|
||||||
return QStringList() << u"text/uri-list"_s;
|
|
||||||
}
|
|
||||||
|
|
||||||
QMimeData *FileViewTreeModel::mimeData(const QModelIndexList &indexes) const {
|
|
||||||
|
|
||||||
if (indexes.isEmpty()) return nullptr;
|
|
||||||
|
|
||||||
QList<QUrl> urls;
|
|
||||||
for (const QModelIndex &idx : indexes) {
|
|
||||||
const FileViewTreeItem *item = IndexToItem(idx);
|
|
||||||
if (item && (item->type == FileViewTreeItem::Type::File || item->type == FileViewTreeItem::Type::Directory || item->type == FileViewTreeItem::Type::VirtualRoot)) {
|
|
||||||
urls << QUrl::fromLocalFile(item->file_path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (urls.isEmpty()) return nullptr;
|
|
||||||
|
|
||||||
QMimeData *data = new QMimeData();
|
|
||||||
data->setUrls(urls);
|
|
||||||
return data;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileViewTreeModel::SetRootPaths(const QStringList &paths) {
|
|
||||||
|
|
||||||
Reset();
|
|
||||||
|
|
||||||
for (const QString &path : paths) {
|
|
||||||
QFileInfo info(path);
|
|
||||||
if (!info.exists() || !info.isDir()) continue;
|
|
||||||
|
|
||||||
FileViewTreeItem *virtual_root = new FileViewTreeItem(FileViewTreeItem::Type::VirtualRoot, root_);
|
|
||||||
virtual_root->file_path = info.absoluteFilePath();
|
|
||||||
virtual_root->file_info = info;
|
|
||||||
virtual_root->display_text = info.absoluteFilePath();
|
|
||||||
virtual_root->lazy_loaded = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileViewTreeModel::SetNameFilters(const QStringList &filters) {
|
|
||||||
name_filters_ = filters;
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileViewTreeModel::Reset() {
|
|
||||||
|
|
||||||
beginResetModel();
|
|
||||||
|
|
||||||
// Clear children without notifications since we're in a reset
|
|
||||||
qDeleteAll(root_->children);
|
|
||||||
root_->children.clear();
|
|
||||||
|
|
||||||
endResetModel();
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
/*
|
|
||||||
* Strawberry Music Player
|
|
||||||
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
|
|
||||||
*
|
|
||||||
* Strawberry is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Strawberry is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef FILEVIEWTREEMODEL_H
|
|
||||||
#define FILEVIEWTREEMODEL_H
|
|
||||||
|
|
||||||
#include "config.h"
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QVariant>
|
|
||||||
#include <QStringList>
|
|
||||||
#include <QIcon>
|
|
||||||
|
|
||||||
#include "core/simpletreemodel.h"
|
|
||||||
#include "fileviewtreeitem.h"
|
|
||||||
|
|
||||||
class QFileIconProvider;
|
|
||||||
class QMimeData;
|
|
||||||
|
|
||||||
class FileViewTreeModel : public SimpleTreeModel<FileViewTreeItem> {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit FileViewTreeModel(QObject *parent = nullptr);
|
|
||||||
~FileViewTreeModel() override;
|
|
||||||
|
|
||||||
enum Role {
|
|
||||||
Role_Type = Qt::UserRole + 1,
|
|
||||||
Role_FilePath,
|
|
||||||
Role_FileName,
|
|
||||||
RoleCount
|
|
||||||
};
|
|
||||||
|
|
||||||
// QAbstractItemModel
|
|
||||||
Qt::ItemFlags flags(const QModelIndex &idx) const override;
|
|
||||||
QVariant data(const QModelIndex &idx, const int role) const override;
|
|
||||||
bool hasChildren(const QModelIndex &parent) const override;
|
|
||||||
bool canFetchMore(const QModelIndex &parent) const override;
|
|
||||||
void fetchMore(const QModelIndex &parent) override;
|
|
||||||
QStringList mimeTypes() const override;
|
|
||||||
QMimeData *mimeData(const QModelIndexList &indexes) const override;
|
|
||||||
|
|
||||||
void SetRootPaths(const QStringList &paths);
|
|
||||||
void SetNameFilters(const QStringList &filters);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void Reset();
|
|
||||||
void LazyLoad(FileViewTreeItem *item);
|
|
||||||
QIcon GetIcon(const FileViewTreeItem *item) const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
QFileIconProvider *icon_provider_;
|
|
||||||
QStringList name_filters_;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // FILEVIEWTREEMODEL_H
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
* Strawberry Music Player
|
* Strawberry Music Player
|
||||||
* This file was part of Clementine.
|
* This file was part of Clementine.
|
||||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||||
* Copyright 2018-2026, Jonas Kvinge <jonas@jkvinge.net>
|
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
* Copyright 2023, Daniel Ostertag <daniel.ostertag@dakes.de>
|
* Copyright 2023, Daniel Ostertag <daniel.ostertag@dakes.de>
|
||||||
*
|
*
|
||||||
* Strawberry is free software: you can redistribute it and/or modify
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
@@ -21,10 +21,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QMap>
|
|
||||||
|
|
||||||
#include "constants/timeconstants.h"
|
#include "constants/timeconstants.h"
|
||||||
#include "core/song.h"
|
|
||||||
#include "filterparser.h"
|
#include "filterparser.h"
|
||||||
#include "filtertreenop.h"
|
#include "filtertreenop.h"
|
||||||
#include "filtertreeand.h"
|
#include "filtertreeand.h"
|
||||||
@@ -33,126 +31,9 @@
|
|||||||
#include "filtertreeterm.h"
|
#include "filtertreeterm.h"
|
||||||
#include "filtertreecolumnterm.h"
|
#include "filtertreecolumnterm.h"
|
||||||
#include "filterparsersearchcomparators.h"
|
#include "filterparsersearchcomparators.h"
|
||||||
#include "filtercolumn.h"
|
|
||||||
|
|
||||||
using namespace Qt::Literals::StringLiterals;
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
enum class FilterOperator {
|
|
||||||
None,
|
|
||||||
Eq,
|
|
||||||
Ne,
|
|
||||||
Gt,
|
|
||||||
Ge,
|
|
||||||
Lt,
|
|
||||||
Le
|
|
||||||
};
|
|
||||||
|
|
||||||
const QMap<QString, FilterOperator> &GetFilterOperatorsMap() {
|
|
||||||
|
|
||||||
static const QMap<QString, FilterOperator> filter_operators_map_ = []() {
|
|
||||||
QMap<QString, FilterOperator> filter_operators_map;
|
|
||||||
filter_operators_map.insert(u"="_s, FilterOperator::Eq);
|
|
||||||
filter_operators_map.insert(u"=="_s, FilterOperator::Eq);
|
|
||||||
filter_operators_map.insert(u"!="_s, FilterOperator::Ne);
|
|
||||||
filter_operators_map.insert(u"<>"_s, FilterOperator::Ne);
|
|
||||||
filter_operators_map.insert(u">"_s, FilterOperator::Gt);
|
|
||||||
filter_operators_map.insert(u">="_s, FilterOperator::Ge);
|
|
||||||
filter_operators_map.insert(u"<"_s, FilterOperator::Lt);
|
|
||||||
filter_operators_map.insert(u"<="_s, FilterOperator::Le);
|
|
||||||
return filter_operators_map;
|
|
||||||
}();
|
|
||||||
|
|
||||||
return filter_operators_map_;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class ColumnType {
|
|
||||||
Unknown,
|
|
||||||
Text,
|
|
||||||
Int,
|
|
||||||
UInt,
|
|
||||||
Int64,
|
|
||||||
Float
|
|
||||||
};
|
|
||||||
|
|
||||||
const QMap<QString, FilterColumn> &GetFilterColumnsMap() {
|
|
||||||
|
|
||||||
static const QMap<QString, FilterColumn> filter_columns_map_ = []() {
|
|
||||||
QMap<QString, FilterColumn> filter_columns_map;
|
|
||||||
filter_columns_map.insert(u"albumartist"_s, FilterColumn::AlbumArtist);
|
|
||||||
filter_columns_map.insert(u"albumartistsort"_s, FilterColumn::AlbumArtistSort);
|
|
||||||
filter_columns_map.insert(u"artist"_s, FilterColumn::Artist);
|
|
||||||
filter_columns_map.insert(u"artistsort"_s, FilterColumn::ArtistSort);
|
|
||||||
filter_columns_map.insert(u"album"_s, FilterColumn::Album);
|
|
||||||
filter_columns_map.insert(u"albumsort"_s, FilterColumn::AlbumSort);
|
|
||||||
filter_columns_map.insert(u"title"_s, FilterColumn::Title);
|
|
||||||
filter_columns_map.insert(u"titlesort"_s, FilterColumn::TitleSort);
|
|
||||||
filter_columns_map.insert(u"composer"_s, FilterColumn::Composer);
|
|
||||||
filter_columns_map.insert(u"composersort"_s, FilterColumn::ComposerSort);
|
|
||||||
filter_columns_map.insert(u"performer"_s, FilterColumn::Performer);
|
|
||||||
filter_columns_map.insert(u"performersort"_s, FilterColumn::PerformerSort);
|
|
||||||
filter_columns_map.insert(u"grouping"_s, FilterColumn::Grouping);
|
|
||||||
filter_columns_map.insert(u"genre"_s, FilterColumn::Genre);
|
|
||||||
filter_columns_map.insert(u"comment"_s, FilterColumn::Comment);
|
|
||||||
filter_columns_map.insert(u"filename"_s, FilterColumn::Filename);
|
|
||||||
filter_columns_map.insert(u"url"_s, FilterColumn::URL);
|
|
||||||
filter_columns_map.insert(u"track"_s, FilterColumn::Track);
|
|
||||||
filter_columns_map.insert(u"year"_s, FilterColumn::Year);
|
|
||||||
filter_columns_map.insert(u"samplerate"_s, FilterColumn::Samplerate);
|
|
||||||
filter_columns_map.insert(u"bitdepth"_s, FilterColumn::Bitdepth);
|
|
||||||
filter_columns_map.insert(u"bitrate"_s, FilterColumn::Bitrate);
|
|
||||||
filter_columns_map.insert(u"playcount"_s, FilterColumn::Playcount);
|
|
||||||
filter_columns_map.insert(u"skipcount"_s, FilterColumn::Skipcount);
|
|
||||||
filter_columns_map.insert(u"length"_s, FilterColumn::Length);
|
|
||||||
filter_columns_map.insert(u"rating"_s, FilterColumn::Rating);
|
|
||||||
return filter_columns_map;
|
|
||||||
}();
|
|
||||||
|
|
||||||
return filter_columns_map_;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const QMap<FilterColumn, ColumnType> &GetColumnTypesMap() {
|
|
||||||
|
|
||||||
static const QMap<FilterColumn, ColumnType> column_types_map_ = []() {
|
|
||||||
QMap<FilterColumn, ColumnType> column_types_map;
|
|
||||||
column_types_map.insert(FilterColumn::AlbumArtist, ColumnType::Text);
|
|
||||||
column_types_map.insert(FilterColumn::AlbumArtistSort, ColumnType::Text);
|
|
||||||
column_types_map.insert(FilterColumn::Artist, ColumnType::Text);
|
|
||||||
column_types_map.insert(FilterColumn::ArtistSort, ColumnType::Text);
|
|
||||||
column_types_map.insert(FilterColumn::Album, ColumnType::Text);
|
|
||||||
column_types_map.insert(FilterColumn::AlbumSort, ColumnType::Text);
|
|
||||||
column_types_map.insert(FilterColumn::Title, ColumnType::Text);
|
|
||||||
column_types_map.insert(FilterColumn::TitleSort, ColumnType::Text);
|
|
||||||
column_types_map.insert(FilterColumn::Composer, ColumnType::Text);
|
|
||||||
column_types_map.insert(FilterColumn::ComposerSort, ColumnType::Text);
|
|
||||||
column_types_map.insert(FilterColumn::Performer, ColumnType::Text);
|
|
||||||
column_types_map.insert(FilterColumn::PerformerSort, ColumnType::Text);
|
|
||||||
column_types_map.insert(FilterColumn::Grouping, ColumnType::Text);
|
|
||||||
column_types_map.insert(FilterColumn::Genre, ColumnType::Text);
|
|
||||||
column_types_map.insert(FilterColumn::Comment, ColumnType::Text);
|
|
||||||
column_types_map.insert(FilterColumn::Filename, ColumnType::Text);
|
|
||||||
column_types_map.insert(FilterColumn::URL, ColumnType::Text);
|
|
||||||
column_types_map.insert(FilterColumn::Track, ColumnType::Int);
|
|
||||||
column_types_map.insert(FilterColumn::Year, ColumnType::Int);
|
|
||||||
column_types_map.insert(FilterColumn::Samplerate, ColumnType::Int);
|
|
||||||
column_types_map.insert(FilterColumn::Bitdepth, ColumnType::Int);
|
|
||||||
column_types_map.insert(FilterColumn::Bitrate, ColumnType::Int);
|
|
||||||
column_types_map.insert(FilterColumn::Playcount, ColumnType::UInt);
|
|
||||||
column_types_map.insert(FilterColumn::Skipcount, ColumnType::UInt);
|
|
||||||
column_types_map.insert(FilterColumn::Length, ColumnType::Int64);
|
|
||||||
column_types_map.insert(FilterColumn::Rating, ColumnType::Float);
|
|
||||||
return column_types_map;
|
|
||||||
}();
|
|
||||||
|
|
||||||
return column_types_map_;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
FilterParser::FilterParser(const QString &filter_string) : filter_string_(filter_string), iter_{}, end_{} {}
|
FilterParser::FilterParser(const QString &filter_string) : filter_string_(filter_string), iter_{}, end_{} {}
|
||||||
|
|
||||||
FilterTree *FilterParser::parse() {
|
FilterTree *FilterParser::parse() {
|
||||||
@@ -238,7 +119,7 @@ bool FilterParser::checkAnd() {
|
|||||||
bool FilterParser::checkOr(const bool step_over) {
|
bool FilterParser::checkOr(const bool step_over) {
|
||||||
|
|
||||||
if (!buf_.isEmpty()) {
|
if (!buf_.isEmpty()) {
|
||||||
if (buf_.size() == 2 && buf_[0] == u'O' && buf_[1] == u'R') {
|
if (buf_ == "OR"_L1) {
|
||||||
if (step_over) {
|
if (step_over) {
|
||||||
buf_.clear();
|
buf_.clear();
|
||||||
advance();
|
advance();
|
||||||
@@ -260,8 +141,7 @@ bool FilterParser::checkOr(const bool step_over) {
|
|||||||
advance();
|
advance();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
buf_ += u'O';
|
buf_ += "OR"_L1;
|
||||||
buf_ += u'R';
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -311,8 +191,6 @@ FilterTree *FilterParser::parseSearchTerm() {
|
|||||||
bool in_quotes = false;
|
bool in_quotes = false;
|
||||||
bool previous_char_operator = false;
|
bool previous_char_operator = false;
|
||||||
|
|
||||||
buf_.reserve(32);
|
|
||||||
|
|
||||||
for (; iter_ != end_; ++iter_) {
|
for (; iter_ != end_; ++iter_) {
|
||||||
if (previous_char_operator) {
|
if (previous_char_operator) {
|
||||||
if (iter_->isSpace()) {
|
if (iter_->isSpace()) {
|
||||||
@@ -347,7 +225,7 @@ FilterTree *FilterParser::parseSearchTerm() {
|
|||||||
prefix += *iter_;
|
prefix += *iter_;
|
||||||
previous_char_operator = true;
|
previous_char_operator = true;
|
||||||
}
|
}
|
||||||
else if (prefix.size() == 1 && prefix[0] != u'=' && *iter_ == u'=') {
|
else if (prefix != u'=' && *iter_ == u'=') {
|
||||||
prefix += *iter_;
|
prefix += *iter_;
|
||||||
previous_char_operator = true;
|
previous_char_operator = true;
|
||||||
}
|
}
|
||||||
@@ -374,145 +252,132 @@ FilterTree *FilterParser::createSearchTermTreeNode(const QString &column, const
|
|||||||
return new FilterTreeNop;
|
return new FilterTreeNop;
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterColumn filter_column = FilterColumn::Unknown;
|
|
||||||
FilterParserSearchTermComparator *cmp = nullptr;
|
FilterParserSearchTermComparator *cmp = nullptr;
|
||||||
|
|
||||||
if (!column.isEmpty()) {
|
if (!column.isEmpty()) {
|
||||||
filter_column = GetFilterColumnsMap().value(column, FilterColumn::Unknown);
|
if (Song::kTextSearchColumns.contains(column, Qt::CaseInsensitive)) {
|
||||||
const ColumnType column_type = GetColumnTypesMap().value(filter_column, ColumnType::Unknown);
|
if (prefix == u'=' || prefix == "=="_L1) {
|
||||||
const FilterOperator filter_operator = GetFilterOperatorsMap().value(prefix, FilterOperator::None);
|
|
||||||
switch (column_type) {
|
|
||||||
case ColumnType::Text:{
|
|
||||||
switch (filter_operator) {
|
|
||||||
case FilterOperator::Eq:
|
|
||||||
cmp = new FilterParserTextEqComparator(value);
|
cmp = new FilterParserTextEqComparator(value);
|
||||||
break;
|
}
|
||||||
case FilterOperator::Ne:
|
else if (prefix == "!="_L1 || prefix == "<>"_L1) {
|
||||||
cmp = new FilterParserTextNeComparator(value);
|
cmp = new FilterParserTextNeComparator(value);
|
||||||
break;
|
}
|
||||||
default:
|
else {
|
||||||
cmp = new FilterParserTextContainsComparator(value);
|
cmp = new FilterParserTextContainsComparator(value);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case ColumnType::Int:{
|
else if (Song::kIntSearchColumns.contains(column, Qt::CaseInsensitive)) {
|
||||||
bool ok = false;
|
bool ok = false;
|
||||||
const int number = value.toInt(&ok);
|
int number = value.toInt(&ok);
|
||||||
if (!ok) break;
|
if (ok) {
|
||||||
switch (filter_operator) {
|
if (prefix == u'=' || prefix == "=="_L1) {
|
||||||
case FilterOperator::None:
|
|
||||||
case FilterOperator::Eq:
|
|
||||||
cmp = new FilterParserIntEqComparator(number);
|
cmp = new FilterParserIntEqComparator(number);
|
||||||
break;
|
}
|
||||||
case FilterOperator::Ne:
|
else if (prefix == "!="_L1 || prefix == "<>"_L1) {
|
||||||
cmp = new FilterParserIntNeComparator(number);
|
cmp = new FilterParserIntNeComparator(number);
|
||||||
break;
|
}
|
||||||
case FilterOperator::Gt:
|
else if (prefix == u'>') {
|
||||||
cmp = new FilterParserIntGtComparator(number);
|
cmp = new FilterParserIntGtComparator(number);
|
||||||
break;
|
}
|
||||||
case FilterOperator::Ge:
|
else if (prefix == ">="_L1) {
|
||||||
cmp = new FilterParserIntGeComparator(number);
|
cmp = new FilterParserIntGeComparator(number);
|
||||||
break;
|
}
|
||||||
case FilterOperator::Lt:
|
else if (prefix == u'<') {
|
||||||
cmp = new FilterParserIntLtComparator(number);
|
cmp = new FilterParserIntLtComparator(number);
|
||||||
break;
|
}
|
||||||
case FilterOperator::Le:
|
else if (prefix == "<="_L1) {
|
||||||
cmp = new FilterParserIntLeComparator(number);
|
cmp = new FilterParserIntLeComparator(number);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
break;
|
else {
|
||||||
|
cmp = new FilterParserIntEqComparator(number);
|
||||||
}
|
}
|
||||||
case ColumnType::UInt:{
|
}
|
||||||
|
}
|
||||||
|
else if (Song::kUIntSearchColumns.contains(column, Qt::CaseInsensitive)) {
|
||||||
bool ok = false;
|
bool ok = false;
|
||||||
const uint number = value.toUInt(&ok);
|
uint number = value.toUInt(&ok);
|
||||||
if (!ok) break;
|
if (ok) {
|
||||||
switch (filter_operator) {
|
if (prefix == u'=' || prefix == "=="_L1) {
|
||||||
case FilterOperator::None:
|
|
||||||
case FilterOperator::Eq:
|
|
||||||
cmp = new FilterParserUIntEqComparator(number);
|
cmp = new FilterParserUIntEqComparator(number);
|
||||||
break;
|
}
|
||||||
case FilterOperator::Ne:
|
else if (prefix == "!="_L1 || prefix == "<>"_L1) {
|
||||||
cmp = new FilterParserUIntNeComparator(number);
|
cmp = new FilterParserUIntNeComparator(number);
|
||||||
break;
|
}
|
||||||
case FilterOperator::Gt:
|
else if (prefix == u'>') {
|
||||||
cmp = new FilterParserUIntGtComparator(number);
|
cmp = new FilterParserUIntGtComparator(number);
|
||||||
break;
|
}
|
||||||
case FilterOperator::Ge:
|
else if (prefix == ">="_L1) {
|
||||||
cmp = new FilterParserUIntGeComparator(number);
|
cmp = new FilterParserUIntGeComparator(number);
|
||||||
break;
|
}
|
||||||
case FilterOperator::Lt:
|
else if (prefix == u'<') {
|
||||||
cmp = new FilterParserUIntLtComparator(number);
|
cmp = new FilterParserUIntLtComparator(number);
|
||||||
break;
|
}
|
||||||
case FilterOperator::Le:
|
else if (prefix == "<="_L1) {
|
||||||
cmp = new FilterParserUIntLeComparator(number);
|
cmp = new FilterParserUIntLeComparator(number);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
break;
|
else {
|
||||||
|
cmp = new FilterParserUIntEqComparator(number);
|
||||||
}
|
}
|
||||||
case ColumnType::Int64:{
|
}
|
||||||
|
}
|
||||||
|
else if (Song::kInt64SearchColumns.contains(column, Qt::CaseInsensitive)) {
|
||||||
qint64 number = 0;
|
qint64 number = 0;
|
||||||
if (filter_column == FilterColumn::Length) {
|
if (column == "length"_L1) {
|
||||||
number = ParseTime(value) * kNsecPerSec;
|
number = ParseTime(value) * kNsecPerSec;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
number = value.toLongLong();
|
number = value.toLongLong();
|
||||||
}
|
}
|
||||||
switch (filter_operator) {
|
if (prefix == u'=' || prefix == "=="_L1) {
|
||||||
case FilterOperator::None:
|
|
||||||
case FilterOperator::Eq:
|
|
||||||
cmp = new FilterParserInt64EqComparator(number);
|
cmp = new FilterParserInt64EqComparator(number);
|
||||||
break;
|
}
|
||||||
case FilterOperator::Ne:
|
else if (prefix == "!="_L1 || prefix == "<>"_L1) {
|
||||||
cmp = new FilterParserInt64NeComparator(number);
|
cmp = new FilterParserInt64NeComparator(number);
|
||||||
break;
|
}
|
||||||
case FilterOperator::Gt:
|
else if (prefix == u'>') {
|
||||||
cmp = new FilterParserInt64GtComparator(number);
|
cmp = new FilterParserInt64GtComparator(number);
|
||||||
break;
|
}
|
||||||
case FilterOperator::Ge:
|
else if (prefix == ">="_L1) {
|
||||||
cmp = new FilterParserInt64GeComparator(number);
|
cmp = new FilterParserInt64GeComparator(number);
|
||||||
break;
|
}
|
||||||
case FilterOperator::Lt:
|
else if (prefix == u'<') {
|
||||||
cmp = new FilterParserInt64LtComparator(number);
|
cmp = new FilterParserInt64LtComparator(number);
|
||||||
break;
|
}
|
||||||
case FilterOperator::Le:
|
else if (prefix == "<="_L1) {
|
||||||
cmp = new FilterParserInt64LeComparator(number);
|
cmp = new FilterParserInt64LeComparator(number);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
break;
|
else {
|
||||||
|
cmp = new FilterParserInt64EqComparator(number);
|
||||||
}
|
}
|
||||||
case ColumnType::Float:{
|
}
|
||||||
|
else if (Song::kFloatSearchColumns.contains(column, Qt::CaseInsensitive)) {
|
||||||
const float rating = ParseRating(value);
|
const float rating = ParseRating(value);
|
||||||
switch (filter_operator) {
|
if (prefix == u'=' || prefix == "=="_L1) {
|
||||||
case FilterOperator::None:
|
|
||||||
case FilterOperator::Eq:
|
|
||||||
cmp = new FilterParserFloatEqComparator(rating);
|
cmp = new FilterParserFloatEqComparator(rating);
|
||||||
break;
|
}
|
||||||
case FilterOperator::Ne:
|
else if (prefix == "!="_L1 || prefix == "<>"_L1) {
|
||||||
cmp = new FilterParserFloatNeComparator(rating);
|
cmp = new FilterParserFloatNeComparator(rating);
|
||||||
break;
|
}
|
||||||
case FilterOperator::Gt:
|
else if (prefix == u'>') {
|
||||||
cmp = new FilterParserFloatGtComparator(rating);
|
cmp = new FilterParserFloatGtComparator(rating);
|
||||||
break;
|
}
|
||||||
case FilterOperator::Ge:
|
else if (prefix == ">="_L1) {
|
||||||
cmp = new FilterParserFloatGeComparator(rating);
|
cmp = new FilterParserFloatGeComparator(rating);
|
||||||
break;
|
}
|
||||||
case FilterOperator::Lt:
|
else if (prefix == u'<') {
|
||||||
cmp = new FilterParserFloatLtComparator(rating);
|
cmp = new FilterParserFloatLtComparator(rating);
|
||||||
break;
|
}
|
||||||
case FilterOperator::Le:
|
else if (prefix == "<="_L1) {
|
||||||
cmp = new FilterParserFloatLeComparator(rating);
|
cmp = new FilterParserFloatLeComparator(rating);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
break;
|
else {
|
||||||
|
cmp = new FilterParserFloatEqComparator(rating);
|
||||||
}
|
}
|
||||||
case ColumnType::Unknown:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter_column != FilterColumn::Unknown && cmp != nullptr) {
|
if (cmp) {
|
||||||
return new FilterTreeColumnTerm(filter_column, cmp);
|
return new FilterTreeColumnTerm(column, cmp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new FilterTreeTerm(new FilterParserTextContainsComparator(value));
|
return new FilterTreeTerm(new FilterParserTextContainsComparator(value));
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user