Compare commits

...

14 Commits

Author SHA1 Message Date
d1767e7409 Added docs to install dolibarr and minor updates to other setups 2026-01-02 00:16:41 -05:00
3e808f0a50 Updated verbosity to multiple levels in venv tools. 2025-08-09 10:26:35 -04:00
00696fc449 feat(scripts): add reusable venv utilities and file case renaming script
- Added `scripts/library/venv_utils.py`:
  - Utilities for creating, activating, and managing Python virtual environments.
  - Supports automatic requirement installation and optional --verbose mode.
- Added `scripts/change_case.py`:
  - CLI script to recursively rename files and directories based on case transformation (lower, upper, capitalize).
  - Supports --recursive and --case flags, and integrates with `venv_utils.py`.
- Updated README with usage instructions and documentation for new components.
2025-07-12 13:58:34 -04:00
e966f4625a Added changes to README to reflect latest changes in scripts. 2025-02-08 15:13:02 -05:00
8ff995a29f feat: Add shorthand parameters to video_manage_audio.py and video_manage_subtitles.py
- Added shorthand options for common arguments:
  - `-t` for --track
  - `-f` for --file
  - `-d` for --dir
- Simplified command-line usage for managing audio and subtitle tracks in video files.
2025-02-08 13:55:14 -05:00
2152479d2e feat: Add video_manage_subtitles.py to manage subtitle tracks in video files
- Implement functionality to remove, keep, or remove all subtitle tracks.
- Supports processing single video files or entire directories.
- Includes options for managing subtitle tracks by index.
2025-02-08 13:50:38 -05:00
53f483b574 feat: Enhanced functionalities of video_manage_audio.py to support removing and keeping tracks.
- Refactored audio processing logic to handle both 'remove' and 'keep' commands.
- Added a command-line argument to specify whether to remove a specific audio track or keep only a selected audio track while preserving other streams.
- Improved handling of video file processing in directories with recursive function.
- Streamlined error handling for more informative output during ffmpeg execution.
- Updated argument parsing with command options and enhanced autocomplete support.
2025-02-08 13:37:41 -05:00
4c76cbfe76 Renamed video_remove_audio.py to video_manage_audio.py to reflect incomming changes. 2025-02-08 13:16:33 -05:00
4ef0772105 Added argcomplete to video_autoreduce scripts. 2025-02-08 13:07:02 -05:00
98f9a57ea1 feat: add video_autoreduce_rename.py for automated renaming
- Adds `video_autoreduce_rename.py`, a companion script to rename video files and directories containing resolution tags.
- Automatically replaces higher resolution tags (e.g., 2160p, 1080p) with a specified max height (default: 720p).
- Supports debug mode for detailed output.
- Prevents overwriting existing files by skipping conflicts.
- Complements `video_autoreduce.py` for a streamlined video processing workflow.
2025-02-08 13:01:15 -05:00
218951f035 Merge branch 'main' gitea.fabq.ca 2025-02-08 12:55:54 -05:00
c062b32dc6 feat: add video_autoreduce.py for automatic video resolution reduction
Added `video_autoreduce.py`, a Python script that scans directories for video files exceeding a specified resolution and prepares them for automatic resolution reduction using FFmpeg.

- Detects video files in a given directory.
- Determines their resolution using `ffprobe`.
- Identifies videos exceeding a maximum height threshold.
- Supports subtitle codec validation and ensures proper output handling.
- Includes utility functions for file management and logging.

This script will help streamline batch video processing while preserving essential metadata and subtitles.
2025-02-08 12:55:09 -05:00
45fb6b35b3 Updated README with information about latest notes. 2025-02-03 10:25:28 -05:00
9246fde44a Added some notes on debugging WordPress sites. 2025-02-03 10:22:54 -05:00
14 changed files with 2013 additions and 130 deletions

View File

