mirror of
https://github.com/plexguide/Huntarr-Sonarr.git
synced 2025-12-16 20:04:16 -06:00
- Updated macOS installer workflows to include Apprise and its dependencies for notification support. - Modified Windows build script to explicitly install Apprise and its dependencies. - Enhanced huntarr.spec to bundle Apprise data files, addressing attachment directory issues. - Ensured all relevant Apprise modules are collected during the build process for both macOS and Windows.
485 lines
20 KiB
YAML
485 lines
20 KiB
YAML
name: macOS Installer Build (Intel x86_64)
|
|
|
|
# Add permissions needed for creating releases
|
|
permissions:
|
|
contents: write
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- '*' # This will trigger on any branch push
|
|
tags:
|
|
- "*" # This will trigger on any tag push
|
|
pull_request:
|
|
branches:
|
|
- main
|
|
|
|
jobs:
|
|
build-macos-intel-installer:
|
|
name: Build macOS Intel Installer
|
|
runs-on: macos-latest
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v3
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Set up Python 3.9
|
|
uses: actions/setup-python@v4
|
|
with:
|
|
python-version: '3.9'
|
|
|
|
- name: Install dependencies
|
|
run: |
|
|
python -m pip install --upgrade pip
|
|
pip install -r requirements.txt
|
|
pip install py2app==0.28.6 pyinstaller==6.1.0
|
|
|
|
- name: Extract metadata
|
|
id: meta
|
|
run: |
|
|
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
|
|
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
|
echo "IS_TAG=true" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "VERSION=$(cat version.txt)" >> $GITHUB_OUTPUT
|
|
echo "BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT
|
|
echo "IS_TAG=false" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
- name: Create app_launcher.py
|
|
run: |
|
|
cat > app_launcher.py << 'EOF'
|
|
#!/usr/bin/env python3
|
|
import os
|
|
import sys
|
|
import logging
|
|
import shutil
|
|
import json
|
|
import traceback
|
|
import time
|
|
from datetime import datetime
|
|
|
|
# Ensure Desktop log for critical errors
|
|
desktop_log = os.path.expanduser("~/Desktop/huntarr_error.log")
|
|
with open(desktop_log, "a") as f:
|
|
f.write(f"\n[{datetime.now().isoformat()}] Starting app_launcher.py\n")
|
|
|
|
# Setup base paths before anything else - Use a location that doesn't require admin privileges
|
|
# First try the app bundle location, then fall back to Documents folder which usually has permissions
|
|
bundle_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
|
|
if hasattr(sys, "_MEIPASS"):
|
|
bundle_dir = sys._MEIPASS
|
|
|
|
app_name = "Huntarr"
|
|
# Use app's own directory for storage to avoid permission issues
|
|
if os.path.exists(os.path.join(bundle_dir, "Contents", "Resources")):
|
|
# Running as a .app bundle
|
|
with open(desktop_log, "a") as f:
|
|
f.write(f"Detected running as .app bundle\n")
|
|
resources_dir = os.path.join(bundle_dir, "Contents", "Resources")
|
|
config_dir = os.path.join(resources_dir, "config")
|
|
else:
|
|
# Create config in user's Documents folder as a safer alternative
|
|
home = os.path.expanduser("~")
|
|
config_dir = os.path.join(home, "Documents", app_name, "config")
|
|
with open(desktop_log, "a") as f:
|
|
f.write(f"Using Documents folder for configuration: {config_dir}\n")
|
|
|
|
# Set up log directory
|
|
log_dir = os.path.join(config_dir, "logs")
|
|
|
|
# Create essential directories
|
|
for dir_path in [
|
|
config_dir,
|
|
os.path.join(config_dir, "settings"),
|
|
os.path.join(config_dir, "stateful"),
|
|
os.path.join(config_dir, "user"),
|
|
os.path.join(config_dir, "logs"),
|
|
os.path.join(config_dir, "scheduler"),
|
|
]:
|
|
try:
|
|
os.makedirs(dir_path, exist_ok=True)
|
|
with open(desktop_log, "a") as f:
|
|
f.write(f"Created directory: {dir_path}\n")
|
|
except Exception as e:
|
|
with open(desktop_log, "a") as f:
|
|
f.write(f"Error creating directory {dir_path}: {str(e)}\n")
|
|
|
|
# Configure logging
|
|
try:
|
|
logging.basicConfig(
|
|
level=logging.DEBUG,
|
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
handlers=[
|
|
logging.FileHandler(os.path.join(log_dir, "huntarr.log")),
|
|
logging.StreamHandler()
|
|
]
|
|
)
|
|
logger = logging.getLogger("Huntarr")
|
|
logger.setLevel(logging.DEBUG)
|
|
|
|
# Make sure log messages appear at the appropriate levels
|
|
for handler in logger.handlers:
|
|
handler.setLevel(logging.DEBUG)
|
|
|
|
with open(desktop_log, "a") as f:
|
|
f.write("Logging system initialized\n")
|
|
except Exception as e:
|
|
with open(desktop_log, "a") as f:
|
|
f.write(f"Error setting up logging: {str(e)}\n")
|
|
raise
|
|
|
|
# Create necessary default config files if they don't exist
|
|
try:
|
|
# Create default scheduler file
|
|
scheduler_dir = os.path.join(config_dir, "scheduler")
|
|
scheduler_file = os.path.join(scheduler_dir, "schedule.json")
|
|
if not os.path.exists(scheduler_file):
|
|
default_schedule = {
|
|
"global": [],
|
|
"sonarr": [],
|
|
"radarr": [],
|
|
"lidarr": [],
|
|
"readarr": []
|
|
}
|
|
with open(scheduler_file, "w") as f:
|
|
json.dump(default_schedule, f, indent=2)
|
|
logger.debug(f"Created default scheduler file at {scheduler_file}")
|
|
|
|
# Create default general.json with appropriate timeouts
|
|
general_file = os.path.join(config_dir, "settings", "general.json")
|
|
if not os.path.exists(general_file):
|
|
default_general = {
|
|
"api_timeout": 120,
|
|
"command_wait_delay": 1,
|
|
"command_wait_attempts": 600,
|
|
"log_level": "DEBUG"
|
|
}
|
|
with open(general_file, "w") as f:
|
|
json.dump(default_general, f, indent=2)
|
|
logger.debug(f"Created default general settings at {general_file}")
|
|
except Exception as e:
|
|
logger.exception(f"Error creating default config files: {str(e)}")
|
|
|
|
try:
|
|
# Set environment variables to mimic Docker container
|
|
os.environ["HUNTARR_CONFIG_DIR"] = config_dir
|
|
os.environ["FLASK_ENV"] = "production"
|
|
|
|
# Create a file to record the config location for other processes
|
|
config_location_file = os.path.join(os.path.dirname(desktop_log), "huntarr_config_location.txt")
|
|
with open(config_location_file, "w") as f:
|
|
f.write(config_dir)
|
|
|
|
# Make sure we have write permissions to the config directory
|
|
test_file_path = os.path.join(config_dir, "write_test.txt")
|
|
try:
|
|
with open(test_file_path, "w") as f:
|
|
f.write("Permission test")
|
|
os.remove(test_file_path)
|
|
logger.debug(f"Confirmed write permissions to {config_dir}")
|
|
except Exception as perm_error:
|
|
logger.error(f"No write permission to {config_dir}: {str(perm_error)}")
|
|
# Try to use a temporary directory if we can't write to our preferred locations
|
|
import tempfile
|
|
temp_config_dir = os.path.join(tempfile.gettempdir(), f"huntarr_{int(time.time())}")
|
|
os.makedirs(temp_config_dir, exist_ok=True)
|
|
config_dir = temp_config_dir
|
|
log_dir = os.path.join(config_dir, "logs")
|
|
os.makedirs(log_dir, exist_ok=True)
|
|
os.environ["HUNTARR_CONFIG_DIR"] = config_dir
|
|
logger.warning(f"Switched to temporary directory: {config_dir}")
|
|
with open(config_location_file, "w") as f:
|
|
f.write(config_dir)
|
|
|
|
# Log environment information for debugging
|
|
logger.debug(f"Python version: {sys.version}")
|
|
logger.debug(f"Python executable: {sys.executable}")
|
|
logger.debug(f"Current working directory: {os.getcwd()}")
|
|
logger.debug(f"HUNTARR_CONFIG_DIR = {os.environ.get('HUNTARR_CONFIG_DIR')}")
|
|
|
|
# List all environment variables for debugging
|
|
logger.debug("Environment variables:")
|
|
for key, value in sorted(os.environ.items()):
|
|
logger.debug(f" {key} = {value}")
|
|
|
|
# Check if running in PyInstaller bundle
|
|
if hasattr(sys, "_MEIPASS"):
|
|
logger.debug(f"Running in PyInstaller bundle: {sys._MEIPASS}")
|
|
bundle_dir = sys._MEIPASS
|
|
os.chdir(bundle_dir)
|
|
logger.debug(f"Changed working directory to: {os.getcwd()}")
|
|
|
|
# List bundle contents for debugging
|
|
logger.debug("Bundle contents:")
|
|
for root, dirs, files in os.walk(bundle_dir, topdown=True, followlinks=False):
|
|
rel_path = os.path.relpath(root, bundle_dir)
|
|
if rel_path == ".":
|
|
rel_path = ""
|
|
logger.debug(f" Directory: {rel_path}")
|
|
for file in files:
|
|
logger.debug(f" {os.path.join(rel_path, file)}")
|
|
|
|
# Import main module with debug info
|
|
logger.debug("Attempting to import main module...")
|
|
try:
|
|
sys.path.insert(0, os.getcwd())
|
|
import main
|
|
logger.debug("Main module imported successfully")
|
|
except ImportError as e:
|
|
logger.error(f"Failed to import main: {str(e)}")
|
|
raise
|
|
|
|
# Start the main application
|
|
logger.debug("Starting main application")
|
|
main.main()
|
|
except Exception as e:
|
|
logger.exception(f"Fatal error in app_launcher: {str(e)}")
|
|
with open(desktop_log, "a") as f:
|
|
f.write(f"\n[{datetime.now().isoformat()}] FATAL ERROR: {str(e)}\n")
|
|
f.write("\nTraceback:\n")
|
|
traceback.print_exc(file=f)
|
|
|
|
# Add more diagnostic information
|
|
f.write("\n\nSystem Information:\n")
|
|
f.write(f"Python version: {sys.version}\n")
|
|
f.write(f"Python path: {sys.path}\n")
|
|
f.write(f"Working directory: {os.getcwd()}\n")
|
|
|
|
# Try to list directory contents
|
|
try:
|
|
f.write("\nDirectory contents:\n")
|
|
for item in os.listdir(os.getcwd()):
|
|
f.write(f" {item}\n")
|
|
except Exception as dir_err:
|
|
f.write(f"Error listing directory: {str(dir_err)}\n")
|
|
EOF
|
|
|
|
- name: Create runtime hook
|
|
run: |
|
|
mkdir -p hooks
|
|
cat > hooks/runtime_hook.py << 'EOF'
|
|
import os
|
|
import sys
|
|
|
|
# Set up app environment variables
|
|
os.environ["FLASK_ENV"] = "production"
|
|
|
|
# Setup config directory path in user's home
|
|
home = os.path.expanduser("~")
|
|
config_dir = os.path.join(home, "Library", "Application Support", "Huntarr", "config")
|
|
os.environ["HUNTARR_CONFIG_DIR"] = config_dir
|
|
|
|
# Add bundle resources directory to path
|
|
if ".app" in sys.executable:
|
|
bundle_dir = os.path.abspath(os.path.dirname(sys.executable))
|
|
resources_dir = os.path.abspath(os.path.join(bundle_dir, "..", "Resources"))
|
|
if resources_dir not in sys.path:
|
|
sys.path.insert(0, resources_dir)
|
|
EOF
|
|
|
|
- name: Create icon
|
|
run: |
|
|
mkdir -p icon.iconset
|
|
sips -s format png frontend/static/logo/huntarr.ico --out icon.iconset/icon_16x16.png --resampleWidth 16 || true
|
|
sips -s format png frontend/static/logo/huntarr.ico --out icon.iconset/icon_32x32.png --resampleWidth 32 || true
|
|
sips -s format png frontend/static/logo/huntarr.ico --out icon.iconset/icon_64x64.png --resampleWidth 64 || true
|
|
sips -s format png frontend/static/logo/huntarr.ico --out icon.iconset/icon_128x128.png --resampleWidth 128 || true
|
|
sips -s format png frontend/static/logo/huntarr.ico --out icon.iconset/icon_256x256.png --resampleWidth 256 || true
|
|
sips -s format png frontend/static/logo/huntarr.ico --out icon.iconset/icon_512x512.png --resampleWidth 512 || true
|
|
|
|
cp icon.iconset/icon_32x32.png icon.iconset/icon_16x16@2x.png || true
|
|
cp icon.iconset/icon_64x64.png icon.iconset/icon_32x32@2x.png || true
|
|
cp icon.iconset/icon_128x128.png icon.iconset/icon_64x64@2x.png || true
|
|
cp icon.iconset/icon_256x256.png icon.iconset/icon_128x128@2x.png || true
|
|
cp icon.iconset/icon_512x512.png icon.iconset/icon_256x256@2x.png || true
|
|
|
|
iconutil -c icns icon.iconset -o frontend/static/logo/huntarr.icns || true
|
|
|
|
# If icon conversion fails, create a fallback icon
|
|
if [ ! -f frontend/static/logo/huntarr.icns ]; then
|
|
cp /System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericApplicationIcon.icns frontend/static/logo/huntarr.icns || true
|
|
fi
|
|
|
|
- name: Create PyInstaller spec
|
|
run: |
|
|
cat > Huntarr.spec << 'EOF'
|
|
# -*- mode: python ; coding: utf-8 -*-
|
|
from PyInstaller.building.api import PYZ, EXE, COLLECT
|
|
from PyInstaller.building.build_main import Analysis
|
|
from PyInstaller.building.datastruct import Tree
|
|
import os
|
|
import sys
|
|
|
|
block_cipher = None
|
|
|
|
a = Analysis(
|
|
['app_launcher.py'],
|
|
pathex=['.'],
|
|
binaries=[],
|
|
datas=[
|
|
('frontend', 'frontend'),
|
|
('version.txt', '.'),
|
|
('README.md', '.'),
|
|
('LICENSE', '.'),
|
|
('src', 'src'),
|
|
],
|
|
hiddenimports=[
|
|
'flask',
|
|
'flask.json',
|
|
'requests',
|
|
'waitress',
|
|
'bcrypt',
|
|
'qrcode',
|
|
'PIL',
|
|
'pyotp',
|
|
'qrcode.image.pil',
|
|
'routes',
|
|
'main',
|
|
# Apprise notification support
|
|
'apprise',
|
|
'apprise.common',
|
|
'apprise.conversion',
|
|
'apprise.decorators',
|
|
'apprise.locale',
|
|
'apprise.logger',
|
|
'apprise.manager',
|
|
'apprise.utils',
|
|
'apprise.URLBase',
|
|
'apprise.AppriseAsset',
|
|
'apprise.AppriseAttachment',
|
|
'apprise.AppriseConfig',
|
|
'apprise.cli',
|
|
'apprise.config',
|
|
'apprise.attachment',
|
|
'apprise.plugins',
|
|
'markdown',
|
|
'yaml',
|
|
'cryptography',
|
|
],
|
|
hookspath=['hooks'],
|
|
hooksconfig={},
|
|
runtime_hooks=['hooks/runtime_hook.py'],
|
|
excludes=[],
|
|
win_no_prefer_redirects=False,
|
|
win_private_assemblies=False,
|
|
cipher=block_cipher,
|
|
noarchive=False,
|
|
)
|
|
|
|
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
|
|
|
exe = EXE(
|
|
pyz,
|
|
a.scripts,
|
|
[],
|
|
exclude_binaries=True,
|
|
name='Huntarr',
|
|
debug=False,
|
|
bootloader_ignore_signals=False,
|
|
strip=False,
|
|
upx=True,
|
|
console=True,
|
|
disable_windowed_traceback=False,
|
|
argv_emulation=True,
|
|
target_arch=None,
|
|
codesign_identity=None,
|
|
entitlements_file=None,
|
|
icon='frontend/static/logo/huntarr.icns',
|
|
)
|
|
|
|
coll = COLLECT(
|
|
exe,
|
|
a.binaries,
|
|
a.zipfiles,
|
|
a.datas,
|
|
strip=False,
|
|
upx=True,
|
|
upx_exclude=[],
|
|
name='Huntarr',
|
|
)
|
|
|
|
app = BUNDLE(
|
|
coll,
|
|
name='Huntarr.app',
|
|
icon='frontend/static/logo/huntarr.icns',
|
|
bundle_identifier='io.huntarr.app',
|
|
info_plist={
|
|
'CFBundleShortVersionString': '${{ steps.meta.outputs.VERSION }}',
|
|
'CFBundleVersion': '${{ steps.meta.outputs.VERSION }}',
|
|
'NSHighResolutionCapable': True,
|
|
'NSRequiresAquaSystemAppearance': False,
|
|
'LSEnvironment': {
|
|
'HUNTARR_CONFIG_DIR': '~/Library/Application Support/Huntarr/config',
|
|
'PYTHONPATH': '@executable_path/../Resources',
|
|
},
|
|
'CFBundleDocumentTypes': [],
|
|
'NSPrincipalClass': 'NSApplication',
|
|
},
|
|
)
|
|
EOF
|
|
|
|
- name: Build macOS app bundle
|
|
run: python -m PyInstaller Huntarr.spec --clean --collect-all apprise
|
|
|
|
- name: Create PKG installer
|
|
run: |
|
|
# Create a simple postinstall script
|
|
mkdir -p scripts
|
|
cat > scripts/postinstall << 'EOF'
|
|
#!/bin/bash
|
|
|
|
# Create config directory in user's Application Support
|
|
mkdir -p "$HOME/Library/Application Support/Huntarr/config"
|
|
# Logs are now stored in database only
|
|
|
|
# Set permissions
|
|
chmod -R 755 "$HOME/Library/Application Support/Huntarr"
|
|
|
|
exit 0
|
|
EOF
|
|
|
|
chmod +x scripts/postinstall
|
|
|
|
# Create PKG installer
|
|
version="${{ steps.meta.outputs.VERSION }}"
|
|
branch="${{ steps.meta.outputs.BRANCH }}"
|
|
|
|
if [[ "${{ steps.meta.outputs.IS_TAG }}" == "true" ]]; then
|
|
pkg_name="Huntarr-${version}-mac-intel.pkg"
|
|
else
|
|
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
|
|
pkg_name="Huntarr-${version}-mac-main-intel.pkg"
|
|
elif [[ "${{ github.ref }}" == "refs/heads/dev" ]]; then
|
|
pkg_name="Huntarr-${version}-mac-dev-intel.pkg"
|
|
else
|
|
# Sanitize branch name by replacing slashes with hyphens
|
|
branch_safe=$(echo "${branch}" | tr '/' '-')
|
|
pkg_name="Huntarr-${version}-mac-${branch_safe}-intel.pkg"
|
|
fi
|
|
fi
|
|
|
|
pkgbuild --root dist/ \
|
|
--scripts scripts/ \
|
|
--identifier io.huntarr.app \
|
|
--version ${version} \
|
|
--install-location /Applications \
|
|
${pkg_name}
|
|
|
|
- name: Upload installer as artifact
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: huntarr-macos-intel-installer
|
|
path: '*.pkg'
|
|
retention-days: 30
|
|
|
|
- name: Upload to release
|
|
if: steps.meta.outputs.IS_TAG == 'true'
|
|
uses: softprops/action-gh-release@v1
|
|
with:
|
|
files: '*.pkg'
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|