@ -8,7 +8,13 @@ This repository is structured into several key directories:
- **scripts/**: Contains individual scripts for various tasks. Currently, it includes: - **scripts/**: Contains individual scripts for various tasks. Currently, it includes:
- `video_remove_audio.py`: A script for removing audio from video files. - `scripts/library/`: Libraries used by Python scripts.
- `venv_utils.py`: Utility functions for creating, activating, and managing Python virtual environments.
- `change_case.py`: A script for renaming files and directories by changing their case.
- `video_manage_audio.py`: A script for removing audio from video files.
- `video_manage_subtitles.py`: A script for removing subtitles from video files.
- `video_autoreduce.py`: A script for automatic resolution reduction of video files.
- `video_autoreduce_rename.py`: A script for automated renaming of video files post resolution reduction.
- **notes/**: A collection of markdown files containing notes on various topics, including: - **notes/**: A collection of markdown files containing notes on various topics, including:
@ -22,6 +28,7 @@ This repository is structured into several key directories:
- `pdftk.md`: PDF Toolkit usage. - `pdftk.md`: PDF Toolkit usage.
- `pip packaging.md`: Packaging Python projects with pip. - `pip packaging.md`: Packaging Python projects with pip.
- `ssh.md`: Secure Shell (SSH) configuration and tips. - `ssh.md`: Secure Shell (SSH) configuration and tips.
- `wordpress.md`: WordPress debugging and tips.
- **pages/other/**: Templates for other pages, such as the homepage of my Debian package repository. These are provided as inspiration and should not be used as-is. - **pages/other/**: Templates for other pages, such as the homepage of my Debian package repository. These are provided as inspiration and should not be used as-is.

163
notes/wordpress.md Normal file
View File

@ -0,0 +1,163 @@
# WordPress
## Table of Contents
- [WordPress](#wordpress)
- [Table of Contents](#table-of-contents)
- [Documentation](#documentation)
- [Common WordPress Errors](#common-wordpress-errors)
- [Debugging Steps](#debugging-steps)
- [Debugging Tools](#debugging-tools)
## Documentation
- [wp-cli](https://developer.wordpress.org/cli/commands/)
- [Documentation Overview](https://www.wordpress.info/doc/overview/)
- [Tutorials](https://wordpress.com/learn/)
## Common WordPress Errors
1. **HTTP Errors**
- **404 Not Found**: Page or resource missing.
- **403 Forbidden**: Insufficient permissions to access the resource.
- **500 Internal Server Error**: Generic error, often caused by server misconfiguration or PHP errors.
- **503 Service Unavailable**: Server is overloaded or in maintenance mode.
2. **Database Errors**
- **Error Establishing a Database Connection**: Database credentials incorrect or database server is down.
- **Table Prefix Issues**: Wrong `$table_prefix` in `wp-config.php`.
- **Corrupt Database**: Can be fixed using `wp db repair`.
3. **PHP Errors**
- **Parse Error**: Syntax error in PHP files.
- **Fatal Error**: Missing function or class.
- **Deprecated Function Warnings**: Old functions being used.
## Debugging Steps
1. **Enable Debug Mode**
Edit `wp-config.php`:
```php
// Enable Debug Mode
define('WP_DEBUG', true);
// Enable Debug logging to the /wp-content/debug.log file
define('WP_DEBUG_LOG', true);
// Disable display of WordPress errors and warnings
define('WP_DEBUG_DISPLAY', false);
// Disable display of PHP errors and warnings
@ini_set('display_errors', 0);
```
Logs will be saved in `wp-content/debug.log`.
2. **Check `.htaccess`**
Ensure WordPress rules exist:
```apache
# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress
```
3. **Check Apache and VirtualHost config**
**Redhat and derivatives**
- `/etc/httpd/conf/httpd.conf`
- `/etc/httpd/conf.d/welcome.conf`
**Debian and derivatives**
- `/etc/apache2/apache2.conf`
- `/etc/apache2/sites-available/000-default.conf`
```apache
<Directory "/var/www">
AllowOverride All
# Allow open access:
Require all granted
</Directory>
<Directory "/var/www/html">
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
```
4. **Reset File Permissions**
```bash
find /var/www/html -type d -exec chmod 755 {} \;
find /var/www/html -type f -exec chmod 644 {} \;
```
4. **Disable Plugins & Themes**
Hide the plugin directory:
```bash
mv wp-content/plugins wp-content/.plugins
```
Switch to a default theme:
```bash
wp theme install twentytwentyfive --activate
wp theme activate twentytwentyfive
```
5. **Check Server Logs**
For Apache:
```bash
tail -f /var/log/httpd/error_log
```
For Nginx:
```bash
tail -f /var/log/nginx/error.log
```
6. **Verify Database Connection**
```bash
wp db check
```
If corrupt:
```bash
wp db repair
```
7. **Increase Memory Limit**
Edit `wp-config.php`:
```php
define('WP_MEMORY_LIMIT', '256M');
```
## Debugging Tools
- **WP-CLI**: Command-line interface for WordPress.
- **Query Monitor**: Plugin for analyzing database queries and errors.
- **Health Check & Troubleshooting**: Plugin for diagnosing issues.

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
argcomplete

125
scripts/change_case.py Executable file
View File

@ -0,0 +1,125 @@
#!/usr/bin/env python3
# === Standard library ===
import sys
import os
from pathlib import Path
from typing import Optional
from argparse import ArgumentParser, Namespace
# Allow importing from scripts/library even when run directly
project_root = str(Path(__file__).resolve().parent.parent)
if project_root not in sys.path:
sys.path.append(project_root)
# === Local import ===
from scripts.library import parse_verbose, run_in_venv
def rename_by_case(root_dir: str, case: str, recursive: bool = False) -> None:
"""
Renames all files and directories in a given directory by changing their case.
Args:
root_dir (str): Root path to start renaming.
case (str): One of "lower", "upper", "capitalize".
recursive (bool): Whether to apply renaming recursively.
Returns:
None
"""
def apply_case(name: str) -> str:
if case == "lower":
return name.lower()
elif case == "upper":
return name.upper()
elif case == "capitalize":
return name.capitalize()
else:
raise ValueError(f"Unsupported case transformation: {case}")
for current_dir, dirs, files in os.walk(root_dir, topdown=False):
# Skip descending into subdirectories if not recursive
if not recursive and current_dir != root_dir:
continue
# Rename files
for name in files:
old_path = os.path.join(current_dir, name)
new_name = apply_case(name)
new_path = os.path.join(current_dir, new_name)
if old_path != new_path:
if not os.path.exists(new_path):
os.rename(old_path, new_path)
else:
print(
f"Cannot rename {old_path} to {new_path}: target already exists."
)
# Rename directories
for name in dirs:
old_path = os.path.join(current_dir, name)
new_name = apply_case(name)
new_path = os.path.join(current_dir, new_name)
if old_path != new_path:
if not os.path.exists(new_path):
os.rename(old_path, new_path)
else:
print(
f"Cannot rename {old_path} to {new_path}: target already exists."
)
def parse_args() -> Namespace:
"""
Parse and validate command-line arguments.
Returns:
Namespace: Parsed arguments with 'case' and 'recursive' options.
"""
import argcomplete
parser = ArgumentParser(
description="Rename files and directories by changing case (lower, upper, capitalize)."
)
parser.add_argument(
"--case",
"-c",
type=str,
choices=["lower", "upper", "capitalize"],
default="lower",
help="Case transformation to apply: 'lower', 'upper', or 'capitalize'. Default is 'lower'.",
)
parser.add_argument(
"--recursive",
"-r",
action="store_true",
help="Apply the transformation recursively in all subdirectories.",
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="Enable verbose output (debug information, warnings)",
)
argcomplete.autocomplete(parser)
return parser.parse_args()
def main() -> None:
"""
Main entry point. Parses arguments and runs case renaming in current working directory.
"""
args = parse_args()
cwd: str = os.getcwd()
rename_by_case(cwd, case=args.case, recursive=args.recursive)
if __name__ == "__main__":
args = parse_verbose()
run_in_venv(main, verbose=args.verbose)

View File

@ -0,0 +1,5 @@
# scripts/library/__init__.py
from .venv_utils import parse_verbose, run_in_venv
__all__ = ["parse_verbose", "run_in_venv"]

View File

@ -0,0 +1,149 @@
#!/usr/bin/env python3
# === Standard library ===
import os
import sys
import subprocess
import venv
from pathlib import Path
from typing import Callable
from argparse import ArgumentParser, Namespace
# === Configuration ===
# Define the virtual environment directory in the user's home folder
VENV_DIR: Path = Path.home() / ".scripts_fabq_venv"
# Define the path to the requirements.txt file in the project root
REQUIREMENTS_FILE: Path = Path(__file__).resolve().parents[2] / "requirements.txt"
def parse_verbose() -> Namespace:
"""
Parse the --verbose flag before virtual environment setup.
This is useful when argument parsing requires packages that may
only be available after the virtual environment is initialized.
Returns:
Namespace: Contains a single attribute 'verbose' (bool).
"""
parser = ArgumentParser(add_help=False)
parser.add_argument(
"-v",
"--verbose",
action="count",
default=0,
help="Increase verbosity: -v = verbose, -vv = debug, -vvv = trace",
)
parser.add_argument(
"--debug",
dest="verbose",
action="store_const",
const=2,
help="Enable debug output (equivalent to -vv)",
)
parser.add_argument(
"--trace",
dest="verbose",
action="store_const",
const=3,
help="Enable full trace output (equivalent to -vvv)",
)
return parser.parse_known_args()[0]
def is_in_venv() -> bool:
"""
Determine whether the script is currently running inside a virtual environment.
Returns:
bool: True if running inside a virtual environment, False otherwise.
"""
return hasattr(sys, "real_prefix") or sys.prefix != getattr(
sys, "base_prefix", sys.prefix
)
def create_venv(verbose: bool = False) -> None:
"""
Create a virtual environment in the predefined VENV_DIR location.
Args:
verbose (bool): If True, prints progress messages.
"""
if verbose >= 2:
print(f"Creating virtual environment in {VENV_DIR}...")
venv.create(VENV_DIR, with_pip=True)
def install_requirements(verbose: bool = False) -> None:
"""
Install Python dependencies from requirements.txt into the venv.
Args:
verbose (bool): If True, prints progress and pip output.
"""
if not REQUIREMENTS_FILE.exists():
print(f"Error: requirements.txt not found at {REQUIREMENTS_FILE}")
sys.exit(1)
if verbose >= 2:
print(f"Installing/updating requirements from {REQUIREMENTS_FILE}...")
pip_cmd = [str(VENV_DIR / "bin" / "pip")]
try:
subprocess.check_call(
pip_cmd + ["install", "-r", str(REQUIREMENTS_FILE)],
stdout=None if verbose >= 2 else subprocess.DEVNULL,
stderr=None if verbose >= 2 else subprocess.DEVNULL,
)
except subprocess.CalledProcessError as e:
print(f"Error: Failed to install requirements: {e}")
sys.exit(1)
try:
subprocess.check_call(
pip_cmd + ["check"],
stdout=None if verbose >= 2 else subprocess.DEVNULL,
stderr=None if verbose >= 2 else subprocess.DEVNULL,
)
except subprocess.CalledProcessError:
print("Error: Dependency conflict detected — your environment may be broken.")
raise
def run_in_venv(main_func: Callable[[], None], verbose: bool = False) -> None:
"""
Ensure the script is running inside the expected virtual environment.
If not already in the venv:
- Create it if needed
- Install requirements
- Relaunch the script inside the venv
If already in the venv:
- Ensure requirements are installed
- Execute the given main function
Args:
main_func (Callable[[], None]): The function to execute after setup.
verbose (bool): If True, enables progress messages and pip output.
"""
if not is_in_venv():
if not VENV_DIR.exists():
create_venv(verbose)
install_requirements(verbose)
# Relaunch the script inside the virtual environment
os.execv(
str(VENV_DIR / "bin" / "python"),
[str(VENV_DIR / "bin" / "python")] + sys.argv,
)
else:
install_requirements(verbose)
main_func()

493
scripts/video_autoreduce.py Executable file
View File

@ -0,0 +1,493 @@
#!/usr/bin/env python3
# video_autoreduce.py
import argparse
import argcomplete
import colorama
import os
import re
import subprocess
import sys
import json
colorama.init()
creset = colorama.Fore.RESET
ccyan = colorama.Fore.CYAN
cyellow = colorama.Fore.YELLOW
cgreen = colorama.Fore.GREEN
cred = colorama.Fore.RED
def get_ffmpeg_codecs(codec_type="S"):
"""
Retrieve a list of supported codecs for a specific type from ffmpeg.
This function runs the `ffmpeg -codecs` command and parses its output to extract
the list of supported codecs based on the provided `codec_type` (e.g., 'S' for subtitles,
'V' for video, 'A' for audio). The list of codecs is returned as a set for efficient lookup.
Args:
codec_type (str): The type of codecs to retrieve.
- "S" for subtitles (default)
- "V" for video
- "A" for audio
- "D" for data
- "T" for attachment
Returns:
set: A set of codec names of the specified type.
If an error occurs or no codecs are found, an empty set is returned.
"""
try:
# Run ffmpeg -codecs
result = subprocess.run(["ffmpeg", "-codecs"],
capture_output=True,
text=True,
check=True)
output = result.stdout
# Extract codecs using regex
codec_pattern = re.compile(rf"^[ D.E]+{codec_type}[ A-Z.]+ (\S+)",
re.MULTILINE)
codecs = codec_pattern.findall(output)
return set(codecs) # Return as a set for easy lookup
except subprocess.CalledProcessError as e:
print("Error running ffmpeg:", e)
return set()
def findfreename(filepath, attempt=0):
'''
Given a filepath, it will try to find a free filename by appending a number to the name.
First, it tries using the filepath passed in the argument, then adds a number to the end. If all fail, it appends (#).
Args:
filepath (str): A string containing the full filepath.
attempt (int): The number of attempts already made to find a free filename.
Returns:
str: The first free filepath found.
'''
attempt += 1
filename = str(filepath)[:str(filepath).rindex(".")]
extension = str(filepath)[str(filepath).rindex("."):]
copynumpath = filename + f"({attempt})" + extension
if not os.path.exists(filepath) and attempt <= 2:
return filepath
elif not os.path.exists(copynumpath):
return copynumpath
return findfreename(filepath, attempt)
def deletefile(filepath):
'''Delete a file, Returns a boolean
Args:
filepath : A string containing the full filepath
Returns:
Bool : The success of the operation
'''
try:
os.remove(filepath)
except OSError:
print(f"{cred}Error deleting {filepath}{creset}")
return False
print(f"{cgreen}Successfully deleted {filepath}{creset}")
return True
def ensure_output_path(output_path):
'''
Ensure that the output directory exists. If it doesn't, attempt to create it.
Args:
output_path (str): The path of the output directory to be ensured.
Returns:
None
'''
try:
os.makedirs(output_path, exist_ok=True)
# Attempts to create the output directory if it doesn't exist.
# exist_ok=True ensures that an OSError is not raised if the directory already exists.
except OSError as e:
# If an OSError occurs during directory creation, print an error message and exit the program.
print(f"{cred}Failed to create output directory:{creset} {e}",
file=sys.stderr)
sys.exit(1)
def has_supported_subs(video_file, supported_subtitle_codecs, debug=False):
"""
Check if the given video file contains subtitles in a supported codec.
This function uses `ffprobe` to extract subtitle stream information from the
video file and checks whether any subtitle stream has a codec that matches
one of the supported codecs provided in the `supported_subtitle_codecs` list.
Args:
video_file (str): Path to the video file to be checked.
supported_subtitle_codecs (set): Set of subtitle codec names that are considered supported.
debug (bool): If True, prints debug information for errors. Defaults to False.
Returns:
bool: True if at least one subtitle stream is found with a supported codec,
False otherwise or in case of an error.
"""
try:
result = subprocess.run([
'ffprobe', '-v', 'error', '-select_streams', 's', '-show_entries',
'stream=index,codec_name', '-of', 'json', video_file
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True)
if result.returncode != 0:
if debug:
print(f"ffprobe error: {result.stderr.strip()}")
return False # Assume no supported subtitles on failure
try:
streams = json.loads(result.stdout).get("streams", [])
except json.JSONDecodeError as e:
if debug:
print(f"JSON parsing error: {e}")
return False # Assume no supported subtitles if JSON is malformed
return any(
stream.get("codec_name") in supported_subtitle_codecs
for stream in streams)
except Exception as e:
if debug:
print(f"Unexpected error: {e}")
return False # Assume no supported subtitles
def find_videos_to_convert(input_path, max_height=720, debug=False):
'''
Find video files in the specified directory tree that meet conversion criteria.
Args:
input_path (str): The path of the directory to search for video files.
max_height (int, optional): The maximum height (in pixels) of the video to consider for conversion. Default is 720.
debug (bool, optional): If True, print debug messages. Default is False.
Returns:
list: A list of paths to video files that meet the conversion criteria.
'''
# Print a message indicating the start of the scanning process
print(
f"{cgreen}Scanning {input_path} for video files to convert.{creset}\n")
# Initialize lists to store all video files and valid video files
video_files = []
valid_videos_files = []
# Find all video files in the directory tree
for root, dirs, files in os.walk(input_path):
for file in files:
fullpath = os.path.join(root, file)
# Check if the file is a video file with one of the specified extensions
if (file.lower().endswith(
('.mp4', '.avi', '.mkv', '.mov', '.wmv', '.flv', '.divx'))
and os.path.isfile(fullpath)):
video_files.append(fullpath)
# List of encodings to try for decoding output from ffprobe
ENCODINGS_TO_TRY = ['utf-8', 'latin-1', 'iso-8859-1']
# Iterate through each video file
for video_file in video_files:
try:
# Run ffprobe command to get video resolution
resolution_output = subprocess.check_output(
[
'ffprobe', '-v', 'error', '-select_streams', 'v:0',
'-show_entries', 'stream=width,height', '-of', 'csv=p=0',
video_file
],
stderr=subprocess.STDOUT)
# Attempt to decode the output using different encodings
dimensions_str = None
for encoding in ENCODINGS_TO_TRY:
try:
dimensions_str = resolution_output.decode(
encoding).strip().rstrip(',')
dimensions = [
int(d) for d in dimensions_str.split(',')
if d.strip().isdigit()
]
if len(dimensions) >= 2:
width, height = dimensions[:2]
else:
raise ValueError(
f"Unexpected dimensions format: {dimensions_str}")
break # Stop trying encodings once successful decoding and conversion
except (UnicodeDecodeError, ValueError):
continue # Try the next encoding
# If decoding was unsuccessful with all encodings, skip this video
if dimensions_str is None:
if debug:
print(
f"{cred}Error processing {video_file}: Unable to decode output from ffprobe using any of the specified encodings.{creset}",
file=sys.stderr)
continue # Skip this video and continue with the next one
# Ignore vertical videos
if height > width:
continue # Skip this video and continue with the next one
# Check if the video height exceeds the maximum allowed height
if height > max_height:
valid_videos_files.append(video_file)
except subprocess.CalledProcessError as e:
if debug:
# Print error message if subprocess call fails
if e.output is not None:
decoded_error = e.output.decode("utf-8")
print(
f"{cred}Error processing {video_file}: {decoded_error}{creset}\n",
file=sys.stderr)
else:
print(
f"{cred}Error processing {video_file}: No output from subprocess{creset}\n",
file=sys.stderr)
except ValueError as e:
if debug:
# Print error message if value error occurs during decoding
print(
f"{cred}Error processing {video_file}: ValueError decoding the file{creset}\n",
file=sys.stderr)
valid_videos_files.sort()
return valid_videos_files
def convert_videos(input_path, output_path, max_height=720, debug=False):
'''
Convert videos taller than a specified height to 720p resolution with x265 encoding and MKV container.
Args:
input_path (str): The path of the directory containing input video files.
output_path (str): The path of the directory where converted video files will be saved.
max_height (int, optional): The maximum height (in pixels) of the video to consider for conversion. Default is 720.
debug (bool, optional): If True, print debug messages. Default is False.
Returns:
bool: True if conversion succeeds, False otherwise.
'''
# Get supported subtitle codecs
supported_subtitle_codecs = get_ffmpeg_codecs("S")
# Find video files to convert in the input directory
video_files = find_videos_to_convert(input_path, max_height, debug)
counter = 0
# If no video files found, print a message and return False
if len(video_files) == 0:
print(f"No video files to convert found.\n")
return False
# Ensure the output directory exists
ensure_output_path(output_path)
# Print a message indicating the start of the conversion process
print(
f"Converting {len(video_files)} videos taller than {max_height} pixels to 720p resolution with x265 encoding and MKV container...\n"
)
# Variable to keep a track of the current_file in case of failure
current_file = None
# Iterate through each video file for conversion
for video_file in video_files:
counter += 1
# Print conversion progress information
print(
f"{cgreen}****** Starting conversion {counter} of {len(video_files)}: '{os.path.basename(video_file)}'...{creset}"
)
print(f"{ccyan}Original file:{creset}")
print(video_file)
# Generate output file path and name
output_file = findfreename(
os.path.join(
output_path,
os.path.splitext(os.path.basename(video_file))[0] + '.mkv'))
try:
# Get original video width and height
video_info = subprocess.run([
'ffprobe', '-v', 'error', '-select_streams', 'v:0',
'-show_entries', 'stream=width,height', '-of', 'csv=p=0',
video_file
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True)
width, height = map(
int,
video_info.stdout.strip().rstrip(',').split(','))
# Calculate the scaled width maintaining aspect ratio
scaled_width = int(max_height * (width / height))
# Ensure the width is even
if scaled_width % 2 != 0:
scaled_width += 1
# Keep a track of the current_file in case of failure
current_file = output_file
# Check if the video has supported subtitles
include_subs = has_supported_subs(video_file,
supported_subtitle_codecs, debug)
# Construct the FFmpeg command dynamically
ffmpeg_command = [
'ffmpeg', '-n', '-i', video_file, '-map', '0:v', '-c:v',
'libx265', '-vf', f'scale={scaled_width}:{max_height}',
'-preset', 'medium', '-crf', '28', '-map', '0:a?', '-c:a',
'copy'
]
# Only include subtitles if they are supported
if include_subs:
ffmpeg_command.extend(['-map', '0:s?', '-c:s', 'copy'])
ffmpeg_command.append(output_file)
# Print the FFmpeg command as a single string
if (debug):
print("FFmpeg command: " + ' '.join(ffmpeg_command))
# Run the FFmpeg command
process = subprocess.run(ffmpeg_command,
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT)
# Check if ffmpeg process returned successfully
if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode,
process.args,
output=process.stderr)
except subprocess.CalledProcessError as e:
# If we have a partial video converted, delete it due to the failure
if current_file:
deletefile(current_file)
# Handle subprocess errors
if debug:
if e.output is not None:
decoded_error = e.output.decode("utf-8")
print(
f"{cred}Error processing {video_file}: {decoded_error}{creset}\n",
file=sys.stderr)
else:
print(
f"{cred}Error processing {video_file}: No output from subprocess{creset}\n",
file=sys.stderr)
except KeyboardInterrupt:
print(f"{cyellow}Conversions cancelled, cleaning up...{creset}")
# If we have a partial video converted, delete it due to the failure
if current_file:
deletefile(current_file)
current_file = None
exit()
except Exception as e:
# If we have a partial video converted, delete it due to the failure
if current_file:
deletefile(current_file)
# Handle other exceptions
if debug:
print(
f"{cred}Error processing {video_file}: {str(e)}{creset}\n",
file=sys.stderr)
else:
# Print success message if conversion is successful
print(
f"{ccyan}Successfully converted video to output file:{creset}")
print(f"{output_file}\n")
try:
os.chmod(output_file, 0o777)
except PermissionError:
print(f"{cred}PermissionError on: '{output_file}'{creset}")
# Remove the now succefully converted filepath
current_file = None
return True
def main():
'''
Main function to parse command line arguments and initiate video conversion.
'''
# Create argument parser
parser = argparse.ArgumentParser(
description=
'Convert videos taller than a certain height to 720p resolution with x265 encoding and MKV container.'
)
# Define command line arguments
parser.add_argument(
'input_path',
nargs='?',
default=os.getcwd(),
help=
'directory path to search for video files (default: current directory)'
)
parser.add_argument(
'-o',
'--output-path',
default=os.path.join(os.getcwd(), 'converted'),
help='directory path to save converted videos (default: ./converted)')
parser.add_argument(
'-mh',
'--max-height',
type=int,
default=720,
help='maximum height of videos to be converted (default: 720)')
parser.add_argument(
'--debug',
action='store_true',
help='enable debug mode for printing additional error messages')
# Enable autocomplete for argparse
argcomplete.autocomplete(parser)
# Parse command line arguments
args = parser.parse_args()
# Convert videos
convert_videos(args.input_path, args.output_path, args.max_height,
args.debug)
if __name__ == "__main__":
# Execute main function when the script is run directly
main()

View File

@ -0,0 +1,133 @@
#!/usr/bin/env python3
# video_autoreduce_rename.py
import argparse
import argcomplete
import colorama
import os
import re
colorama.init()
creset = colorama.Fore.RESET
ccyan = colorama.Fore.CYAN
cyellow = colorama.Fore.YELLOW
cgreen = colorama.Fore.GREEN
cred = colorama.Fore.RED
def autorename(input_path, max_height=720, debug=False):
'''
Rename video files and folders in the specified directory tree that contain a resolution
in their name to the specified max_height resolution.
Args:
input_path (str): The path of the directory to search for video files and folders.
max_height (int, optional): The maximum height (in pixels) of the video to consider for conversion. Default is 720.
debug (bool, optional): If True, print debug messages. Default is False.
'''
# Define patterns to search for various resolutions
resolution_patterns = {
2160: r'(4096x2160|3840x2160|2880p|2160p|2160|1440p|4k)',
1080: r'(1920x1080|1080p|1080)',
720: r'(1280x720|720p|720)',
}
files_to_rename = []
dirs_to_rename = []
for dirpath, dirnames, filenames in os.walk(input_path, topdown=True):
# Collect files to rename
for filename in filenames:
for resolution, pattern in resolution_patterns.items():
# Only process resolutions greater than max_height
if resolution > max_height and re.search(
pattern, filename, re.IGNORECASE):
old_file = os.path.join(dirpath, filename)
new_filename = re.sub(pattern,
f'{max_height}p',
filename,
flags=re.IGNORECASE)
new_file = os.path.join(dirpath, new_filename)
if old_file != new_file:
files_to_rename.append((old_file, new_file))
break
# Collect directories to rename
for dirname in dirnames:
for resolution, pattern in resolution_patterns.items():
# Only process resolutions greater than max_height
if resolution > max_height and re.search(
pattern, dirname, re.IGNORECASE):
old_dir = os.path.join(dirpath, dirname)
new_dirname = re.sub(pattern,
f'{max_height}p',
dirname,
flags=re.IGNORECASE)
new_dir = os.path.join(dirpath, new_dirname)
if old_dir != new_dir:
dirs_to_rename.append((old_dir, new_dir))
break
# Rename files first
for old_file, new_file in files_to_rename:
if os.path.exists(new_file):
print(f"Error: Target filename {new_file} already exists.")
continue
os.rename(old_file, new_file)
if debug:
print(f'Renamed file: {old_file} -> {new_file}')
# Rename directories after
for old_dir, new_dir in sorted(dirs_to_rename, key=lambda x: -len(x[0])):
if os.path.exists(new_dir):
print(f"Error: Target directory name {new_dir} already exists.")
continue
os.rename(old_dir, new_dir)
if debug:
print(f'Renamed directory: {old_dir} -> {new_dir}')
def main():
'''
Main function to parse command line arguments and initiate file renamings.
'''
# Create argument parser
parser = argparse.ArgumentParser(
description=
'Rename video files containing resolutions in their filenames to a specified max_height resolution.'
)
# Define command line arguments
parser.add_argument(
'input_path',
nargs='?',
default=os.getcwd(),
help=
'directory path to search for video files (default: current directory)'
)
parser.add_argument(
'-mh',
'--max-height',
type=int,
default=720,
help='maximum height of videos to be converted (default: 720)')
parser.add_argument(
'--debug',
action='store_true',
help='enable debug mode for printing additional messages')
# Enable autocomplete for argparse
argcomplete.autocomplete(parser)
# Parse command line arguments
args = parser.parse_args()
# Rename videos
autorename(args.input_path, args.max_height, args.debug)
if __name__ == "__main__":
# Execute main function when the script is run directly
main()

132
scripts/video_manage_audio.py Executable file
View File

@ -0,0 +1,132 @@
#!/usr/bin/env python3
# video_manage_audio.py
import argparse
import argcomplete
import colorama
import os
import subprocess
# Initialize colorama for colored output in the terminal
colorama.init()
creset = colorama.Fore.RESET
ccyan = colorama.Fore.CYAN
cyellow = colorama.Fore.YELLOW
cgreen = colorama.Fore.GREEN
cred = colorama.Fore.RED
def process_audio(file_path, track, command):
"""
Modify audio tracks of a video file based on the given command.
- 'remove': Remove the specified audio track while keeping everything else.
- 'keep': Keep only the specified audio track and remove all others.
The function preserves video, subtitles, and metadata.
"""
print(f"{cgreen}Processing file: {file_path}{creset}")
output_file = f"{os.path.splitext(file_path)[0]}_{command}_audio{os.path.splitext(file_path)[1]}"
# Construct ffmpeg command based on user choice
if command == "remove":
# Remove only the specified audio track while keeping all other streams
ffmpeg_command = [
"ffmpeg", "-i", file_path, "-map", "0", "-map", f"-0:a:{track}",
"-c", "copy", output_file
]
elif command == "keep":
# Keep only the specified audio track while preserving video, subtitles, and metadata
ffmpeg_command = [
"ffmpeg", "-i", file_path, "-map", "0:v", "-map", f"0:a:{track}",
"-map", "0:s?", "-map", "0:t?", "-c", "copy", output_file
]
else:
print(f"{cred}Invalid command: {command}{creset}")
return
try:
# Execute the ffmpeg command and capture output
result = subprocess.run(ffmpeg_command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True)
if result.returncode == 0:
print(
f"{ccyan}Audio processing complete. Output saved to {output_file}{creset}\n"
)
else:
print(
f"{cred}Command failed with return code {result.returncode}{creset}\n"
)
print(f"{cred}Error output:\n{result.stderr}{creset}\n")
except subprocess.CalledProcessError as e:
print(f"{cred}Error processing audio: {e}{creset}\n")
def process_directory(dir_path, track, command):
"""
Recursively processes all video files in the specified directory.
Applies the chosen audio modification (remove or keep) to each file.
"""
for root, _, files in os.walk(dir_path):
for file in files:
if file.endswith((".mp4", ".mkv", ".avi", ".mov")):
file_path = os.path.join(root, file)
process_audio(file_path, track, command)
def main():
"""
Main function to parse command-line arguments and initiate the audio processing.
"""
# Create argument parser
parser = argparse.ArgumentParser(
description="Manage audio tracks in video files.")
# Define command line arguments
# Add a positional argument for the command
parser.add_argument('command',
choices=['remove', 'keep'],
help='Command to run')
# Add other arguments with both short and long options, including defaults
parser.add_argument("-t",
"--track",
type=int,
default=0,
help="Audio track index (default is 0).")
parser.add_argument("-f",
"--file",
type=str,
help="Path to a specific video file.")
parser.add_argument(
"-d",
"--dir",
type=str,
default=os.getcwd(),
help="Directory to process (default is current directory).")
# Enable autocomplete for command-line arguments
argcomplete.autocomplete(parser)
# Parse command line arguments
args = parser.parse_args()
# Process a single file if provided, otherwise process a directory
if args.file:
if os.path.isfile(args.file):
process_audio(args.file, args.track, args.command)
else:
print(f"{cred}File {args.file} does not exist.{creset}")
else:
if os.path.isdir(args.dir):
process_directory(args.dir, args.track, args.command)
else:
print(f"{cred}Directory {args.dir} does not exist.{creset}")
if __name__ == "__main__":
main()

142
scripts/video_manage_subtitles.py Executable file
View File

@ -0,0 +1,142 @@
#!/usr/bin/env python3
# video_manage_subtitles.py
import argparse
import argcomplete
import colorama
import os
import subprocess
# Initialize colorama for colored output in the terminal
colorama.init()
creset = colorama.Fore.RESET
ccyan = colorama.Fore.CYAN
cyellow = colorama.Fore.YELLOW
cgreen = colorama.Fore.GREEN
cred = colorama.Fore.RED
def process_subtitles(file_path, track, command):
"""
Modify subtitles tracks of a video file based on the given command.
- 'remove': Remove the specified subtitle track while keeping everything else.
- 'keep': Keep only the specified subtitle track and remove all others.
- 'none': Remove all subtitle tracks.
The function preserves video, audio, and metadata.
"""
print(f"{cgreen}Processing file: {file_path}{creset}")
output_file = f"{os.path.splitext(file_path)[0]}_{command}_subtitles{os.path.splitext(file_path)[1]}"
# Construct ffmpeg command based on user choice
if command == "remove":
# Remove only the specified subtitle track while keeping all other streams
ffmpeg_command = [
"ffmpeg", "-i", file_path, "-map", "0", "-map", f"-0:s:{track}",
"-c", "copy", output_file
]
elif command == "keep":
# Keep only the specified subtitle track while preserving video, audio, and metadata
ffmpeg_command = [
"ffmpeg", "-i", file_path, "-map", "0:v", "-map", "0:a", "-map",
f"0:s:{track}", "-map", "0:t?", "-c", "copy", output_file
]
elif command == "none":
# Remove all subtitle tracks
ffmpeg_command = [
"ffmpeg", "-i", file_path, "-map", "0", "-map", "-0:s", "-c",
"copy", output_file
]
else:
print(f"{cred}Invalid command: {command}{creset}")
return
try:
# Execute the ffmpeg command and capture output
result = subprocess.run(ffmpeg_command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True)
if result.returncode == 0:
print(
f"{ccyan}Subtitles processing complete. Output saved to {output_file}{creset}\n"
)
else:
print(
f"{cred}Command failed with return code {result.returncode}{creset}\n"
)
print(f"{cred}Error output:\n{result.stderr}{creset}\n")
except subprocess.CalledProcessError as e:
print(f"{cred}Error processing subtitles: {e}{creset}\n")
def process_directory(dir_path, track, command):
"""
Recursively processes all video files in the specified directory.
Applies the chosen subtitle modification (remove, keep, none) to each file.
"""
for root, _, files in os.walk(dir_path):
for file in files:
if file.endswith((".mp4", ".mkv", ".avi", ".mov")):
file_path = os.path.join(root, file)
process_subtitles(file_path, track, command)
def main():
"""
Main function to parse command-line arguments and initiate the subtitle processing.
"""
# Create argument parser
parser = argparse.ArgumentParser(
description="Manage subtitle tracks in video files.")
# Define command line arguments
# Add a positional argument for the command
parser.add_argument('command',
choices=['remove', 'keep', 'none'],
help='Command to run (remove, keep, or none)')
# Add other arguments with both short and long options, including defaults
parser.add_argument(
"-t",
"--track",
type=int,
default=0,
help=
"Subtitle track index (default is 0). Use 'none' to remove all subtitles."
)
parser.add_argument("-f",
"--file",
type=str,
help="Path to a specific video file.")
parser.add_argument(
"-d",
"--dir",
type=str,
default=os.getcwd(),
help="Directory to process (default is current directory).")
# Enable autocomplete for command-line arguments
argcomplete.autocomplete(parser)
# Parse command line arguments
args = parser.parse_args()
# Process a single file if provided, otherwise process a directory
if args.file:
if os.path.isfile(args.file):
process_subtitles(args.file, args.track, args.command)
else:
print(f"{cred}File {args.file} does not exist.{creset}")
else:
if os.path.isdir(args.dir):
process_directory(args.dir, args.track, args.command)
else:
print(f"{cred}Directory {args.dir} does not exist.{creset}")
if __name__ == "__main__":
main()

View File

@ -1,95 +0,0 @@
#!/usr/bin/env python3
import os
import argparse
import argcomplete
import colorama
import subprocess
colorama.init()
creset = colorama.Fore.RESET
ccyan = colorama.Fore.CYAN
cyellow = colorama.Fore.YELLOW
cgreen = colorama.Fore.GREEN
cred = colorama.Fore.RED
# Function to remove audio track from a video using ffmpeg
def remove_audio_track(file_path, track):
print(f"{cgreen}Processing file: {file_path}{creset}")
output_file = f"{os.path.splitext(file_path)[0]}_no_audio{os.path.splitext(file_path)[1]}"
# ffmpeg command to remove the specified audio track and keep other streams
command = [
"ffmpeg", "-i", file_path, "-map", "0", "-map", f"-0:a:{track}", "-c",
"copy", output_file
]
try:
result = subprocess.run(command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True)
if result.returncode == 0:
print(
f"{ccyan}Audio track {track} removed. Output saved to {output_file}{creset}\n"
)
else:
print(
f"{cred}Command failed with return code {result.returncode}{creset}\n"
)
print(f"{cred}Error output:", result.stderr, f"{creset}\n")
except subprocess.CalledProcessError as e:
print(f"{cred}Error removing audio track: {e}{creset}\n")
# Function to recursively process videos in a directory
def process_directory(dir_path, track):
for root, _, files in os.walk(dir_path):
for file in files:
if file.endswith((".mp4", ".mkv", ".avi",
".mov")): # Add more formats as needed
file_path = os.path.join(root, file)
remove_audio_track(file_path, track)
def main():
# Set up argument parser
parser = argparse.ArgumentParser(
description="Remove audio track from video files.")
parser.add_argument("--track",
type=int,
default=0,
help="Audio track index to remove (default is 0).")
parser.add_argument("--file",
type=str,
help="Path to a specific video file.")
parser.add_argument(
"--dir",
type=str,
default=os.getcwd(),
help="Directory to process (default is current directory).")
# Enable autocomplete for argparse
argcomplete.autocomplete(parser)
args = parser.parse_args()
# Process single file if provided
if args.file:
if os.path.isfile(args.file):
remove_audio_track(args.file, args.track)
else:
print(f"File {args.file} does not exist.")
# Otherwise, process all files in the specified directory
else:
if os.path.isdir(args.dir):
process_directory(args.dir, args.track)
else:
print(f"Directory {args.dir} does not exist.")
if __name__ == "__main__":
main()

View File

@ -68,6 +68,7 @@ Replace the placeholders below with the appropriate values for your setup:
- Hostname - Hypervisor: `<hostname-hypervisor>` (e.g., proxmox-hypervisor.domain.com) - Hostname - Hypervisor: `<hostname-hypervisor>` (e.g., proxmox-hypervisor.domain.com)
- Hostname - Hypervisor NAS: `<hostname-hypervisor-nas>` (e.g., nas-server.domain.com) - Hostname - Hypervisor NAS: `<hostname-hypervisor-nas>` (e.g., nas-server.domain.com)
- Name - Hypervisor NAS: `<name-hypervisor-nas>` (e.g., nas-server) - Name - Hypervisor NAS: `<name-hypervisor-nas>` (e.g., nas-server)
- Container ID - Hypervisor: `<container-id-hypervisor>` (e.g., 100)
- **SSH Keys** - **SSH Keys**
@ -140,40 +141,40 @@ ssh <username>@<hostname-hypervisor-nas> "ls /mnt/proxmox/template/cache/"
**Create the container** **Create the container**
```bash ```bash
ssh <username-hypervisor>@<hostname-hypervisor> "pct create 100 <name-hypervisor-nas>:vztmpl/debian-12-upgraded_12.5_amd64.tar.zst --hostname <hostname-intranet> --cores 2 --memory 2048 --swap 2048 --net0 name=net0,bridge=vmbr0,ip=dhcp,firewall=1 --rootfs <name-hypervisor-nas>:250 --unprivileged 1 --features nesting=1 --ssh-public-keys <ssh-key-proxmox> --start 1" ssh <username-hypervisor>@<hostname-hypervisor> "pct create <container-id-hypervisor> <name-hypervisor-nas>:vztmpl/debian-12-upgraded_12.5_amd64.tar.zst --hostname <hostname-intranet> --cores 2 --memory 2048 --swap 2048 --net0 name=net0,bridge=vmbr0,ip=dhcp,firewall=1 --rootfs <name-hypervisor-nas>:250 --unprivileged 1 --features nesting=1 --ssh-public-keys <ssh-key-proxmox> --start 1"
``` ```
**Backup** **Backup**
```bash ```bash
ssh <username-hypervisor>@<hostname-hypervisor> "vzdump 100 --compress zstd --mode stop --storage <name-hypervisor-nas> --note \"$(date +'%Y-%m-%d %H:%M') Backup fresh install\"" ssh <username-hypervisor>@<hostname-hypervisor> "vzdump <container-id-hypervisor> --compress zstd --mode stop --storage <name-hypervisor-nas> --note \"$(date +'%Y-%m-%d %H:%M') Backup fresh install\""
``` ```
**Set the state of the Proxmox HA Manager for Container 100** **Set the state of the Proxmox HA Manager for Container <container-id-hypervisor>**
```bash ```bash
ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager add ct:100" ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager add ct:<container-id-hypervisor>"
ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager remove ct:100" ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager remove ct:<container-id-hypervisor>"
``` ```
**Set the state and limits of the Proxmox Container 100 in the HA Manager** **Set the state and limits of the Proxmox Container <container-id-hypervisor> in the HA Manager**
```bash ```bash
ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager set ct:100 --state started --max_relocate 3 --max_restart 3" ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager set ct:<container-id-hypervisor> --state started --max_relocate 3 --max_restart 3"
ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager set ct:100 --state stopped" ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager set ct:<container-id-hypervisor> --state stopped"
ssh <username-hypervisor>@<hostname-hypervisor> "pct reboot 100" ssh <username-hypervisor>@<hostname-hypervisor> "pct reboot <container-id-hypervisor>"
``` ```
**Destroy the Proxmox Container 100 forcefully** **Destroy the Proxmox Container <container-id-hypervisor> forcefully**
```bash ```bash
ssh <username-hypervisor>@<hostname-hypervisor> "pct destroy 100 --force --purge" ssh <username-hypervisor>@<hostname-hypervisor> "pct destroy <container-id-hypervisor> --force --purge"
``` ```
**Move the Proxmox Container 100 to another host** **Move the Proxmox Container <container-id-hypervisor> to another host**
```bash ```bash
ssh <username-hypervisor>@<hostname-hypervisor> "pct migrate 100 hv2" ssh <username-hypervisor>@<hostname-hypervisor> "pct migrate <container-id-hypervisor> hv2"
``` ```
### SSH Connection ### SSH Connection
@ -220,7 +221,7 @@ chown -R aptly:aptly /home/aptly/.ssh/
2. **Backup before starting** 2. **Backup before starting**
```bash ```bash
ssh <username-hypervisor>@<hostname-hypervisor> "vzdump 100 --compress zstd --mode stop --storage <name-hypervisor-nas> --note \"$(date +'%Y-%m-%d %H:%M') Backup fresh install\"" ssh <username-hypervisor>@<hostname-hypervisor> "vzdump <container-id-hypervisor> --compress zstd --mode stop --storage <name-hypervisor-nas> --note \"$(date +'%Y-%m-%d %H:%M') Backup fresh install\""
``` ```
3. **Install Required Dependencies** 3. **Install Required Dependencies**
@ -623,11 +624,11 @@ chown -R aptly:aptly /home/aptly/.ssh/
14. **Back-up post installation** 14. **Back-up post installation**
```bash ```bash
ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager set ct:100 --state stopped" ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager set ct:<container-id-hypervisor> --state stopped"
ssh <username-hypervisor>@<hostname-hypervisor> "vzdump 100 --compress zstd --mode stop --storage <name-hypervisor-nas> --note \"$(date +'%Y-%m-%d %H:%M') Backup post installation\"" ssh <username-hypervisor>@<hostname-hypervisor> "vzdump <container-id-hypervisor> --compress zstd --mode stop --storage <name-hypervisor-nas> --note \"$(date +'%Y-%m-%d %H:%M') Backup post installation\""
``` ```
15. **Start the server** 15. **Start the server**
```bash ```bash
ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager set ct:100 --state started --max_relocate 3 --max_restart 3" ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager set ct:<container-id-hypervisor> --state started --max_relocate 3 --max_restart 3"
``` ```

View File

@ -0,0 +1,626 @@
# Installing Dolibarr on Debian
## Table of Contents
- [Installing Dolibarr on Debian](#installing-dolibarr-on-debian)
- [Table of Contents](#table-of-contents)
- [Introduction](#introduction)
- [Why Dolibarr?](#why-dolibarr)
- [Prerequisites](#prerequisites)
- [What This Guide Covers](#what-this-guide-covers)
- [Placeholders](#placeholders)
- [Important Warnings and Security Practices](#important-warnings-and-security-practices)
- [Useful Commands and Information](#useful-commands-and-information)
- [Documentation](#documentation)
- [Links](#links)
- [Software on the Machine](#software-on-the-machine)
- [Paths](#paths)
- [Proxmox Commands](#proxmox-commands)
- [SSH Connection](#ssh-connection)
- [Installation Procedure](#installation-procedure)
## Introduction
Welcome to the installation guide for Dolibarr on Debian. Dolibarr is an open-source ERP and CRM platform designed for small and medium-sized businesses, offering features such as invoicing, accounting, inventory management, and customer relationship management through a clean, web-based interface. By following this guide, youll be able to install and configure Dolibarr on your Debian system efficiently.
## Why Dolibarr?
Dolibarr is a flexible and open-source ERP/CRM solution that allows you to keep full control over your business data and infrastructure. It covers a wide range of needs for small and medium-sized organizations, including invoicing, accounting, customer management, inventory, and project tracking, all from a single web interface. Being self-hosted, Dolibarr gives you data ownership, transparency, and the ability to customize or extend the platform to fit your workflows without relying on third-party SaaS providers.
## Prerequisites
Before you begin the installation process, ensure that your Debian system meets the following requirements:
- Debian GNU/Linux 9 (Stretch) or later
- Access to a terminal with sudo privileges
- Basic familiarity with the command line interface
- Stable internet connection to download necessary packages
## What This Guide Covers
This guide will walk you through the installation of Dolibarr on Debian step by step, covering:
1. **Installation**: Installing Dolibarr from the official Git repository.
2. **Configuration**: Adjusting Dolibarr and system settings to match your environment.
3. **Web Setup**: Configuring Dolibarr with Apache and accessing the web installer.
4. **Security**: Hardening Debian, PHP, and Dolibarr for a production setup.
## Placeholders
Replace the placeholders below with the appropriate values for your setup:
- **User Details**
- Username: `<username>` (e.g., admin)
- Username - Hypervisor: `<username-hypervisor>` (e.g., admin)
- **Server Configuration**
- Server IP address: `<server-ip-address>` (e.g., 192.168.1.100)
- Hostname - Intranet: `<hostname-intranet>` (e.g., dolibarr-server.domain.com)
- Hostname - Internet: `<hostname-internet>` (e.g., dolibarr.domain.com)
- Hostname - Hypervisor: `<hostname-hypervisor>` (e.g., proxmox-hypervisor.domain.com)
- Hostname - Hypervisor NAS: `<hostname-hypervisor-nas>` (e.g., nas-server.domain.com)
- Name - Hypervisor NAS: `<name-hypervisor-nas>` (e.g., nas-server)
- Container ID - Hypervisor: `<container-id-hypervisor>` (e.g., 100)
- **SSH Keys**
- SSH key - Proxmox: `<ssh-key-proxmox>` (e.g., /home/user/.ssh/id_rsa.pub)
- SSH key - Client: `<ssh-key-client>` (e.g., /home/user/.ssh/client_id_rsa.pub)
- **Networking**
- Wireguard port: `<wireguard-port>` (e.g., 51820)
- **Dolibarr specifics post-install**
- Contract paths: `<dolibarr-contract-paths-source>` (e.g., /home/username/contracts/)
- Fonts paths: `<dolibarr-fonts-paths-source>` (e.g., /home/username/fonts/)
## Important Warnings and Security Practices
Before executing any commands in this documentation, please adhere to the following guidelines to ensure the security and integrity of the system:
1. **Execute Commands with Caution**: Always review and understand a command before executing it. Misuse of commands can lead to data loss or system instability.
2. **Backup Command Execution**: The backup command must be executed only by authorized users. Ensure that proper permissions are set to prevent unauthorized access to backup files.
3. **Regular Backups**: Maintain regular backups of all critical data. It is advisable to use automated backup solutions and verify backup integrity periodically.
4. **System Updates**: Regularly update the system and all installed packages to protect against vulnerabilities. Use the package manager responsibly to avoid potential conflicts.
5. **Monitor System Logs**: Continuously monitor system logs for any unusual activity. Use logging tools to help identify potential security breaches or system failures.
6. **User Permissions**: Ensure that user permissions are strictly managed. Limit access to sensitive commands and data to only those who need it to perform their job functions.
7. **Network Security**: Implement proper network security measures, such as firewalls and intrusion detection systems, to protect against external threats.
8. **Data Encryption**: Encrypt sensitive data at rest and in transit to prevent unauthorized access.
By following these practices, you will help maintain the security and stability of the system while minimizing the risk of data loss or compromise.
## Useful Commands and Information
### Documentation
- [Setup other](https://wiki.dolibarr.org/index.php?title=Setup_Other)
- [Create an ODT or ODS document template](https://wiki.dolibarr.org/index.php?title=Create_an_ODT_or_ODS_document_template)
- [Setup Other](https://wiki.dolibarr.org/index.php?title=Setup_Other)
- [List of releases, change log and compatibilities](https://wiki.dolibarr.org/index.php?title=List_of_releases,_change_log_and_compatibilities)
- [GitHub](https://github.com/Dolibarr/dolibarr)
### Links
- [https](https://<hostname-intranet>/)
- [http](http://<hostname-intranet>/)
- [local https](https://<hostname-intranet>/)
- [local http](http://<hostname-intranet>/)
- [<server-ip-address>](http://<server-ip-address>/)
### Software on the Machine
- **Operating System**: Debian
- **Database**: MariaDB (configured as MySQL)
- **Web Server**: Apache
- **Security**: GnuPG, WireGuard, UFW
- **Other**: Git, sudo, certbot (for SSL certificate management)
### Paths
- **Dolibarr Configuration**: `/etc/dolibarr/app.ini`
- **Dolibarr Work Path**: `/var/lib/dolibarr`
### Proxmox Commands
**List available Proxmox templates**
```bash
ssh <username>@<hostname-hypervisor-nas> "ls /mnt/proxmox/template/cache/"
```
**Create the container**
```bash
ssh <username-hypervisor>@<hostname-hypervisor> "pct create <container-id-hypervisor> <name-hypervisor-nas>:vztmpl/debian-12-upgraded_12.5_amd64.tar.zst --hostname <hostname-intranet> --cores 4 --memory 4096 --swap 2048 --net0 name=net0,bridge=vmbr0,ip=dhcp,firewall=1 --rootfs <name-hypervisor-nas>:250 --unprivileged 1 --features nesting=1 --ssh-public-keys <ssh-key-proxmox>"
```
**Backup**
```bash
ssh <username-hypervisor>@<hostname-hypervisor> "vzdump <container-id-hypervisor> --compress zstd --mode stop --storage <name-hypervisor-nas> --note \"$(date +'%Y-%m-%d %H:%M') Backup fresh install\""
```
**Set the state of the Proxmox HA Manager for Container <container-id-hypervisor>**
```bash
ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager add ct:<container-id-hypervisor>"
ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager remove ct:<container-id-hypervisor>"
```
**Set the state and limits of the Proxmox Container <container-id-hypervisor> in the HA Manager**
```bash
ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager set ct:<container-id-hypervisor> --state started --max_relocate 3 --max_restart 3"
ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager set ct:<container-id-hypervisor> --state stopped"
ssh <username-hypervisor>@<hostname-hypervisor> "pct start <container-id-hypervisor>"
ssh <username-hypervisor>@<hostname-hypervisor> "pct stop <container-id-hypervisor>"
ssh <username-hypervisor>@<hostname-hypervisor> "pct reboot <container-id-hypervisor>"
```
**Destroy the Proxmox Container <container-id-hypervisor> forcefully**
```bash
ssh <username-hypervisor>@<hostname-hypervisor> "pct destroy <container-id-hypervisor> --force --purge"
```
**Move the Proxmox Container <container-id-hypervisor> to another host**
```bash
ssh <username-hypervisor>@<hostname-hypervisor> "pct migrate <container-id-hypervisor> hv2"
```
### SSH Connection
**Connection with specific keys**
```bash
ssh -i <ssh-key-client> root@<hostname-intranet>
ssh -i <ssh-key-client> root@<server-ip-address>
ssh -i <ssh-key-client> <username>@<hostname-intranet>
ssh -i <ssh-key-client> <username>@<server-ip-address>
```
**Remove offending keys from known_hosts**
```bash
ssh-keygen -f "/home/<username>/.ssh/known_hosts" -R "<hostname-intranet>"
ssh-keygen -f "/home/<username>/.ssh/known_hosts" -R "<server-ip-address>"
```
**Copy SSH public key to remote host**
```bash
ssh-copy-id -i <ssh-key-client> root@<server-ip-address>
ssh-copy-id -i <ssh-key-client> root@<hostname-intranet>
ssh-copy-id -i <ssh-key-client> <username>@<server-ip-address>
ssh-copy-id -i <ssh-key-client> <username>@<hostname-intranet>
```
**Transfer SSH keys and files**
```bash
scp /home/<username>/.ssh/<username>* <username>@<hostname-intranet>:/home/<username>/.ssh/
mkdir -p /home/<username>/.ssh/
mv /home/<username>/.ssh/<username>* /home/<username>/.ssh/
chown -R <username>:<username> /home/<username>/.ssh/
cat /home/<username>/.ssh/<username>.pub >> /home/<username>/.ssh/authorized_keys
```
## Installation Procedure
1. **Fresh Debian Installation**
- Install a fresh Debian operating system on your new server following the standard installation procedure.
2. **Backup before starting**
```bash
ssh <username-hypervisor>@<hostname-hypervisor> "vzdump <container-id-hypervisor> --compress zstd --mode stop --storage <name-hypervisor-nas> --note \"$(date +'%Y-%m-%d %H:%M') Backup fresh install\""
```
3. **Install Required Dependencies**
**Upgrade the base system**
```bash
apt update
apt upgrade -y
```
**Install dependencies**
- [Prerequisites](https://wiki.dolibarr.org/index.php?title=Prerequisites)
- [Dolibarr downloads](https://www.dolibarr.org/downloads.php)
- [Dolibarr Sourceforge Debian packages](https://sourceforge.net/projects/dolibarr/files/Dolibarr%20installer%20for%20Debian-Ubuntu%20%28DoliDeb%29/20.0.3/)
```bash
apt install -y git apache2 mariadb-server php php-mysql php-mbstring php-gd php-curl php-xml php-intl php-imap php-zip libapache2-mod-php sudo
apt install -y libreoffice-common libreoffice-writer --no-install-recommends
apt install -y fonts-dejavu fonts-liberation ttf-mscorefonts-installer
apt install -y gpg
```
4. **Ensure Hostname**
```bash
nano /etc/hosts
# Add line: 127.0.1.1 <hostname-intranet>
nano /etc/hostname
# Set to: <hostname-intranet>
hostnamectl set-hostname <hostname-intranet>
```
5. **Add Users**
```bash
adduser <username>
usermod -aG sudo <username>
```
```bash
mkdir -p /home/www-data
chown www-data:www-data /home/www-data
usermod -d /home/www-data www-data
usermod -s /bin/bash www-data
```
```bash
adduser --system --shell /bin/bash --gecos 'Dolibarr CRM' --group --disabled-password --home /home/www-data www-data
```
6. **Setup SSH Connectors**
- Configure SSH connectors as per your setup script to establish secure connections to the server.
7. **Test users, SSH, and sudo**
1. **Transfer SSH keys for User**
2. **Connect as User with SSH key**
3. **Test sudo**
```bash
sudo su -
```
4. **Disconnect as root**
8. **Secure SSH**
```bash
nano /etc/ssh/sshd_config
```
```ini
PermitRootLogin no
PasswordAuthentication no
ChallengeResponseAuthentication no
```
**Restart SSH**
```bash
systemctl restart ssh
```
9. **Configure Firewall**
**Open ports**
```bash
ufw allow OpenSSH
ufw allow <wireguard-port>/udp
```
**Enable firewall**
```bash
ufw enable
```
10. **Configure Mariadb**
1. **Security**
```bash
mariadb-secure-installation
mariadb
```
2. **Mariadb Users**
```sql
SET old_passwords=0;
CREATE USER 'dolibarr' IDENTIFIED BY '';
```
```sql
CREATE USER IF NOT EXISTS 'dolibarr'@'192.168.1.%' IDENTIFIED BY '';
GRANT ALL PRIVILEGES ON *.* TO 'dolibarr'@'192.168.1.%' IDENTIFIED BY '' WITH GRANT OPTION;
GRANT ALL PRIVILEGES ON *.* TO 'dolibarr'@'localhost' IDENTIFIED BY '' WITH GRANT OPTION;
SET PASSWORD FOR 'dolibarr'@'localhost' = PASSWORD('');
SET PASSWORD FOR 'dolibarr'@'192.168.1.%' = PASSWORD('');
FLUSH PRIVILEGES;
```
3. **Create Dolibarr Database**
```bash
mariadb -u root -p
```
Inside the MariaDB shell:
```sql
CREATE DATABASE dolibarr;
CREATE USER 'dolibarruser'@'localhost' IDENTIFIED BY 'strongpassword';
GRANT ALL PRIVILEGES ON dolibarr.* TO 'dolibarruser'@'localhost';
FLUSH PRIVILEGES;
EXIT;
```
11. **Install Dolibarr**
[Versions](https://wiki.dolibarr.org/index.php?title=List_of_releases,_change_log_and_compatibilities)
```bash
sudo su -
mv /var/www/html /var/www/.html
cd /var/www
git clone --depth 1 -b 20.0.3 https://github.com/Dolibarr/dolibarr.git dolibarr
mv dolibarr-20.0.3 dolibarr
```
12. **Create required directory structure**
```bash
chmod -R 755 /var/www/dolibarr
chown -R www-data:www-data /var/www/dolibarr
cd dolibarr ; touch htdocs/conf/conf.php ; chown www-data htdocs/conf/conf.php
mkdir -p /var/lib/dolibarr/sessions ; chown www-data /var/lib/dolibarr/sessions
mkdir -p /var/lib/dolibarr/documents ; chown www-data /var/lib/dolibarr/documents
```
13. **Apache Configuration**
1. **Allow http https**
```bash
ufw allow https
ufw allow http
```
2. **Backup Default Files**
Backup the default index.html and configuration files.
```bash
mv /var/www/html /var/www/.html
mv /etc/apache2/sites-available/000-default.conf /etc/apache2/sites-available/.000-default.conf
rm /etc/apache2/sites-enabled/000-default.conf
```
3. **Create Default Server Block for Unauthorized Access**
4. **Site configuration**
Edit the Apache configuration file to proxy to the Dolibarr CRM.
```bash
nano /etc/apache2/sites-available/<hostname-intranet>.conf
```
Add the following configuration:
```apache
<VirtualHost *:80>
LogLevel info
ServerName <hostname-intranet>
ServerAdmin admin@fabq.ca
DocumentRoot /var/www/dolibarr
# Alias for Dolibarr
Alias / /var/www/dolibarr/htdocs/
<Directory /var/www/dolibarr/htdocs>
Options +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/error.dolibarr.log
CustomLog ${APACHE_LOG_DIR}/access.dolibarr.log combined
</VirtualHost>
```
5. **Enabling the new VirtualHost**
```bash
a2ensite <hostname-intranet>.conf
a2enmod rewrite
```
6. **Managing Apache**
**Enable Apache**
```bash
systemctl enable apache2
```
**Apache reload**
```bash
systemctl reload apache2
```
**`Optional` Apache restart**
```bash
systemctl restart apache2
```
14. **Create folders**
```bash
mkdir -p /var/tmp
mkdir -p /var/upload_tmp
```
15. **Set temporary file paths**
```bash
nano /etc/php/8.2/apache2/php.ini
```
```ini
sys_temp_dir = "/var/tmp"
upload_tmp_dir = "/var/upload_tmp"
open_basedir = "/var/www/dolibarr:/var/lib/dolibarr/documents:/var/lib/dolibarr/sessions:/var/tmp:/var/upload_tmp"
```
16. **Fix permissions**
```bash
chown -R www-data:www-data /var/lib/php/sessions
chown -R www-data:www-data /var/www/dolibarr/htdocs/conf/conf.php
chmod 640 /var/www/dolibarr/htdocs/conf/conf.php
find /var/www/dolibarr -type d -exec chmod 755 {} \;
find /var/www/dolibarr -type f -exec chmod 644 {} \;
chmod go-w /var/lib/dolibarr/documents;
chmod go-w /var/lib/dolibarr/sessions;
chown -R www-data:www-data /var/tmp
chown -R www-data:www-data /var/upload_tmp
chmod -R 750 /var/tmp
chmod -R 750 /var/upload_tmp
```
17. **Secure PHP**
```bash
nano /etc/php/8.2/apache2/php.ini
```
```ini
session.use_strict_mode = 1
allow_url_fopen = No
disable_functions = dl, apache_note, apache_setenv, pcntl_alarm, pcntl_fork, pcntl_waitpid, pcntl_wait, pcntl_wifexited, pcntl_wifstopped, pcntl_wifsignaled, pcntl_wifcontinued, pcntl_wexitstatus, pcntl_wtermsig, pcntl_wstopsig, pcntl_signal, pcntl_signal_get_handler, pcntl_signal_dispatch, pcntl_get_last_error, pcntl_strerror, pcntl_sigprocmask, pcntl_sigwaitinfo, pcntl_sigtimedwait, pcntl_exec, pcntl_getpriority, pcntl_setpriority, pcntl_async_signals, show_source, virtual, passthru, shell_exec, system, proc_open, popen
session.gc_maxlifetime = 604800
```
18. **Complete the Installation through Web Interface**
- Open your web browser and navigate to `http://<hostname-intranet>/install/`
- Follow the on-screen instructions to complete the installation.
19. **Lock installation**
```bash
chmod -w /var/www/dolibarr/htdocs/conf/conf.php
touch /var/lib/dolibarr/documents/install.lock;
chmod go-w /var/lib/dolibarr/documents;
```
20. **Secure installation**
```bash
chmod -R -w /var/www/dolibarr/htdocs
cp /etc/php/8.4/apache2/php.ini /etc/php/8.4/apache2/php.ini.back
nano /etc/php/8.4/apache2/php.ini
```
```ini
session.save_path = /var/lib/dolibarr/sessions
session.use_strict_mode = 1
session.use_only_cookies = 1
session.cookie_httponly = 1
session.cookie_samesite = Lax
session.gc_maxlifetime = 604800
open_basedir = "/var/www/dolibarr:/var/lib/dolibarr/documents:/var/lib/dolibarr/sessions:/var/tmp:/var/upload_tmp"
short_open_tag = Off
allow_url_fopen = Off
allow_url_include = Off
disable_functions = dl, apache_note, apache_setenv, pcntl_alarm, pcntl_fork, pcntl_waitpid, pcntl_wait, pcntl_wifexited, pcntl_wifstopped, pcntl_wifsignaled, pcntl_wifcontinued, pcntl_wexitstatus, pcntl_wtermsig, pcntl_wstopsig, pcntl_signal, pcntl_signal_get_handler, pcntl_signal_dispatch, pcntl_get_last_error, pcntl_strerror, pcntl_sigprocmask, pcntl_sigwaitinfo, pcntl_sigtimedwait, pcntl_exec, pcntl_getpriority, pcntl_setpriority, pcntl_async_signals, show_source, virtual, passthru, shell_exec, system, proc_open, popen
```
```bash
nano /var/www/dolibarr/htdocs/conf/conf.php
chmod o-r /var/www/dolibarr/htdocs/conf/conf.php
```
```php
$dolibarr_main_prod='1';
```
21. **Configure SSL with Certbot (Optional, If directly accessed)**
```bash
certbot --apache -d <hostname-internet>
systemctl status certbot.timer
certbot renew --dry-run
```
22. **Verify installation**
- Verify that the Dolibarr instance is installed restored by accessing it through a web browser. Ensure that all repositories, users, and configurations are intact.
```bash
systemctl status apache2
```
Visit the server at [http://<hostname-internet>](http://<hostname-internet>) or [https://<hostname-internet>](https://<hostname-internet>).
23. **Backup post installation**
```bash
ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager set ct:<container-id-hypervisor> --state stopped"
ssh <username-hypervisor>@<hostname-hypervisor> "vzdump <container-id-hypervisor> --compress zstd --mode stop --storage <name-hypervisor-nas> --note \"$(date +'%Y-%m-%d %H:%M') Backup post installation\""
```
24. **Start the server**
```bash
ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager set ct:<container-id-hypervisor> --state started --max_relocate 3 --max_restart 3"
```
25. **Move contracts**
```bash
scp '<dolibarr-contract-paths-source>/contract.odt' <username>@<hostname-intranet>:~/
sudo su -
mv /home/<username>/*.odt /var/lib/dolibarr/documents/doctemplates/contracts/
chown -R www-data:www-data /var/lib/dolibarr/documents/doctemplates/contracts/
```
26. **ODT to PDF**
[See forum 1](https://www.dolibarr.org/forum/t/solved-odt-to-pdf/16931)
[See forum 2](https://www.dolibarr.org/forum/t/setup-dolibarr-to-generate-odt-to-pdf/22112/9)
```bash
MAIN_ODT_AS_PDF
```
27. **Install fonts**
```bash
scp -r <dolibarr-fonts-paths-source>/Lato <username>@<hostname-intranet>:~/
```
```bash
mv /home/<username>/Lato /usr/local/share/fonts/
chmod -R 755 /usr/local/share/fonts/
chown -R root:root /usr/local/share/fonts/
```

View File

@ -63,6 +63,7 @@ Replace the placeholders below with the appropriate values for your setup:
- Hostname - Hypervisor: `<hostname-hypervisor>` (e.g., proxmox-hypervisor.domain.com) - Hostname - Hypervisor: `<hostname-hypervisor>` (e.g., proxmox-hypervisor.domain.com)
- Hostname - Hypervisor NAS: `<hostname-hypervisor-nas>` (e.g., nas-server.domain.com) - Hostname - Hypervisor NAS: `<hostname-hypervisor-nas>` (e.g., nas-server.domain.com)
- Name - Hypervisor NAS: `<name-hypervisor-nas>` (e.g., nas-server) - Name - Hypervisor NAS: `<name-hypervisor-nas>` (e.g., nas-server)
- Container ID - Hypervisor: `<container-id-hypervisor>` (e.g., 100)
- **SSH Keys** - **SSH Keys**
@ -126,40 +127,40 @@ ssh <username>@<hostname-hypervisor-nas> "ls /mnt/proxmox/template/cache/"
**Create the container** **Create the container**
```bash ```bash
ssh <username-hypervisor>@<hostname-hypervisor> "pct create 100 <name-hypervisor-nas>:vztmpl/debian-12-upgraded_12.5_amd64.tar.zst --hostname <hostname-intranet> --cores 2 --memory 2048 --swap 2048 --net0 name=net0,bridge=vmbr0,ip=dhcp,firewall=1 --rootfs <name-hypervisor-nas>:250 --unprivileged 1 --features nesting=1 --ssh-public-keys <ssh-key-proxmox> --start 1" ssh <username-hypervisor>@<hostname-hypervisor> "pct create <container-id-hypervisor> <name-hypervisor-nas>:vztmpl/debian-12-upgraded_12.5_amd64.tar.zst --hostname <hostname-intranet> --cores 2 --memory 2048 --swap 2048 --net0 name=net0,bridge=vmbr0,ip=dhcp,firewall=1 --rootfs <name-hypervisor-nas>:250 --unprivileged 1 --features nesting=1 --ssh-public-keys <ssh-key-proxmox> --start 1"
``` ```
**Backup** **Backup**
```bash ```bash
ssh <username-hypervisor>@<hostname-hypervisor> "vzdump 100 --compress zstd --mode stop --storage <name-hypervisor-nas> --note \"$(date +'%Y-%m-%d %H:%M') Backup fresh install\"" ssh <username-hypervisor>@<hostname-hypervisor> "vzdump <container-id-hypervisor> --compress zstd --mode stop --storage <name-hypervisor-nas> --note \"$(date +'%Y-%m-%d %H:%M') Backup fresh install\""
``` ```
**Set the state of the Proxmox HA Manager for Container 100** **Set the state of the Proxmox HA Manager for Container <container-id-hypervisor>**
```bash ```bash
ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager add ct:100" ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager add ct:<container-id-hypervisor>"
ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager remove ct:100" ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager remove ct:<container-id-hypervisor>"
``` ```
**Set the state and limits of the Proxmox Container 100 in the HA Manager** **Set the state and limits of the Proxmox Container <container-id-hypervisor> in the HA Manager**
```bash ```bash
ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager set ct:100 --state started --max_relocate 3 --max_restart 3" ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager set ct:<container-id-hypervisor> --state started --max_relocate 3 --max_restart 3"
ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager set ct:100 --state stopped" ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager set ct:<container-id-hypervisor> --state stopped"
ssh <username-hypervisor>@<hostname-hypervisor> "pct reboot 100" ssh <username-hypervisor>@<hostname-hypervisor> "pct reboot <container-id-hypervisor>"
``` ```
**Destroy the Proxmox Container 100 forcefully** **Destroy the Proxmox Container <container-id-hypervisor> forcefully**
```bash ```bash
ssh <username-hypervisor>@<hostname-hypervisor> "pct destroy 100 --force --purge" ssh <username-hypervisor>@<hostname-hypervisor> "pct destroy <container-id-hypervisor> --force --purge"
``` ```
**Move the Proxmox Container 100 to another host** **Move the Proxmox Container <container-id-hypervisor> to another host**
```bash ```bash
ssh <username-hypervisor>@<hostname-hypervisor> "pct migrate 100 hv2" ssh <username-hypervisor>@<hostname-hypervisor> "pct migrate <container-id-hypervisor> hv2"
``` ```
### SSH Connection ### SSH Connection
@ -208,7 +209,7 @@ cat /home/<username>/.ssh/<username>.pub >> /home/<username>/.ssh/authorized_key
2. **Backup before starting** 2. **Backup before starting**
```bash ```bash
ssh <username-hypervisor>@<hostname-hypervisor> "vzdump 100 --compress zstd --mode stop --storage <name-hypervisor-nas> --note \"$(date +'%Y-%m-%d %H:%M') Backup fresh install\"" ssh <username-hypervisor>@<hostname-hypervisor> "vzdump <container-id-hypervisor> --compress zstd --mode stop --storage <name-hypervisor-nas> --note \"$(date +'%Y-%m-%d %H:%M') Backup fresh install\""
``` ```
3. **Install Required Dependencies** 3. **Install Required Dependencies**
@ -503,11 +504,11 @@ cat /home/<username>/.ssh/<username>.pub >> /home/<username>/.ssh/authorized_key
19. **Backup post installation** 19. **Backup post installation**
```bash ```bash
ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager set ct:100 --state stopped" ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager set ct:<container-id-hypervisor> --state stopped"
ssh <username-hypervisor>@<hostname-hypervisor> "vzdump 100 --compress zstd --mode stop --storage <name-hypervisor-nas> --note \"$(date +'%Y-%m-%d %H:%M') Backup post installation\"" ssh <username-hypervisor>@<hostname-hypervisor> "vzdump <container-id-hypervisor> --compress zstd --mode stop --storage <name-hypervisor-nas> --note \"$(date +'%Y-%m-%d %H:%M') Backup post installation\""
``` ```
20. **Start the server** 20. **Start the server**
```bash ```bash
ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager set ct:100 --state started --max_relocate 3 --max_restart 3" ssh <username-hypervisor>@<hostname-hypervisor> "ha-manager set ct:<container-id-hypervisor> --state started --max_relocate 3 --max_restart 3"
``` ```