# Windows

Eigentlich sollte man es nicht verwenden, aber es ist zu weit verbreitet, um es zu vermeiden.

# Windows Registry

### Meine eigenen Standard-Anpassungen

Ihr kennt das: neues Gerät, frisches Windows, und erst mal die Feststellung, dass Windows doch so einige Dinge gegen euren Willen anders tut als ihr es erwartet.  
Microsoft ist halt auch noch nicht auf den Trichter gekommen, genau diese Einstellungen über Microsoft-Accounts zwischen Geräten zu synchronisieren...

Das sind hier nur Dinge, die ich unbedingt brauche, die irgendwie ohne halt auch nicht gehen:

```Python
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\System]
; Windows Hello für alle
"AllowDomainPINLogon"=dword:00000001
; Unschärfe ist eh blöd
"DisableAcrylicBackgroundOnLogon"=dword:00000001

[HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced]
; Das müsste eh überall so sein
"HideFileExt"=dword:00000000

```

### Crashkurs

#### TL;DR

- HKLM = Alle User
- HKCU = nur aktueller User - wisst ihr doch, oder? Der Rest der Baumstruktur ist nahezu identisch, nur dass eines halt für die Maschine zieht und das andere nicht.
- HKEY\_CLASSES\_ROOT = der Weg, wie Windows weiß, was mit euren Files gemacht werden soll. Hier spielt ihr rum, wenn euch irgendwelche Programme auf den Sack gehen.
- HKLM überschreibt HKCU - das wäre nur logisch, oder?

HKLM ist fast immer eine exakte Spiegelung von HKCU, von Werten her... Nur Extrasoftware schreibt Differenzen in den beiden Registry-Bäumen.

weiter dann hier -&gt; [MS Docs: Windows registry information for advanced users](https://docs.microsoft.com/en-us/troubleshoot/windows-server/performance/windows-registry-advanced-users)

# Windows-Sicherheit (Enterprise)

### Fefe / Windows-Boardmittel-Sicherheit

[https://blog.fefe.de/?ts=9c40177c?css=fefe.css](https://blog.fefe.de/?ts=9c40177c?css=fefe.css)

Ich halte das hier mal fest, geht um [diese Präsentation](https://ptrace.fefe.de/Zero%20Trust/).  
Meine Vermutung ist, dass es sich dabei noch um OnPrem-Umgebungen handelt.

1. Software Restriction Policies
2. Office Makros ganz weg (Inklusive Plugins und Software die noch darauf beruht)
3. Zitat: "NTLM Auth weg, Credential Caching für Admins und generell Server abschalten, Debugging Rechte global abschalten und nur im Einzelfall erlauben. LAPS benutzen, auch für Server."
4. [ESAE](https://learn.microsoft.com/de-de/security/privileged-access-workstations/esae-retirement)
5. Hardwarekeys bzw. Smartcards für Admins (damit es keine Keylogger Passwörter gibt)
6. Exchange Split Permissions
7. Backup ohne Domänenmitgliedschaft, Firewall so dass Nur Backup zum Server kann aber nicht andersrum

### Grundkonzept

Das unsicherste ist für den Anwender das bequemste, und demnach auch das vom Anwender bevorzugte.  
Alle Sicherungsmaßnahmen machen den eigentlichen Umgang mit dem System unbequemer.  
Daher ist die ideale Sicherheit ein Kompromiss aus Sicherung und Bequemlichkeit.

### Domänenabsicherung

1. Fullcloud-Entra / Exchange Online ohne Hybridbetrieb erreichen.
2. Zentrales User-Management via Microsoft Entra
3. Single-Sign-On aus Entra-Daten (weil: Psychologie: Müssen viele Accounts gepflegt werden, wird das gleiche Passwort überall verwendet)
4. Multifaktor-Authentifizierung mit Hardwarekeys (idealerweise via Biometrie) für alle Microsoft Entra Accounts.  
    \- Alternativ: Multifaktor mit Windows 11 &amp; TPM-Chip der Mainboards.

Danach kommen Dinge wie Patchmanagement etc.

Der Angreifer ist der, der beim unbedarften Anwender anruft, weil er einen Schriftverkehr dazu aus der vorne an der Straße stehenden Papiertonne gefischt hat.

# DNS over HTTPS in Windows

Leider nicht ganz Klickibunti hier. Nehmen wir als Beispiel mal [DNSforge](https://dnsforge.de/):

```Powershell
netsh dns add encryption server=176.9.93.198 dohtemplate=https://dnsforge.de/dns-query
netsh dns add encryption server=176.9.1.117 dohtemplate=https://dnsforge.de/dns-query
netsh dns add encryption server=2a01:4f8:151:34aa::198 dohtemplate=https://dnsforge.de/dns-query
netsh dns add encryption server=2a01:4f8:141:316d::117 dohtemplate=https://dnsforge.de/dns-query
Get-ChildItem -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters\DohWellKnownServers'
pause
exit
```

Danach in den normalen Ethernet Einstellungen entsprechend auf Manuell umstellen. Dann ist auch die Verschlüsselung auswählbar.  
Ab Werk gibts Google, Quad9 und Cloudflare integriert, wie man am Registry Key sehen kann.

[Halb geklaut von ZDnet](https://www.zdnet.de/88388444/windows-10-doh-bei-build-20236-aktivieren/), die haben im Artikel aber IPv6 übersehen.

# Gratis HEVC Codecs (für .heic)

<a>hier klicken...</a>

Ist ein OEM Setup Paket. Für einfache Handhabung muss man aber 99 Cent hinlegen, das OEM Paket ist natürlich nicht im Suchindex, wenn Microsoft damit schon Geld machen kann...

# Scripts

# Firewall Export to CSV

```powershell
import csv

txt_file = 'input.txt'
csv_file = 'output.csv'

with open(txt_file, 'r') as file:
    lines = file.readlines()

# Entferne doppelte Leerzeichen zwischen den Spaltenwerten
lines = [line.replace('  ', ' ') for line in lines]

data = [line.strip().split('\t') for line in lines]

with open(csv_file, 'w', newline='') as file:
    writer = csv.writer(file, delimiter=',')
    writer.writerows(data)

print('Die TXT-Datei wurde erfolgreich in eine CSV-Datei umgewandelt.')
```

**Vorsicht:** [Nur ein schneller ChatGPT Vorgang](https://chat.openai.com/share/897ab652-0a8b-4281-bbbe-0a6dac50af7c) und kurz an Beispieldaten getestet. **Testet selbst, bevor Ihr das auf Produktivdaten loslässt!**

# PYTHON Bilder-Stapelverarbeitung

<p class="callout warning">Kurze Vorwarnung dass dies hier überwiegend KI-generiertes Zeug ist. Hat funktioniert, muss aber nicht erneut funktionieren. Es ist wichtig dass ihr in Grundzügen nachvollziehen könnt, was die Scripts tun, auch wenn sie generiert sind.  
Pastet diese Scripts z.B. in eine KI rein und lasst sie euch erklären. Easy as that... Nur kurz schildern wer ihr seid und was ihr beruflich macht und warum ihr euch jetzt unbedingt das Script anschauen müsst.  
  
Generatoren: ChatGPT, Claude.ai, manchmal Llama, je nachdem was so passte.  
</p>

Umgang mit auch größeren Anzahlen an Bildern. Teils getestet an einem ganzen Terabyte Bildern.

### Sortierung nach Bildgröße  


<details data-tc-id="w-0.2806250612273664" id="bkmrk-fest-definierte-gr%C3%B6%C3%9F" tc-textcontent="true"><summary>Fest definierte Größe</summary>

ChatGPT 3.5 als Hilfestellung

Skript sammelt Bilder die kleiner als festgelegte Auflösung sind in einem anderen Ordner und belässt die, die größer sind im Ordner.

Das Beispiel hier trennt Bilder &gt;4K von denen die es nicht sind.

```powershell
param (
	[string]$SourceFolder = "C:\Path\to\SourceFolder",
    [string]$DestinationFolder = "C:\Path\to\DestinationFolder",
    [int]$MinWidth = 3840,
    [int]$MinHeight = 2160
)

# Create the destination folder if it doesn't exist
if (-not (Test-Path $DestinationFolder)) {
    New-Item -ItemType Directory -Path $DestinationFolder | Out-Null
}

# Get all image files in the source folder and its subfolders
$images = Get-ChildItem -Path $SourceFolder -Filter "*.jpg" -File -Recurse

$totalImages = $images.Count
$processedImages = 0

# Process each image file
foreach ($image in $images) {
    Write-Host "Processing $($image.FullName)"

    # Use .NET classes to read image dimensions
    $imageStream = New-Object System.IO.FileStream($image.FullName, [System.IO.FileMode]::Open)
    $imageBitmap = New-Object System.Drawing.Bitmap($imageStream)
    $width = $imageBitmap.Width
    $height = $imageBitmap.Height
    $imageStream.Close()

    # Check if the image is smaller than the specified dimensions
    if ($width -lt $MinWidth -or $height -lt $MinHeight) {
        $destinationPath = Join-Path -Path $DestinationFolder -ChildPath $image.Name
        Write-Host "Moving $($image.FullName) to $destinationPath"
        Move-Item -Path $image.FullName -Destination $destinationPath
    }

    $processedImages++
    $progress = [math]::Round(($processedImages / $totalImages) * 100, 2)
    Write-Progress -Activity "Moving images" -Status "Progress: $progress%" -PercentComplete $progress
}

Write-Progress -Activity "Moving images" -Status "Progress: 100%" -PercentComplete 100
Write-Host "Image move complete!"

```

</details><details data-tc-id="w-0.06695377130188063" id="bkmrk-auswahldialoge-f%C3%BCr-a" tc-textcontent="true"><summary>Auswahldialoge für Auflösung und Ordner</summary>

Claude 3.5 Sonnet

1. Mindestauflösung Höhe und Breite in Pixeln auswählen
2. Ordnerdialoge für Quell- und Zielordner wählen
3. Alle Bilder, die die Mindestauflösung haben oder überschreiten, werden in den gewählten Zielordner verschoben

Fehler sind in ./error\_log.txt

```powershell
Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName System.Windows.Forms

# Get the directory of the script
$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
$logFile = Join-Path $scriptPath "error_log.txt"

# Function to log errors
function Log-Error {
    param (
        [string]$message
    )
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    "$timestamp - $message" | Out-File -FilePath $logFile -Append
}

# Function to show folder selection dialog
function Select-Folder {
    param (
        [string]$Description
    )
    $folderBrowser = New-Object System.Windows.Forms.FolderBrowserDialog
    $folderBrowser.Description = $Description
    $folderBrowser.RootFolder = [System.Environment+SpecialFolder]::MyComputer
    if ($folderBrowser.ShowDialog() -eq "OK") {
        return $folderBrowser.SelectedPath
    }
    return $null
}

# Prompt user for resolution
do {
    $targetWidth = Read-Host "Enter the target width in pixels (e.g., 3840 for 4K)"
} while (-not ($targetWidth -match '^\d+$'))

do {
    $targetHeight = Read-Host "Enter the target height in pixels (e.g., 2160 for 4K)"
} while (-not ($targetHeight -match '^\d+$'))

$targetWidth = [int]$targetWidth
$targetHeight = [int]$targetHeight

Write-Host "Target resolution: $targetWidth x $targetHeight"

# Prompt user to select source and destination folders
$sourceFolder = Select-Folder "Select the source folder containing images"
if (-not $sourceFolder) {
    Write-Host "Source folder selection cancelled. Exiting script."
    exit
}

$destinationFolder = Select-Folder "Select the destination folder for matching images"
if (-not $destinationFolder) {
    Write-Host "Destination folder selection cancelled. Exiting script."
    exit
}

# Create destination folder if it doesn't exist
if (!(Test-Path -Path $destinationFolder)) {
    New-Item -ItemType Directory -Path $destinationFolder | Out-Null
    Write-Host "Created destination folder: $destinationFolder"
}

# Get all image files in the source folder
$imageFiles = Get-ChildItem -Path $sourceFolder -Include *.jpg, *.jpeg, *.png, *.bmp -File -Recurse

if ($imageFiles.Count -eq 0) {
    $message = "No image files found in $sourceFolder"
    Write-Host $message
    Log-Error $message
    exit
}

Write-Host "Found $($imageFiles.Count) image files to process"

# Initialize counter and total
$i = 0
$total = $imageFiles.Count
$movedCount = 0

# Initialize progress bar
Write-Progress -Activity "Processing Images" -Status "0% Complete" -PercentComplete 0

foreach ($file in $imageFiles) {
    try {
        $image = $null
        try {
            $image = [System.Drawing.Image]::FromFile($file.FullName)
            $width = $image.Width
            $height = $image.Height
            
            # Check if image meets or exceeds target resolution
            if ($width -ge $targetWidth -and $height -ge $targetHeight) {
                # Close the image before moving
                $image.Dispose()
                $image = $null
                
                # Move the file to the destination folder
                Move-Item -Path $file.FullName -Destination $destinationFolder -Force
                Write-Host "Moved: $($file.Name) (${width}x${height})"
                $movedCount++
            }
        }
        finally {
            # Ensure image is disposed even if an error occurs
            if ($image -ne $null) {
                $image.Dispose()
            }
        }
    }
    catch {
        $errorMessage = "Error processing $($file.Name): $_"
        Write-Host $errorMessage
        Log-Error $errorMessage
    }
    
    # Update progress
    $i++
    $percentComplete = ($i / $total) * 100
    Write-Progress -Activity "Processing Images" -Status "$i of $total processed" -PercentComplete $percentComplete
}

Write-Progress -Activity "Processing Images" -Completed
Write-Host "Processing complete. Moved $movedCount out of $total images."
```

<details data-tc-id="w-0.15204833499055403" tc-textcontent="true"><summary>ungetestete BETA</summary>

Mit Claude Sonnet erweitert. Hinzu kommen Abfragen nach Ordnern und Auflösung, Erkennung der Bildschirmauflösung etc.

<p class="callout danger">ungetesteter direkter AI output</p>

```powershell
Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName System.Windows.Forms

# Get the directory of the script
$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
$logFile = Join-Path $scriptPath "error_log.txt"

# Function to log errors
function Log-Error {
    param (
        [string]$message
    )
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    "$timestamp - $message" | Out-File -FilePath $logFile -Append
}

# Function to show folder selection dialog
function Select-Folder {
    param (
        [string]$Description
    )
    $folderBrowser = New-Object System.Windows.Forms.FolderBrowserDialog
    $folderBrowser.Description = $Description
    $folderBrowser.RootFolder = [System.Environment+SpecialFolder]::MyComputer
    if ($folderBrowser.ShowDialog() -eq "OK") {
        return $folderBrowser.SelectedPath
    }
    return $null
}

# Get current screen resolution
$currentResolution = (Get-WmiObject -Class Win32_VideoController).VideoModeDescription
$resolutionMatch = $currentResolution -match '(\d+)\s*x\s*(\d+)'
if ($resolutionMatch) {
    $currentWidth = [int]$Matches[1]
    $currentHeight = [int]$Matches[2]
    Write-Host "Current screen resolution: $currentWidth x $currentHeight"
    
    $useCurrentResolution = Read-Host "Do you want to use the current screen resolution? (Y/N)"
    if ($useCurrentResolution -eq 'Y' -or $useCurrentResolution -eq 'y') {
        $targetWidth = $currentWidth
        $targetHeight = $currentHeight
    }
}

# If not using current resolution, prompt user for resolution
if (-not $targetWidth -or -not $targetHeight) {
    do {
        $targetWidth = Read-Host "Enter the target width in pixels (e.g., 3840 for 4K)"
    } while (-not ($targetWidth -match '^\d+$'))

    do {
        $targetHeight = Read-Host "Enter the target height in pixels (e.g., 2160 for 4K)"
    } while (-not ($targetHeight -match '^\d+$'))

    $targetWidth = [int]$targetWidth
    $targetHeight = [int]$targetHeight
}

Write-Host "Target resolution: $targetWidth x $targetHeight"

# Prompt user to select source and destination folders
$sourceFolder = Select-Folder "Select the source folder containing images"
if (-not $sourceFolder) {
    Write-Host "Source folder selection cancelled. Exiting script."
    exit
}

$destinationFolder = Select-Folder "Select the destination folder for matching images"
if (-not $destinationFolder) {
    Write-Host "Destination folder selection cancelled. Exiting script."
    exit
}

# Create destination folder if it doesn't exist
if (!(Test-Path -Path $destinationFolder)) {
    New-Item -ItemType Directory -Path $destinationFolder | Out-Null
    Write-Host "Created destination folder: $destinationFolder"
}

# Get all image files in the source folder
$imageFiles = Get-ChildItem -Path $sourceFolder -Include *.jpg, *.jpeg, *.png, *.bmp -File -Recurse

if ($imageFiles.Count -eq 0) {
    $message = "No image files found in $sourceFolder"
    Write-Host $message
    Log-Error $message
    exit
}

Write-Host "Found $($imageFiles.Count) image files to process"

# Initialize counter and total
$i = 0
$total = $imageFiles.Count
$movedCount = 0

# Initialize progress bar
Write-Progress -Activity "Processing Images" -Status "0% Complete" -PercentComplete 0

foreach ($file in $imageFiles) {
    try {
        $image = $null
        try {
            $image = [System.Drawing.Image]::FromFile($file.FullName)
            $width = $image.Width
            $height = $image.Height
            
            # Check if image meets or exceeds target resolution
            if ($width -ge $targetWidth -and $height -ge $targetHeight) {
                # Close the image before moving
                $image.Dispose()
                $image = $null
                
                # Move the file to the destination folder
                Move-Item -Path $file.FullName -Destination $destinationFolder -Force
                Write-Host "Moved: $($file.Name) (${width}x${height})"
                $movedCount++
            }
        }
        finally {
            # Ensure image is disposed even if an error occurs
            if ($image -ne $null) {
                $image.Dispose()
            }
        }
    }
    catch {
        $errorMessage = "Error processing $($file.Name): $_"
        Write-Host $errorMessage
        Log-Error $errorMessage
    }
    
    # Update progress
    $i++
    $percentComplete = ($i / $total) * 100
    Write-Progress -Activity "Processing Images" -Status "$i of $total processed" -PercentComplete $percentComplete
}

Write-Progress -Activity "Processing Images" -Completed
Write-Host "Processing complete. Moved $movedCount out of $total images."
```

</details></details><details data-tc-id="w-0.5149495937437251" id="bkmrk-bilder-%3C4k-verschieb" tc-textcontent="true"><summary>Bilder &lt;4K verschieben</summary>

<p class="callout warning">PYTHON, kein Powershell</p>

pip install Pillow tqdm

```python
import os
from PIL import Image
import shutil
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
from tqdm import tqdm
# Import and disable the DecompressionBombWarning
from PIL import Image, ImageFile
Image.MAX_IMAGE_PIXELS = None  # Disable the warning
ImageFile.LOAD_TRUNCATED_IMAGES = True  # Handle truncated images

def get_image_resolution(image_path):
    """Get the resolution of an image file."""
    try:
        with Image.open(image_path) as img:
            return img.size
    except Exception as e:
        print(f"Error reading {image_path}: {str(e)}")
        return None

def is_4k_or_higher(width, height):
    """Check if the resolution is 4K (3840x2160) or higher."""
    return width >= 3840 and height >= 2160

def process_image(image_path, destination_folder):
    """Process a single image and move it if not 4K."""
    try:
        resolution = get_image_resolution(image_path)
        if resolution is None:
            return
        
        width, height = resolution
        if not is_4k_or_higher(width, height):
            # Create destination folder if it doesn't exist
            Path(destination_folder).mkdir(parents=True, exist_ok=True)
            
            # Get destination path
            dest_path = os.path.join(destination_folder, os.path.basename(image_path))
            
            # Move file, overwriting if it exists
            shutil.move(image_path, dest_path)
            
    except Exception as e:
        print(f"Error processing {image_path}: {str(e)}")

def main():
    # Configuration
    source_folder = input("Enter the source folder path: ")
    destination_folder = input("Enter the destination folder for non-4K images: ")
    
    # Validate source folder
    if not os.path.exists(source_folder):
        print("Source folder does not exist!")
        return
    
    # Get list of image files
    image_files = [
        os.path.join(source_folder, f) 
        for f in os.listdir(source_folder) 
        if f.lower().endswith(('.png', '.jpg', '.jpeg'))
    ]
    
    if not image_files:
        print("No image files found in the source folder!")
        return
    
    print(f"Found {len(image_files)} images. Processing...")
    
    # Process images using thread pool for better performance
    with ThreadPoolExecutor(max_workers=8) as executor:
        list(tqdm(
            executor.map(
                lambda x: process_image(x, destination_folder),
                image_files
            ),
            total=len(image_files),
            desc="Processing images"
        ))
    
    print("Done! All non-4K images have been moved.")

if __name__ == "__main__":
    main()
```

</details><details data-tc-id="w-0.7238711130045015" id="bkmrk-bilder-%3E%3D-4k-verschi" tc-textcontent="true"><summary>Bilder &gt;= 4K verschieben</summary>

PYTHON   
pip install Pillow tqdm

```python
import os
from PIL import Image
import shutil
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
from tqdm import tqdm
import warnings
import logging
import sys
from datetime import datetime
# Import and disable the DecompressionBombWarning
from PIL import Image, ImageFile
Image.MAX_IMAGE_PIXELS = None  # Disable the warning
ImageFile.LOAD_TRUNCATED_IMAGES = True  # Handle truncated images

# Get the script's filename without extension to create log filename
script_path = sys.argv[0]
script_name = os.path.splitext(os.path.basename(script_path))[0]
log_filename = f"{script_name}_errors.log"

# Setup logging
logging.basicConfig(
    filename=log_filename,
    level=logging.WARNING,
    format='%(asctime)s - %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S'
)

# Custom warning handler to redirect warnings to logging
def handle_warning(message, category, filename, lineno, file=None, line=None):
    logging.warning(f"Warning: {message}")

# Replace the default warning handler
warnings.showwarning = handle_warning

def get_image_resolution(image_path):
    """Get the resolution of an image file."""
    try:
        with Image.open(image_path) as img:
            return img.size
    except Exception as e:
        logging.error(f"Error reading image {image_path}: {str(e)}")
        return None

def is_4k_or_higher(width, height):
    """Check if the resolution is 4K (3840x2160) or higher."""
    return width >= 3840 and height >= 2160

def process_image(image_path, destination_folder):
    """Process a single image and move it if it IS 4K or higher."""
    try:
        resolution = get_image_resolution(image_path)
        if resolution is None:
            return
        
        width, height = resolution
        if is_4k_or_higher(width, height):
            # Create destination folder if it doesn't exist
            Path(destination_folder).mkdir(parents=True, exist_ok=True)
            
            # Get destination path
            dest_path = os.path.join(destination_folder, os.path.basename(image_path))
            
            # Move file, overwriting if it exists
            shutil.move(image_path, dest_path)
            
    except Exception as e:
        logging.error(f"Error processing {image_path}: {str(e)}")

def main():
    # Log script start
    logging.info(f"Script started at {datetime.now()}")
    
    # Configuration
    source_folder = input("Enter the source folder path: ")
    destination_folder = input("Enter the destination folder for 4K and higher resolution images: ")
    
    # Log folders
    logging.info(f"Source folder: {source_folder}")
    logging.info(f"Destination folder: {destination_folder}")
    
    # Validate source folder
    if not os.path.exists(source_folder):
        logging.error("Source folder does not exist!")
        print("Source folder does not exist!")
        return
    
    # Get list of image files
    image_files = [
        os.path.join(source_folder, f) 
        for f in os.listdir(source_folder) 
        if f.lower().endswith(('.png', '.jpg', '.jpeg'))
    ]
    
    if not image_files:
        logging.warning("No image files found in the source folder!")
        print("No image files found in the source folder!")
        return
    
    print(f"Found {len(image_files)} images. Processing...")
    logging.info(f"Found {len(image_files)} images to process")
    
    # Process images using thread pool for better performance
    with ThreadPoolExecutor(max_workers=8) as executor:
        list(tqdm(
            executor.map(
                lambda x: process_image(x, destination_folder),
                image_files
            ),
            total=len(image_files),
            desc="Processing images"
        ))
    
    logging.info(f"Script completed at {datetime.now()}")
    print("Done! All 4K and higher resolution images have been moved.")
    print(f"Check {log_filename} for any errors that occurred during processing.")

if __name__ == "__main__":
    main()
```

</details><details data-tc-id="w-0.12132587792364447" id="bkmrk-nach-seitenverh%C3%A4ltni" tc-textcontent="true"><summary>nach Seitenverhältnis sortieren</summary>

PYTHON

```python
import os
import tkinter as tk
from tkinter import filedialog
from PIL import Image, ImageFile
from tqdm import tqdm
import re
import shutil

# Disable decompression bomb warning and increase file reading limit
ImageFile.LOAD_TRUNCATED_IMAGES = True
Image.MAX_IMAGE_PIXELS = None

# Define common aspect ratios with their tolerance
COMMON_ASPECT_RATIOS = {
    # Widescreen and Ultra-wide
    'widescreen_16_9': (16/9, 0.02),     # Standard widescreen
    'ultrawide_21_9': (21/9, 0.02),      # Ultra-wide
    'superwide_32_9': (32/9, 0.02),      # Super ultra-wide

    # Portrait Ratios
    'portrait_9_16': (9/16, 0.02),       # Vertical smartphone
    'portrait_10_16': (10/16, 0.02),     # Alternative portrait

    # Square and near-square
    'square_1_1': (1, 0.02),             # Perfect square
    'classic_4_3': (4/3, 0.02),          # Classic 4:3
    'classic_3_2': (3/2, 0.02),          # Classic film/print
    'large_format_5_4': (5/4, 0.02),     # Large format photography

    # Other common ratios
    'widescreen_16_10': (16/10, 0.02),   # Slightly wider widescreen
}

def sanitize_path(path):
    """
    Sanitize path to be safe for Windows file system
    """
    # Replace invalid path characters
    sanitized = re.sub(r'[<>:"/\\|?*]', '_', path)
    
    # Ensure path doesn't end with a dot or space
    sanitized = sanitized.rstrip('. ')
    
    return sanitized

def get_aspect_ratio_name(aspect_ratio):
    """
    Find the closest matching aspect ratio name
    """
    for name, (target_ratio, tolerance) in COMMON_ASPECT_RATIOS.items():
        if abs(aspect_ratio - target_ratio) < tolerance:
            return name
    return 'other_aspect_ratio'

def process_images():
    # Create root window (will be hidden)
    root = tk.Tk()
    root.withdraw()

    # Ask for input folder
    input_folder = filedialog.askdirectory(title="Select Input Folder with Images")
    if not input_folder:
        print("No input folder selected. Exiting.")
        return

    # Get list of all files first
    all_files = []
    for root, dirs, files in os.walk(input_folder):
        for file in files:
            # Check for image extensions
            if file.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.webp')):
                all_files.append(os.path.join(root, file))

    # Tracking variables
    sorted_count = 0
    error_count = 0
    ratio_counts = {}

    # Use tqdm for progress bar
    for input_path in tqdm(all_files, desc="Sorting Images", unit="image"):
        try:
            # Open image and immediately close it after extracting info
            img = Image.open(input_path)
            width, height = img.size
            
            # Explicitly close the image file
            img.close()
            
            # Calculate aspect ratio
            aspect_ratio = width / height
            ratio_name = get_aspect_ratio_name(aspect_ratio)
            
            # Create subfolder if it doesn't exist
            output_subfolder = os.path.join(input_folder, sanitize_path(ratio_name))
            os.makedirs(output_subfolder, exist_ok=True)
            
            # Sanitize filename and create output path
            filename = os.path.basename(input_path)
            sanitized_filename = sanitize_path(filename)
            output_path = os.path.join(output_subfolder, sanitized_filename)
            
            # Move the file
            shutil.move(input_path, output_path)
            
            # Update tracking
            sorted_count += 1
            ratio_counts[ratio_name] = ratio_counts.get(ratio_name, 0) + 1

        except Exception as e:
            tqdm.write(f"Error processing {input_path}: {e}")
            error_count += 1

    # Final report
    print(f"\nTotal images sorted: {sorted_count}")
    print("Sorting breakdown:")
    for ratio, count in sorted(ratio_counts.items(), key=lambda x: x[1], reverse=True):
        print(f"  {ratio}: {count} images")
    print(f"Errors encountered: {error_count}")

if __name__ == "__main__":
    process_images()
```

</details>### Zuschneidung

<details data-tc-id="w-0.7604293599160628" id="bkmrk-auf-seitenverh%C3%A4ltnis" tc-textcontent="true"><summary>Auf Seitenverhältnis zuschneiden</summary>

Bild auswählen, Fokuspunkt setzen, Ziel-Seitenverhältnis eingeben, speichern...

Nicht mehr komplett KI, ich hab da schon Anpassungen drin...

pip install pillow numpy scipy

```python
import tkinter as tk
from tkinter import filedialog, messagebox
from PIL import Image, ImageTk
import numpy as np
from scipy.ndimage import gaussian_filter
import logging

# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def generate_seamless_fill(image, target_width, target_height, focus_x=0.5, focus_y=0.5):
    """
    Fits an image into a new aspect ratio using content-aware fill with focus point.
    
    Parameters:
        focus_x, focus_y: Float between 0 and 1 indicating the focus point position
    """
    logger.info("Starting seamless fill generation...")
    logger.info(f"Focus point: ({focus_x}, {focus_y})")
    
    # Convert to numpy array
    img_array = np.array(image)
    logger.info(f"Converting image to array of shape {img_array.shape}")
    
    # Create the new canvas
    if len(img_array.shape) == 3:
        result = np.zeros((target_height, target_width, img_array.shape[2]), dtype=np.uint8)
        logger.info("Created RGB canvas")
    else:
        result = np.zeros((target_height, target_width), dtype=np.uint8)
        logger.info("Created grayscale canvas")
    
    # Calculate crop dimensions if needed
    src_height, src_width = img_array.shape[:2]
    height_diff = src_height - target_height
    width_diff = src_width - target_width
    
    logger.info(f"Height difference: {height_diff}, Width difference: {width_diff}")
    
    # Calculate source and destination regions using focus point
    if height_diff > 0:  # Need to crop height
        # Calculate crop position based on focus point
        focus_pixel_y = int(src_height * focus_y)
        half_target = target_height // 2
        src_y_start = max(0, min(src_height - target_height, focus_pixel_y - half_target))
        src_y_end = src_y_start + target_height
        dst_y_start = 0
        dst_y_end = target_height
        logger.info(f"Cropping height: {src_y_start}:{src_y_end} -> {dst_y_start}:{dst_y_end}")
    else:  # Need to pad height
        src_y_start = 0
        src_y_end = src_height
        dst_y_start = abs(height_diff) // 2
        dst_y_end = dst_y_start + src_height
        logger.info(f"Padding height: {src_y_start}:{src_y_end} -> {dst_y_start}:{dst_y_end}")
    
    if width_diff > 0:  # Need to crop width
        # Calculate crop position based on focus point
        focus_pixel_x = int(src_width * focus_x)
        half_target = target_width // 2
        src_x_start = max(0, min(src_width - target_width, focus_pixel_x - half_target))
        src_x_end = src_x_start + target_width
        dst_x_start = 0
        dst_x_end = target_width
        logger.info(f"Cropping width: {src_x_start}:{src_x_end} -> {dst_x_start}:{dst_x_end}")
    else:  # Need to pad width
        src_x_start = 0
        src_x_end = src_width
        dst_x_start = abs(width_diff) // 2
        dst_x_end = dst_x_start + src_width
        logger.info(f"Padding width: {src_x_start}:{src_x_end} -> {dst_x_start}:{dst_x_end}")
    
    # Copy the appropriate region of the source image to the destination
    result[dst_y_start:dst_y_end, dst_x_start:dst_x_end] = img_array[src_y_start:src_y_end, src_x_start:src_x_end]
    
    # If we need to fill any borders
    if height_diff < 0 or width_diff < 0:
        logger.info("Generating content-aware fill for borders...")
        mask = np.zeros_like(result, dtype=bool)
        mask[result.sum(axis=2 if len(result.shape) == 3 else -1) == 0] = True
        
        if len(result.shape) == 3:
            for channel in range(result.shape[2]):
                logger.info(f"Processing channel {channel}")
                temp = result[:, :, channel].copy()
                edge_pixels = []
                for i in range(result.shape[0]):
                    for j in range(result.shape[1]):
                        if mask[i, j] and not np.all(mask[max(0, i-1):i+2, max(0, j-1):j+2]):
                            edge_pixels.append((i, j))
                
                logger.info(f"Found {len(edge_pixels)} edge pixels to process")
                for i, j in edge_pixels:
                    neighborhood = temp[max(0, i-2):i+3, max(0, j-2):j+3]
                    valid_pixels = neighborhood[~mask[max(0, i-2):i+3, max(0, j-2):j+3]]
                    if len(valid_pixels) > 0:
                        temp[i, j] = np.mean(valid_pixels)
                
                logger.info("Applying gaussian blur for smooth transitions")
                temp_filled = gaussian_filter(temp, sigma=2)
                result[:, :, channel][mask[:, :]] = temp_filled[mask[:, :]]
    
    logger.info("Seamless fill generation complete")
    return Image.fromarray(result)

class AspectRatioChanger:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("Image Aspect Ratio Changer")
        self.root.geometry("1920x1080")  # This would set the initial window size
        self.focus_point = (0.5, 0.5)  # Default center focus
        self.setup_ui()
        
    def setup_ui(self):
        # Main container
        main_frame = tk.Frame(self.root)
        main_frame.pack(padx=10, pady=10, expand=True, fill=tk.BOTH)
        
        # Left panel for controls
        controls_frame = tk.Frame(main_frame)
        controls_frame.pack(side=tk.LEFT, padx=10, fill=tk.Y)
        
        # File selection
        tk.Button(controls_frame, text="Select Image", command=self.select_image).pack(pady=10)
        
        # Aspect ratio frame
        ratio_frame = tk.Frame(controls_frame)
        ratio_frame.pack(pady=10)
        
        tk.Label(ratio_frame, text="Width ratio:").pack(side=tk.LEFT)
        self.width_entry = tk.Entry(ratio_frame, width=10)
        self.width_entry.pack(side=tk.LEFT, padx=5)
        
        tk.Label(ratio_frame, text="Height ratio:").pack(side=tk.LEFT)
        self.height_entry = tk.Entry(ratio_frame, width=10)
        self.height_entry.pack(side=tk.LEFT, padx=5)
        
        # Common aspect ratios
        self.add_preset_buttons(controls_frame)
        
        # Process button
        tk.Button(controls_frame, text="Process Image", command=self.process_image).pack(pady=10)
        
        # Focus point instructions
        tk.Label(controls_frame, 
                 text="Click on the image to set center point for cropping!",
                 wraplength=200,
                 font=('TkDefaultFont', 10, 'bold'),  # Makes the text bold
                 fg='red'  # Makes the text red
        ).pack(pady=10)
        
        # Add Reset Focus Point button
        tk.Button(controls_frame, 
                  text="Reset Focus Point", 
                  command=self.reset_focus_point
        ).pack(pady=5)
        
        # Right panel for image preview
        self.preview_frame = tk.Frame(main_frame, width=800, height=600)
        self.preview_frame.pack(side=tk.LEFT, padx=10, expand=True, fill=tk.BOTH)
        
        self.preview_canvas = tk.Canvas(self.preview_frame, bg='gray')
        self.preview_canvas.pack(expand=True, fill=tk.BOTH)
        self.preview_canvas.bind("<Button-1>", self.set_focus_point)
        
    def reset_focus_point(self):
        self.focus_point = (0.5, 0.5)  # Reset to center
    
        if hasattr(self, 'preview_image'):  # Only redraw if we have an image
            # Get canvas dimensions
            canvas_width = self.preview_canvas.winfo_width()
            canvas_height = self.preview_canvas.winfo_height()
            
            # Calculate center position
            center_x = canvas_width // 2
            center_y = canvas_height // 2
            
            # Redraw focus point at center
            self.draw_focus_point(center_x, center_y)    
        
    def add_preset_buttons(self, parent):
        presets_frame = tk.Frame(parent)
        presets_frame.pack(pady=10)
        
        presets_row1 = {
            "16:9 (PC)": (16, 9),
            "16:10": (16, 10),
            "4:3 (Tablet)": (4, 3),
        }
        
        presets_row2 = {
            "1:1 (Social Media)": (1, 1),
            "9:16 (Smartphone)": (9, 16),
            "1.44:1 (iPad 2022)": (144, 100)
        }
        
        tk.Label(presets_frame, text="Common Ratios:").pack()
        
        # First row of buttons
        buttons_frame1 = tk.Frame(presets_frame)
        buttons_frame1.pack(pady=(5, 2))
        
        for name, (w, h) in presets_row1.items():
            tk.Button(buttons_frame1, text=name, 
                     command=lambda w=w, h=h: self.set_aspect_ratio(w, h)).pack(side=tk.LEFT, padx=2)
        
        # Second row of buttons
        buttons_frame2 = tk.Frame(presets_frame)
        buttons_frame2.pack(pady=(2, 5))
        
        for name, (w, h) in presets_row2.items():
            tk.Button(buttons_frame2, text=name, 
                     command=lambda w=w, h=h: self.set_aspect_ratio(w, h)).pack(side=tk.LEFT, padx=2)

    
    def set_aspect_ratio(self, width, height):
        self.width_entry.delete(0, tk.END)
        self.height_entry.delete(0, tk.END)
        self.width_entry.insert(0, str(width))
        self.height_entry.insert(0, str(height))
    
    def select_image(self):
        self.image_path = filedialog.askopenfilename(
            filetypes=[("Image files", "*.png *.jpg *.jpeg *.gif *.bmp")]
        )
        if self.image_path:
            logger.info(f"Selected image: {self.image_path}")
            self.load_preview_image()
    
    def load_preview_image(self):
        # Load and resize image for preview
        image = Image.open(self.image_path)
        
        # Calculate resize dimensions to fit canvas
        canvas_width = self.preview_frame.winfo_width()
        canvas_height = self.preview_frame.winfo_height()
        
        # Resize image maintaining aspect ratio
        image.thumbnail((canvas_width, canvas_height), Image.Resampling.LANCZOS)
        
        # Store original image dimensions for focus point calculation
        self.original_size = Image.open(self.image_path).size
        self.preview_size = image.size
        
        # Create PhotoImage and store reference
        self.preview_image = ImageTk.PhotoImage(image)
        
        # Clear canvas and draw new image
        self.preview_canvas.delete("all")
        self.preview_canvas.create_image(
            canvas_width//2, canvas_height//2,
            image=self.preview_image,
            anchor=tk.CENTER,
            tags="preview"
        )
        
        # Draw initial focus point
        self.draw_focus_point(canvas_width//2, canvas_height//2)
    
    def set_focus_point(self, event):
        if not hasattr(self, 'preview_image'):
            return
            
        # Get canvas dimensions
        canvas_width = self.preview_canvas.winfo_width()
        canvas_height = self.preview_canvas.winfo_height()
        
        # Calculate image position and size on canvas
        img_width, img_height = self.preview_size
        x_offset = (canvas_width - img_width) // 2
        y_offset = (canvas_height - img_height) // 2
        
        # Convert click position to image coordinates
        x = event.x - x_offset
        y = event.y - y_offset
        
        # Check if click is within image bounds
        if 0 <= x < img_width and 0 <= y < img_height:
            # Calculate relative position (0-1)
            self.focus_point = (x / img_width, y / img_height)
            logger.info(f"Focus point set to: {self.focus_point}")
            
            # Update focus point marker
            self.draw_focus_point(event.x, event.y)
    
    def draw_focus_point(self, x, y):
        # Clear previous focus point
        self.preview_canvas.delete("focus_point")
        
        # Draw new focus point
        radius = 5
        self.preview_canvas.create_oval(
            x - radius, y - radius,
            x + radius, y + radius,
            fill='red',
            tags="focus_point"
        )
        
        # Draw crosshair
        size = 10
        self.preview_canvas.create_line(
            x - size, y, x + size, y,
            fill='red',
            tags="focus_point"
        )
        self.preview_canvas.create_line(
            x, y - size, x, y + size,
            fill='red',
            tags="focus_point"
        )
    
    def process_image(self):
        if not hasattr(self, 'image_path'):
            messagebox.showerror("Error", "Please select an image first")
            return
            
        try:
            target_width = int(self.width_entry.get())
            target_height = int(self.height_entry.get())
            
            if target_width <= 0 or target_height <= 0:
                raise ValueError("Dimensions must be positive")
                
        except ValueError as e:
            messagebox.showerror("Error", "Please enter valid positive numbers for width and height")
            return
            
        # Load and process image
        try:
            image = Image.open(self.image_path)
            # Calculate new dimensions while maintaining aspect ratio
            orig_width, orig_height = image.size
            scale = min(orig_width / target_width, orig_height / target_height)
            new_width = int(target_width * scale)
            new_height = int(target_height * scale)
            
            # Process the image with focus point
            processed_image = generate_seamless_fill(
                image, new_width, new_height,
                focus_x=self.focus_point[0],
                focus_y=self.focus_point[1]
            )
            
            # Save the processed image
            save_path = filedialog.asksaveasfilename(
                defaultextension=".png",
                filetypes=[("PNG files", "*.png"), ("All files", "*.*")]
            )
            if save_path:
                processed_image.save(save_path)
                messagebox.showinfo("Success", "Image processed and saved successfully!")
                
        except Exception as e:
            messagebox.showerror("Error", f"An error occurred: {str(e)}")
            logger.error(f"Error processing image: {e}", exc_info=True)

    def run(self):
        self.root.mainloop()

if __name__ == "__main__":
    app = AspectRatioChanger()
    app.run()
```

</details>### Upscaling

<details data-tc-id="w-0.6889278544409424" id="bkmrk-auf-4k-%28min-eine-sei" tc-textcontent="true"><summary>Lanczos auf 4K (min eine Seitenlänge)</summary>

Script skaliert unter Beibehaltung des Seitenverhältnisses die Bilder soweit hoch dass mindestens eine Seite die 4K-Auflösung matchen kann. Originale werden in Unterordner verschoben, die hochskalierten landen in dem Ordner den ihr angebt.

Lanczos ist nicht der beste Algorithmus dafür, aber brauchbar. Es kommt teilweise grobkörniges dabei raus, aber es ist ein passender Schnittpunkt aus Geschwindigkeit und Qualität.

Requirements:

Pillow -&gt; die Bildlibrary, ohne die geht nix mit Bildern  
tqdm -&gt; für eine Fortschrittsanzeige

```python
from PIL import Image
import os
from pathlib import Path
import sys
import shutil
from tqdm import tqdm

def upscale_to_4k(image_path, output_path):
    """
    Upscale an image to 4K resolution (3840×2160) while maintaining aspect ratio.
    At least one dimension will match 4K resolution.
    """
    try:
        # Open the image
        with Image.open(image_path) as img:
            # Get original dimensions
            width, height = img.size
            
            # Calculate aspect ratio
            aspect_ratio = width / height
            
            # Calculate new dimensions
            if aspect_ratio > 16/9:  # Wider than 4K aspect ratio
                new_height = 2160
                new_width = int(new_height * aspect_ratio)
            else:  # Taller than 4K aspect ratio
                new_width = 3840
                new_height = int(new_width / aspect_ratio)
            
            # Upscale image using Lanczos resampling
            upscaled_img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
            
            # Create output directory if it doesn't exist
            os.makedirs(os.path.dirname(output_path), exist_ok=True)
            
            # Save the upscaled image with original format
            upscaled_img.save(output_path, quality=95)
            return True
    except Exception as e:
        print(f"Error processing {image_path}: {str(e)}")
        return False

def move_to_originals(file_path):
    """
    Move the original file to an 'originals' subfolder in the source directory.
    """
    source_dir = file_path.parent
    originals_dir = source_dir / "originals"
    os.makedirs(originals_dir, exist_ok=True)
    
    # Create destination path
    dest_path = originals_dir / file_path.name
    
    # Handle file name conflicts
    counter = 1
    while dest_path.exists():
        stem = file_path.stem
        suffix = file_path.suffix
        dest_path = originals_dir / f"{stem}_{counter}{suffix}"
        counter += 1
    
    # Move the file
    shutil.move(str(file_path), str(dest_path))

def main():
    # Get input and output directories from user
    input_dir = input("Enter the path to the folder containing images: ").strip()
    output_dir = input("Enter the path where upscaled images should be saved: ").strip()
    
    # Validate input directory
    if not os.path.exists(input_dir):
        print("Error: Input directory does not exist!")
        sys.exit(1)
    
    # Create output directory if it doesn't exist
    os.makedirs(output_dir, exist_ok=True)
    
    # Supported image formats
    supported_formats = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff'}
    
    # Get list of all image files first
    image_files = [f for f in Path(input_dir).rglob('*') 
                  if f.suffix.lower() in supported_formats]
    
    if not image_files:
        print("No supported image files found in the input directory!")
        sys.exit(1)
    
    processed = 0
    failed = 0
    
    print("\nStarting image upscaling process...")
    
    # Process files with progress bar
    with tqdm(total=len(image_files), desc="Upscaling images", 
             unit="image", ncols=80) as pbar:
        for file_path in image_files:
            # Create corresponding output path
            relative_path = file_path.relative_to(input_dir)
            output_path = Path(output_dir) / relative_path
            
            if upscale_to_4k(str(file_path), str(output_path)):
                # Move original file to originals folder
                move_to_originals(file_path)
                processed += 1
            else:
                failed += 1
            
            pbar.update(1)
    
    # Print summary
    print(f"\nUpscaling complete!")
    print(f"Successfully processed: {processed} images")
    print(f"Failed: {failed} images")
    print(f"Original images have been moved to the 'originals' subfolder")

if __name__ == "__main__":
    main()
```

</details>### KI-Tools

<details data-tc-id="w-0.513653034275673" id="bkmrk-foto-triage-ihr-m%C3%BCss" tc-textcontent="true"><summary>Foto-Triage</summary>

Ihr müsst für KI teils größere Datensätze bauen. Das geht mit einem einfachen Triage-Tool besser und deutlich schneller.  
Drei Ordner auswählen, auswählen was mit good oder bad passieren soll und ab die Post.  
Kommt mit größeren Bildern klar, ist aber etwas hakelig wenns ans Fenster verschieben geht, weil Python das wohl neu zeichnet beim Verschieben...

Tastatur: 1 für gute Bilder, 0 für schlechte Bilder und # fürs Überspringen.

Manchmal gibts Fehlermeldungen, je nach Bild, dafür gibts dann den Skip-Button.  
Manchmal wirds langsam, je nachdem wie schnell die Festplatten sind und wie viel davon geladen werden muss.  
Ich hantiere mit Bildern von fast 16k x 25k Pixeln. Es kommt also schon sehr auf eure Hardware an...

Die Decompression Bomb Protection ist eine Schutzmaßnahme die bei meinen großen Bildern eigentlich ständig anschlug. Ich hab das Gefühl dass es schon bei bisschen über 4K anschlägt.

```python
import os
import shutil
import tkinter as tk
from tkinter import filedialog, ttk, messagebox
from PIL import Image, ImageTk
Image.MAX_IMAGE_PIXELS = None  # Remove decompression bomb protection

class ImageReviewTool:
    def __init__(self, master):
        self.master = master
        master.title("Image Triage Tool")
        master.geometry("800x600")  # Set initial window size
        master.minsize(400, 300)  # Set minimum window size

        # Create the main frame
        self.frame = tk.Frame(master)
        self.frame.pack(fill=tk.BOTH, expand=True)

        # Create the image display area
        self.image_label = tk.Label(self.frame)
        self.image_label.pack(fill=tk.BOTH, expand=True)

        # Create frame for file handling options
        options_frame = tk.Frame(self.frame)
        options_frame.pack(side=tk.BOTTOM, fill=tk.X)

        # Create dropdown for "Good" images handling
        good_options_frame = tk.Frame(options_frame)
        good_options_frame.pack(side=tk.LEFT, padx=10, pady=5)
        
        tk.Label(good_options_frame, text="Good images:").pack(side=tk.LEFT)
        self.good_handling = ttk.Combobox(good_options_frame, 
                                        values=["Copy", "Move"],
                                        state="readonly",
                                        width=10)
        self.good_handling.set("Copy")
        self.good_handling.pack(side=tk.LEFT, padx=5)

        # Create dropdown for "Bad" images handling
        bad_options_frame = tk.Frame(options_frame)
        bad_options_frame.pack(side=tk.LEFT, padx=10, pady=5)
        
        tk.Label(bad_options_frame, text="Bad images:").pack(side=tk.LEFT)
        self.bad_handling = ttk.Combobox(bad_options_frame,
                                       values=["Copy", "Move"],
                                       state="readonly",
                                       width=10)
        self.bad_handling.set("Move")
        self.bad_handling.pack(side=tk.LEFT, padx=5)

        # Create the "Good", "Bad", and "Skip" buttons
        button_frame = tk.Frame(self.frame)
        button_frame.pack(side=tk.BOTTOM, fill=tk.X)

        self.good_button = tk.Button(button_frame, text="Good (1)", command=self.handle_good)
        self.good_button.pack(side=tk.LEFT, padx=10, pady=10)

        self.bad_button = tk.Button(button_frame, text="Bad (0)", command=self.handle_bad)
        self.bad_button.pack(side=tk.LEFT, padx=10, pady=10)

        self.skip_button = tk.Button(button_frame, text="Skip (#)", command=self.handle_skip)
        self.skip_button.pack(side=tk.LEFT, padx=10, pady=10)

        # Create the directory selection buttons
        dir_button_frame = tk.Frame(self.frame)
        dir_button_frame.pack(side=tk.BOTTOM, fill=tk.X)

        self.select_directory_button = tk.Button(dir_button_frame, text="Select Directory", command=self.select_directory)
        self.select_directory_button.pack(side=tk.LEFT, padx=10, pady=10)

        self.select_good_directory_button = tk.Button(dir_button_frame, text="Select Good Directory", command=self.select_good_directory)
        self.select_good_directory_button.pack(side=tk.LEFT, padx=10, pady=10)

        self.select_bad_directory_button = tk.Button(dir_button_frame, text="Select Bad Directory", command=self.select_bad_directory)
        self.select_bad_directory_button.pack(side=tk.LEFT, padx=10, pady=10)

        # Add current file label
        self.file_label = tk.Label(self.frame, text="")
        self.file_label.pack(side=tk.BOTTOM, pady=5)

        self.current_directory = None
        self.good_folder = None
        self.bad_folder = None
        self.current_image_index = 0
        self.image_files = []
        self.reviewed_images = []
        self.log_file = "image_review_log.txt"
        self.current_image = None

        # Bind the window resize event to the resize_image method
        self.master.bind("<Configure>", self.resize_image)

        # Bind keyboard shortcuts
        self.master.bind("1", self.handle_good)
        self.master.bind("0", self.handle_bad)
        self.master.bind("#", self.handle_skip)

    def select_directory(self):
        self.current_directory = filedialog.askdirectory()
        if self.current_directory:
            self.image_files = [f for f in os.listdir(self.current_directory) if f.lower().endswith((".jpg", ".jpeg", ".png", ".gif"))]
            self.current_image_index = 0
            self.reviewed_images = self.load_reviewed_images()
            self.display_image()

    def select_good_directory(self):
        self.good_folder = filedialog.askdirectory()

    def select_bad_directory(self):
        self.bad_folder = filedialog.askdirectory()

    def display_image(self):
        if self.current_directory and self.image_files:
            if self.current_image_index >= len(self.image_files):
                self.image_label.configure(image='')
                self.file_label.config(text="")
                messagebox.showinfo("Complete", "All images have been reviewed!")
                return

            try:
                image_path = os.path.join(self.current_directory, self.image_files[self.current_image_index])
                self.current_image = Image.open(image_path)
                
                # Update file info label
                img_size = os.path.getsize(image_path) / (1024 * 1024)  # Convert to MB
                self.file_label.config(
                    text=f"File: {self.image_files[self.current_image_index]} | "
                         f"Size: {img_size:.1f}MB | "
                         f"Dimensions: {self.current_image.width}x{self.current_image.height}"
                )
                
                self.resize_image(None)
            except Exception as e:
                messagebox.showerror("Error", f"Failed to load image: {str(e)}")
                self.current_image_index += 1
                self.display_image()

    def resize_image(self, event):
        if self.current_image:
            try:
                # Get the current window size
                window_width = self.master.winfo_width()
                window_height = self.master.winfo_height() - 150  # Adjust for buttons and options

                # Scale the image to fit the window while maintaining aspect ratio
                image_ratio = self.current_image.width / self.current_image.height
                if window_width / window_height > image_ratio:
                    new_height = window_height
                    new_width = int(new_height * image_ratio)
                else:
                    new_width = window_width
                    new_height = int(new_width / image_ratio)

                # Use thumbnail for memory-efficient resizing of large images
                img_copy = self.current_image.copy()
                img_copy.thumbnail((new_width, new_height), Image.Resampling.LANCZOS)
                self.photo = ImageTk.PhotoImage(img_copy)
                self.image_label.configure(image=self.photo)
            except Exception as e:
                messagebox.showerror("Error", f"Failed to resize image: {str(e)}")

    def handle_good(self, event=None):
        if self.current_directory and self.good_folder and self.image_files:
            src_path = os.path.join(self.current_directory, self.image_files[self.current_image_index])
            dst_path = os.path.join(self.good_folder, self.image_files[self.current_image_index])
            
            try:
                if self.good_handling.get() == "Copy":
                    shutil.copy2(src_path, dst_path)
                else:  # Move
                    shutil.move(src_path, dst_path)
                
                self.reviewed_images.append(self.image_files[self.current_image_index])
                self.save_reviewed_images()
                self.current_image_index += 1
                self.display_image()
            except Exception as e:
                messagebox.showerror("Error", f"Failed to process image: {str(e)}")

    def handle_bad(self, event=None):
        if self.current_directory and self.bad_folder and self.image_files:
            src_path = os.path.join(self.current_directory, self.image_files[self.current_image_index])
            dst_path = os.path.join(self.bad_folder, self.image_files[self.current_image_index])
            
            try:
                if self.bad_handling.get() == "Copy":
                    shutil.copy2(src_path, dst_path)
                else:  # Move
                    shutil.move(src_path, dst_path)
                
                self.reviewed_images.append(self.image_files[self.current_image_index])
                self.save_reviewed_images()
                self.current_image_index += 1
                self.display_image()
            except Exception as e:
                messagebox.showerror("Error", f"Failed to process image: {str(e)}")

    def handle_skip(self, event=None):
        if self.current_directory and self.image_files:
            self.current_image_index += 1
            self.display_image()

    def load_reviewed_images(self):
        reviewed_images = []
        if os.path.exists(self.log_file):
            with open(self.log_file, "r") as f:
                reviewed_images = [line.strip() for line in f.readlines()]
        return reviewed_images

    def save_reviewed_images(self):
        with open(self.log_file, "w") as f:
            for image in self.reviewed_images:
                f.write(image + "\n")

if __name__ == "__main__":
    root = tk.Tk()
    app = ImageReviewTool(root)
    root.mainloop()
```

</details><details data-tc-id="w-0.4969457531852486" id="bkmrk-computerbilder-von-p" tc-textcontent="true"><summary>"bad" and "good" Sortierung nach Beispielen</summary>

#### Funktionsweise  


Das Script benötigt drei Ordner:

- schlechte Bilder
- gute Bilder
- Ordner mit Fotos die sortiert werden müssen

je nachdem wie viele Beispiele in beiden Ordnern (schlecht und gut) sind desto besser wird sortiert...  
Im Ordner mit den zu sortierenden Bildern werden zwei Ordner erstellt in denen die jeweiligen Fotos verschoben werden.

#### Anlernphase

Es ist völlig egal nach welchen Kriterien ihr sortieren wollt, worum ihr nicht rum kommt ist 100-200 Fotos händisch zu sortieren die schlecht und die gut sind.

Danach lasst ihr das Script einmal über euren Bildersatz laufen.   
Aus dem "bad" Ordner sortiert ihr dann die guten Bilder, die da noch fälschlicherweise sind in den Trainingssatz für die guten Bilder rein.  
Das was übrig bleibt sind dann die echt schlechten Bilder, die ihr am besten dann auch in den schlechten Ordner verschiebt.

Dann nehmt ihr den Ordner in dem die laut Script guten Fotos reinsortiert worden sind und lasst das Script mit dem neuen Datensatz noch mal drüber laufen.

Das ganze wiederholt ihr dann bis das Ergebnis für euch passt. Die Ordner mit den Datensätzen behaltet ihr dann...  
Je nachdem welchen Datensatz ihr vergrößert verbessert ihr die Erkennung der schlechten oder der guten Bilder.

Das ist basic Bilder-KI Training...

#### Script

<p class="callout info">DIe Ordner und Variablen im Script heißen noch "digital art" und "photos" da meine Aufgabe es war rein gezeichnete Bilder von Fotografien zu trennen.</p>

<p class="callout success">Nun auch mit Menü zur Auswahl und Vorab-Training eines Modells und allem Firlefanz...</p>

**Script ist PYTHON**

*pip install pillow torch torchvision*

```python
import torch
import torchvision.transforms as transforms
import torchvision.models as models
from torchvision.models import ResNet50_Weights  # Add this import
from PIL import Image
import os
import shutil
from torch import nn
import numpy as np
from pathlib import Path
import tkinter as tk
from tkinter import filedialog
from torch.utils.data import Dataset, DataLoader
from torchvision.datasets import ImageFolder
import warnings
from PIL import Image, ImageFile

# Increase PIL image size limit and allow truncated images
Image.MAX_IMAGE_PIXELS = None  # Remove size limit
ImageFile.LOAD_TRUNCATED_IMAGES = True
warnings.filterwarnings('ignore', category=Image.DecompressionBombWarning)

class CustomImageDataset(Dataset):
    def __init__(self, photos_path, digital_path, transform=None):
        self.transform = transform
        self.images = []
        self.labels = []
        
        # Load photos (label 0)
        for img_path in Path(photos_path).glob('*'):
            if img_path.suffix.lower() in {'.jpg', '.jpeg', '.png', '.bmp'}:
                self.images.append(str(img_path))
                self.labels.append(0)
        
        # Load digital art (label 1)
        for img_path in Path(digital_path).glob('*'):
            if img_path.suffix.lower() in {'.jpg', '.jpeg', '.png', '.bmp'}:
                self.images.append(str(img_path))
                self.labels.append(1)

    def __len__(self):
        return len(self.images)

    def __getitem__(self, idx):
        image_path = self.images[idx]
        try:
            with Image.open(image_path) as img:
                img = img.convert('RGB')
                
                if max(img.size) > 4096:
                    ratio = 4096 / max(img.size)
                    new_size = tuple(int(dim * ratio) for dim in img.size)
                    img = img.resize(new_size, Image.Resampling.LANCZOS)
                
                if self.transform:
                    image = self.transform(img)
                return image, self.labels[idx]
        except Exception as e:
            print(f"Error loading image {image_path}: {e}")
            return torch.zeros((3, 224, 224)), self.labels[idx]

class ImageSorter:
    def __init__(self):
        # Use the new weights parameter instead of pretrained
        weights = ResNet50_Weights.DEFAULT
        self.model = models.resnet50(weights=weights)
        num_features = self.model.fc.in_features
        self.model.fc = nn.Linear(num_features, 2)
        
        # Update transform to use the weights preprocessing
        self.transform = transforms.Compose([
            transforms.Resize((224, 224)),
            transforms.ToTensor(),
            weights.transforms()  # Use the preprocessing from the weights
        ])
        
        self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
        self.model = self.model.to(self.device)
        self.criterion = nn.CrossEntropyLoss()
        self.optimizer = torch.optim.Adam(self.model.parameters(), lr=0.001)

    def save_model(self, path):
        """Save the trained model to a file"""
        save_dict = {
            'model_state_dict': self.model.state_dict(),
            'optimizer_state_dict': self.optimizer.state_dict(),
        }
        torch.save(save_dict, path)
        print(f"Model saved to {path}")

    def load_model(self, path):
        """Load a trained model from a file"""
        if not os.path.exists(path):
            raise FileNotFoundError(f"No model file found at {path}")
            
        checkpoint = torch.load(path, map_location=self.device)
        self.model.load_state_dict(checkpoint['model_state_dict'])
        self.optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
        print(f"Model loaded from {path}")

    def safe_load_image(self, image_path):
        try:
            with Image.open(image_path) as img:
                img = img.convert('RGB')
                if max(img.size) > 4096:
                    ratio = 4096 / max(img.size)
                    new_size = tuple(int(dim * ratio) for dim in img.size)
                    img = img.resize(new_size, Image.Resampling.LANCZOS)
                return img
        except Exception as e:
            print(f"Error loading image {image_path}: {e}")
            return None

    def train_model(self, photos_path, digital_path, epochs=10):
        dataset = CustomImageDataset(
            photos_path=photos_path,
            digital_path=digital_path,
            transform=self.transform
        )
        
        dataloader = DataLoader(dataset, batch_size=16, shuffle=True)
        
        print("\nStarting training...")
        print(f"Training with {len(dataset)} images")
        print(f"Using device: {self.device}")
        
        self.model.train()
        for epoch in range(epochs):
            running_loss = 0.0
            correct = 0
            total = 0
            
            for i, (images, labels) in enumerate(dataloader):
                images = images.to(self.device)
                labels = labels.to(self.device)
                
                self.optimizer.zero_grad()
                outputs = self.model(images)
                loss = self.criterion(outputs, labels)
                loss.backward()
                self.optimizer.step()
                
                running_loss += loss.item()
                _, predicted = torch.max(outputs.data, 1)
                total += labels.size(0)
                correct += (predicted == labels).sum().item()
            
            epoch_loss = running_loss / len(dataloader)
            accuracy = 100 * correct / total
            print(f'Epoch {epoch + 1}/{epochs} - Loss: {epoch_loss:.4f} - Accuracy: {accuracy:.2f}%')

    def predict_image(self, image_path):
        try:
            img = self.safe_load_image(image_path)
            if img is None:
                return None, None
                
            image_tensor = self.transform(img).unsqueeze(0).to(self.device)
            
            self.model.eval()
            with torch.no_grad():
                outputs = self.model(image_tensor)
                probabilities = torch.softmax(outputs, dim=1)
                confidence, prediction = torch.max(probabilities, 1)
                
            return prediction.item(), confidence.item()
            
        except Exception as e:
            print(f"Error processing {image_path}: {str(e)}")
            return None, None

    def sort_images(self, input_directory):
        input_path = Path(input_directory).resolve()
        digital_art_dir = input_path / 'digital_art'
        photos_dir = input_path / 'photos'
        
        digital_art_dir.mkdir(exist_ok=True, parents=True)
        photos_dir.mkdir(exist_ok=True, parents=True)
        
        print(f"\nCreated directories:")
        print(f"Digital Art: {digital_art_dir}")
        print(f"Photos: {photos_dir}\n")
        
        supported_formats = {'.jpg', '.jpeg', '.png', '.bmp'}
        files_processed = 0
        failed_files = 0
        total_files = len([f for f in input_path.iterdir() 
                          if f.is_file() and f.suffix.lower() in supported_formats])
        
        for file_path in input_path.iterdir():
            if file_path.is_file() and file_path.suffix.lower() in supported_formats:
                if 'digital_art' in str(file_path) or 'photos' in str(file_path):
                    continue
                
                try:
                    files_processed += 1
                    print(f"Processing image {files_processed}/{total_files}: {file_path.name}")
                    
                    prediction, confidence = self.predict_image(str(file_path))
                    if prediction is None:
                        failed_files += 1
                        continue
                        
                    if prediction == 1:
                        destination = digital_art_dir / file_path.name
                        print(f"→ Moving to digital_art folder (confidence: {confidence:.2%})")
                    else:
                        destination = photos_dir / file_path.name
                        print(f"→ Moving to photos folder (confidence: {confidence:.2%})")
                    
                    shutil.move(str(file_path), str(destination))
                    
                except Exception as e:
                    failed_files += 1
                    print(f"Error processing {file_path.name}: {str(e)}")
        
        print(f"\nProcessing complete!")
        print(f"Total images processed: {files_processed}")
        print(f"Failed to process: {failed_files}")

def get_folder_path(title):
    root = tk.Tk()
    root.withdraw()
    folder_path = filedialog.askdirectory(title=title)
    return folder_path if folder_path else None

def get_file_path(title, file_types):
    root = tk.Tk()
    root.withdraw()
    file_path = filedialog.askopenfilename(title=title, filetypes=file_types)
    return file_path if file_path else None

def main():
    print("Digital Art Sorter")
    print("=================")
    
    sorter = ImageSorter()
    
    while True:
        print("\nOptions:")
        print("1. Train and save new model")
        print("2. Load existing model")
        print("3. Sort images using current model")
        print("4. Exit")
        
        choice = input("\nEnter your choice (1-4): ")
        
        if choice == '1':
            print("\nSelect folder containing PHOTO examples")
            photos_training = get_folder_path("Select folder with photo examples")
            if not photos_training:
                continue
                
            print("\nSelect folder containing DIGITAL ART examples")
            digital_training = get_folder_path("Select folder with digital art examples")
            if not digital_training:
                continue
            
            sorter.train_model(photos_training, digital_training)
            
            print("\nSelect where to save the trained model")
            save_path = filedialog.asksaveasfilename(
                defaultextension=".pth",
                filetypes=[("PyTorch Model", "*.pth")]
            )
            if save_path:
                sorter.save_model(save_path)
        
        elif choice == '2':
            print("\nSelect model file to load")
            model_path = get_file_path("Select model file", [("PyTorch Model", "*.pth")])
            if model_path:
                try:
                    sorter.load_model(model_path)
                except Exception as e:
                    print(f"Error loading model: {str(e)}")
        
        elif choice == '3':
            print("\nSelect folder containing images to sort")
            source_folder = get_folder_path("Select folder with images to sort")
            if source_folder:
                sorter.sort_images(source_folder)
        
        elif choice == '4':
            print("\nExiting...")
            break
        
        else:
            print("\nInvalid choice. Please try again.")

if __name__ == "__main__":
    main()
```

</details>###   
Dedup

macht man am besten mit [Czkawka](https://github.com/qarmin/czkawka) oder [DupeGuru](https://github.com/arsenetar/dupeguru)

<details data-tc-id="w-0.6251740140966665" id="bkmrk-basic-dedup-chatgpt-" tc-textcontent="true"><summary>Basic Dedup</summary>

ChatGPT 3.5

(falls Copy statt Move im Script vorab gemacht wurde)

<p class="callout info">Das Ding tut noch nicht das was ich will, womöglich komme ich selbst durcheinander mit Source und Destination. Ich setz also unten noch mal anders an.</p>

```powershell
param (
  # Mag etwas durcheinander und verkehrt sein. KI halt... Jedenfalls ist Destination der Ordner der über bleiben soll. und source der Ordner in dem die Duplikate liegen
    # Ordner in dem gelöscht wird.
    [string]$SourceFolder = "XXXXXXXXXXXXXXXXXXXXXXXXX",
    # Ordner gegen den Verglichen wird
    [string]$DestinationFolder = "XXXXXXXXXXXXXXXXXXXX",

)

# Get all image files in the destination folder
$destinationImages = Get-ChildItem -Path $DestinationFolder -Filter "*.jpg" -File

# Initialize counters for deleted images in each folder
$deletedInSourceFolderCount = 0
$deletedInDestinationFolderCount = 0

# Process each image file in the destination folder
foreach ($destinationImage in $destinationImages) {
    Write-Host "Processing $($destinationImage.FullName)"

    # Construct the source file path based on the destination file name
    $sourceFilePath = Join-Path -Path $SourceFolder -ChildPath $destinationImage.Name

    # Check if the corresponding file exists in the source folder
    if (Test-Path $sourceFilePath) {
        Write-Host "Deleting $($sourceFilePath)"
        # Remove the item (move to Recycle Bin)
        Remove-Item -Path $sourceFilePath -Force -Confirm:$false

        # Increment the counter for deleted images in the source folder
        $deletedInSourceFolderCount++
    }

    # Increment the counter for deleted images in the destination folder
    $deletedInDestinationFolderCount++
}

# Output the deletion statistics
Write-Host "Duplicate removal complete!"
Write-Host "Deleted $deletedInSourceFolderCount images in the source folder: $SourceFolder"
```

</details>### Prüfscripts

<details data-tc-id="w-0.38709692644641513" id="bkmrk-pr%C3%BCfen-ob-keine-bild" tc-textcontent="true"><summary>Prüfen ob keine Bilder &gt;4K existieren</summary>

ChatGPT 3.5

```powershell
$SourceFolder = "C:\Path\to\SourceFolder"
$MinWidth = 3840
$MinHeight = 2160

# Get all image files in the source folder and its subfolders
$images = Get-ChildItem -Path $SourceFolder -Filter "*.jpg" -File -Recurse

$totalImages = $images.Count
$processedImages = 0

# Process each image file
foreach ($image in $images) {
    # Use .NET classes to read image dimensions
    $imageStream = New-Object System.IO.FileStream($image.FullName, [System.IO.FileMode]::Open)
    $imageBitmap = New-Object System.Drawing.Bitmap($imageStream)
    $width = $imageBitmap.Width
    $height = $imageBitmap.Height
    $imageStream.Close()

    # Check if the image is larger than the specified dimensions
    if ($width -ge $MinWidth -and $height -ge $MinHeight) {
        Write-Host "Found large image: $($image.FullName)"
    }

    $processedImages++
    $progress = [math]::Round(($processedImages / $totalImages) * 100, 2)
    Write-Progress -Activity "Checking image sizes" -Status "Progress: $progress%" -PercentComplete $progress
}

Write-Progress -Activity "Checking image sizes" -Status "Progress: 100%" -PercentComplete 100
Write-Host "Image size check complete!"

```

</details><details data-tc-id="w-0.33464739452060155" id="bkmrk-alle-bilder-%3E-4k-l%C3%B6s" tc-textcontent="true"><summary>Alle Bilder &gt; 4K löschen</summary>

```powershell
$SourceFolder = "C:\Path\to\SourceFolder"
$MinWidth = 3840
$MinHeight = 2160

# Get all image files in the source folder and its subfolders
$images = Get-ChildItem -Path $SourceFolder -Filter "*.jpg" -File -Recurse

$totalImages = $images.Count
$processedImages = 0

# Process each image file
foreach ($image in $images) {
    # Use .NET classes to read image dimensions
    $imageStream = New-Object System.IO.FileStream($image.FullName, [System.IO.FileMode]::Open)
    $imageBitmap = New-Object System.Drawing.Bitmap($imageStream)
    $width = $imageBitmap.Width
    $height = $imageBitmap.Height
    $imageStream.Close()

    # Check if the image is larger than the specified dimensions
    if ($width -ge $MinWidth -and $height -ge $MinHeight) {
        Write-Host "Deleting large image: $($image.FullName)"
        Remove-Item -Path $image.FullName -Force
    }

    $processedImages++
    $progress = [math]::Round(($processedImages / $totalImages) * 100, 2)
    Write-Progress -Activity "Checking and deleting large images" -Status "Progress: $progress%" -PercentComplete $progress
}

Write-Progress -Activity "Checking and deleting large images" -Status "Progress: 100%" -PercentComplete 100
Write-Host "Image size check and deletion complete!"
```

</details>

# POWERSHELL Downloads-Ordner sortieren

<p class="callout danger">!!! WORK IN PROGRESS !!!</p>

```powershell
# Pfad zum Downloads-Ordner
$downloadsPath = "$env:userprofile\Downloads"

# Name des Ordners, in den alle nicht sortierten Ordner verschoben werden sollen
$folderName = "Ordner"

# Liste von Zielordnern und den zugehörigen Dateierweiterungen
$folders = @{
    "Dokumente" = @(".pdf", ".odt", ".doc", ".docx", ".rtf", ".txt")
    "Bilder" = @(".jpg", ".jpeg", ".png", ".gif", ".bmp")
    "Musik" = @(".mp3", ".flac", ".wav", ".m4a", ".aac", ".wma")
    "Archive" = @(".zip", ".rar", ".7z", ".tar", ".gz")
    "Setups" = @(".exe", ".msi")
}

# Erstellen des Ordners für nicht sortierte Dateien, falls er nicht vorhanden ist
$otherFolderPath = Join-Path $downloadsPath $folderName
if (-not (Test-Path $otherFolderPath)) {
    New-Item -ItemType Directory -Path $otherFolderPath | Out-Null
}

# Sortieren der Dateien
Get-ChildItem $downloadsPath -Exclude $folderName | Where-Object { $_.PSIsContainer -eq $false } | ForEach-Object {
    $extension = $_.Extension
    foreach ($folder in $folders.Keys) {
        if ($folders[$folder] -contains $extension) {
            $targetPath = Join-Path $downloadsPath $folder
            Move-Item $_.FullName $targetPath
            return
        }
    }
    if ($_.Name -ne $folderName -and $_.Name -ne "Sonstige Dateien") {
        $otherFolderPath = Join-Path $downloadsPath $folderName
        Move-Item $_.FullName $otherFolderPath
    }
}

# Verschieben der verbleibenden Ordner in den Ordner für nicht sortierte Dateien
Get-ChildItem $downloadsPath -Directory | Where-Object { $_.Name -notin $folders.Keys -and $_.Name -ne $folderName -and $_.Name -ne "Sonstige Dateien" } | ForEach-Object {
    Move-Item $_.FullName $otherFolderPath
}

# Schließen des PowerShell-Fensters
Exit

```

Das Script erstellt Ordner und verschiebt Dateien darein. Alle sonstigen Ordner die in dem Downloads-Ordner verbleiben, werden in einen Ordner namens "Ordner" verschoben:

Endresultat ist ein Downloads-Ordner mit sieben Ordnern der entsprechenden Dateitypen. Deutlich aufgeräumter als alles chronologisch rumliegen zu lassen.

# PYTHON Schattenkopie-Management

Ich habe eine ziemlich große Festplatte und deshalb auch die File History von Windows ziemlich aufgebohrt. Zusammen mit der [Bilder-Stapelverarbeitung](https://wiki.notizlo.ch/books/windows/page/python-bilder-stapelverarbeitung) kamen dann allerdings deutlich größere Berge an veränderten Dateien zutage, so dass die File History dann doch noch in Richtung Flut ging.

Jetzt weiß ich natürlich nicht warum Windows das Cleanup nicht machen will nach Umstellung der Aufbewahrungsrichtlinien.   
Muss ich auch nicht wissen... Ist irgendwie so...

```python
import os
import shutil
from datetime import datetime, timedelta
import concurrent.futures

def get_drives():
    """Listet verfügbare Laufwerke auf."""
    import psutil
    drives = [partition.mountpoint for partition in psutil.disk_partitions() if 'fixed' in partition.opts.lower()]
    return drives

def select_drive(drives):
    """Benutzer wählt Laufwerk aus."""
    print("Verfügbare Laufwerke:")
    for i, drive in enumerate(drives, 1):
        total, used, free = shutil.disk_usage(drive)
        print(f"{i}. {drive} - Größe: {total // (1024**3)} GB")
    choice = int(input("Wählen Sie das Laufwerk (Nummer): ")) - 1
    return drives[choice]

def is_old_file(file_path, threshold_days=90):
    """Prüft, ob Datei älter als Schwellenwert ist."""
    file_mtime = datetime.fromtimestamp(os.path.getmtime(file_path))
    return datetime.now() - file_mtime > timedelta(days=threshold_days)

def process_files(directory, threshold_days=90):
    """Durchsucht Dateien parallel."""
    old_files = []
    total_size = 0

    with concurrent.futures.ThreadPoolExecutor() as executor:
        file_futures = []
        for root, _, files in os.walk(directory):
            for file in files:
                file_path = os.path.join(root, file)
                file_futures.append(executor.submit(is_old_file, file_path, threshold_days))

        for future, file_path in zip(concurrent.futures.as_completed(file_futures), 
                                     [os.path.join(root, file) for root, _, files in os.walk(directory) for file in files]):
            if future.result():
                try:
                    file_size = os.path.getsize(file_path)
                    old_files.append((file_path, file_size))
                    total_size += file_size
                except Exception as e:
                    print(f"Fehler bei {file_path}: {e}")

    return old_files, total_size

def main():
    drives = get_drives()
    selected_drive = select_drive(drives)
    directory = os.path.join(selected_drive, 'FileHistory')

    print("Suche nach alten Dateien...")
    old_files, total_size = process_files(directory)

    print(f"\nGefundene alte Dateien: {len(old_files)}")
    print(f"Gesamtgröße: {total_size / (1024**3):.2f} GB")

    for file, size in old_files[:10]:  # Zeige erste 10 Dateien
        print(f"{file} - {size / (1024**2):.2f} MB")

    confirm = input("\nMöchten Sie diese Dateien löschen? (j/n): ")
    if confirm.lower() == 'j':
        for file, _ in old_files:
            try:
                os.remove(file)
            except Exception as e:
                print(f"Fehler beim Löschen von {file}: {e}")
        print("Dateien gelöscht.")

if __name__ == "__main__":
    main()
```

Festplatte auswählen, dann wird ./FileHistory durchsucht und alles erst mal ausgegeben was älter als 90 Tage ist. Dann gibts ne Option zum Löschen.

Hier müsst ihr noch das python Modul psutil installieren.

<details data-tc-id="w-0.3721718081305414" id="bkmrk-das-ganze-geht-auch-" tc-textcontent="true"><summary>Das ganze geht auch in Powershell (dann halt nur langsamer)</summary>

```powershell
# Skript zur Datei-Bereinigung mit Fortschrittsanzeige und Festplattenauswahl
Clear-Host

# Alle Festplatten auflisten
$drives = Get-PSDrive -PSProvider FileSystem | 
    Where-Object {$_.Free -and $_.Used} | 
    Select-Object Root, @{Name='SizeGB';Expression={[math]::Round(($_.Used + $_.Free)/1GB, 2)}}

# Festplattenauswahl
Write-Host "Verfügbare Festplatten:"
for ($i=0; $i -lt $drives.Count; $i++) {
    Write-Host "$($i+1). $($drives[$i].Root) - $($drives[$i].SizeGB) GB"
}

$driveChoice = Read-Host "Wählen Sie die Festplatte (Nummer)"
$selectedDrive = $drives[$driveChoice-1].Root

# Parameter
$path = "$selectedDrive`FileHistory"
$thresholdDays = 90

# Fortschrittsanzeige
function Show-Progress {
    param($total, $current, $activity)
    $percentage = [math]::Floor(($current / $total) * 100)
    Write-Progress -Activity $activity -Status "$percentage% Complete" -PercentComplete $percentage
}

# Dateien sammeln
$allFiles = Get-ChildItem -Path $path -Recurse -File
$total = $allFiles.Count
$oldFiles = @()

# Durchsuchen mit Fortschrittsanzeige
for ($i = 0; $i -lt $total; $i++) {
    $file = $allFiles[$i]
    Show-Progress -total $total -current $i -activity "Dateien durchsuchen"
    
    if ($file.LastWriteTime -lt (Get-Date).AddDays(-$thresholdDays)) {
        $oldFiles += $file
    }
}

# Ergebnisse anzeigen
Write-Host "`nGefundene alte Dateien:"
$oldFiles | Format-Table FullName, LastWriteTime, @{Name='SizeMB';Expression={[math]::Round($_.Length/1MB,2)}}

# Bestätigung und Löschung
$confirm = Read-Host "Möchten Sie diese $($oldFiles.Count) Dateien wirklich löschen? (j/n)"
if ($confirm -eq "j") {
    for ($i = 0; $i -lt $oldFiles.Count; $i++) {
        Show-Progress -total $oldFiles.Count -current $i -activity "Dateien löschen"
        Remove-Item $oldFiles[$i].FullName -Force
    }
    Write-Host "`nAlle alten Dateien wurden gelöscht."
} else {
    Write-Host "Löschvorgang abgebrochen."
}
```

</details>

# PYTHON Tensorflow Bildsortierung

<p class="callout warning">Hierfür braucht man relativ große Bilddatensätze als Beispiele für Tensorflow um zuverlässige Ergebnisse zu bekommen.</p>

### Situation

Ihr habt ein großen Ordner voller Bilder und mit diesen über [https://teachablemachine.withgoogle.com](https://teachablemachine.withgoogle.com) ein Modell mit zwei Klassen erstellt und ALS QUANTITISIERT exportiert.

### Script

python 3.9

pip install tensorflow pillow

```python
#!/usr/bin/env python3
# sorter_tm.py

import os
import sys
import shutil
from PIL import Image
import numpy as np
import tensorflow as tf
tflite = tf.lite

def load_labels(label_path):
    """Liest die Klassennamen aus einer labels.txt im Format '0 Klasse1'."""
    labels = []
    with open(label_path, 'r', encoding='utf-8') as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            parts = line.split(' ', 1)
            if len(parts) < 2:
                raise ValueError(f"Ungültiges Format in labels.txt: '{line}'")
            labels.append(parts[1])
    if not labels:
        raise ValueError("Die labels.txt ist leer oder enthält keine gültigen Einträge.")
    return labels

def prepare_interpreter(model_path):
    """Initialisiert den TFLite-Interpreter."""
    interpreter = tflite.Interpreter(model_path=model_path)
    interpreter.allocate_tensors()
    input_details  = interpreter.get_input_details()[0]
    output_details = interpreter.get_output_details()[0]
    return interpreter, input_details, output_details

def classify_image(interpreter, input_details, output_details, image, target_size):
    input_shape = input_details['shape']
    input_dtype = input_details['dtype']

    # Resize des PIL-Bildes
    img = image.resize(target_size)
    arr = np.array(img)

    if input_dtype == np.float32:
        arr = arr.astype(np.float32) / 255.0
    elif input_dtype == np.uint8:
        arr = arr.astype(np.uint8)
    else:
        raise ValueError(f"Nicht unterstützter Eingabetyp: {input_dtype}")

    arr = np.expand_dims(arr, axis=0)
    interpreter.set_tensor(input_details['index'], arr)
    interpreter.invoke()

    output = interpreter.get_tensor(output_details['index'])[0]
    output = output.astype(np.float32)  # Konvertiere Ausgabe zu float32 für Softmax
    probabilities = tf.nn.softmax(output).numpy()
    idx = np.argmax(probabilities)
    confidence = probabilities[idx]
    return idx, confidence, probabilities

def main(model_path, label_path, src_dir, dst_root, image_size):
    # Labels und Interpreter laden
    labels = load_labels(label_path)
    interpreter, in_det, out_det = prepare_interpreter(model_path)

    # Ausgabeordnerstruktur anlegen
    for lbl in labels:
        os.makedirs(os.path.join(dst_root, lbl), exist_ok=True)

    # Jedes Bild klassifizieren und verschieben
    for filename in os.listdir(src_dir):
        if not filename.lower().endswith((".jpg", ".jpeg", ".png", ".bmp", ".gif")):
            continue

        image_path = os.path.join(src_dir, filename)
        try:
            with Image.open(image_path) as img:
                img = img.convert("RGB")
        except Exception as e:
            print(f"Bild konnte nicht geöffnet werden: {filename} – {e}")
            continue

        idx, confidence, probabilities = classify_image(interpreter, in_det, out_det, img, image_size)
        label = labels[idx]

        target_dir = os.path.join(dst_root, label)
        os.makedirs(target_dir, exist_ok=True)

        target_path = os.path.join(target_dir, filename)
        try:
            shutil.move(image_path, target_path)
            print(f"{filename} → {label} ({confidence:.2%}) – Verteilung: {probabilities}")
        except Exception as e:
            print(f"Fehler beim Verschieben von {filename}: {e}")

if __name__ == "__main__":
    if len(sys.argv) != 6:
        print("Usage: python sorter_tm.py <model.tflite> <labels.txt> "
              "<source_dir> <destination_root> <image_size>")
        print("Beispiel: python sorter_tm.py model/model.tflite model/labels.txt "
              "input_images sorted_images 224")
        sys.exit(1)

    model_file       = sys.argv[1]
    labels_file      = sys.argv[2]
    source_folder    = sys.argv[3]
    destination_root = sys.argv[4]
    size             = int(sys.argv[5])

    main(model_file, labels_file, source_folder, destination_root, (size, size))
```

<details id="bkmrk-vorg%C3%A4ngerversionen-v"><summary>Vorgängerversionen</summary>

V2: Kann noch nicht mit Tensorflow Labels umgehen und erwartet feste Klassen.

```python
#!/usr/bin/env python3
# sorter_tm.py

import os
import sys
import shutil
from PIL import Image
import numpy as np
import tensorflow as tf
tflite = tf.lite

def load_labels(label_path):
    """Liest die Klassennamen aus einer labels.txt."""
    with open(label_path, 'r', encoding='utf-8') as f:
        labels = [line.strip() for line in f if line.strip()]
    if len(labels) != 2 or set(labels) != {"Good", "Bad"}:
        raise ValueError("labels.txt muss genau die Einträge 'Good' und 'Bad' enthalten.")
    return labels

def prepare_interpreter(model_path):
    """Initialisiert den TFLite-Interpreter."""
    interpreter = tflite.Interpreter(model_path=model_path)
    interpreter.allocate_tensors()
    input_details  = interpreter.get_input_details()[0]
    output_details = interpreter.get_output_details()[0]
    return interpreter, input_details, output_details

def classify_image(interpreter, input_details, output_details, image, target_size):
    input_shape = input_details['shape']
    input_dtype = input_details['dtype']

    # Resize des PIL-Bildes
    img = image.resize(target_size)
    arr = np.array(img)

    if input_dtype == np.float32:
        arr = arr.astype(np.float32) / 255.0
    elif input_dtype == np.uint8:
        arr = arr.astype(np.uint8)
    else:
        raise ValueError(f"Nicht unterstützter Eingabetyp: {input_dtype}")

    arr = np.expand_dims(arr, axis=0)
    interpreter.set_tensor(input_details['index'], arr)
    interpreter.invoke()

    output = interpreter.get_tensor(output_details['index'])[0]
    output = output.astype(np.float32)  # Konvertiere Ausgabe zu float32 für Softmax
    probabilities = tf.nn.softmax(output).numpy()
    idx = np.argmax(probabilities)
    confidence = probabilities[idx]
    return idx, confidence, probabilities

def main(model_path, label_path, src_dir, dst_root, image_size):
    # Labels und Interpreter laden
    labels = load_labels(label_path)
    interpreter, in_det, out_det = prepare_interpreter(model_path)

    # Ausgabeordnerstruktur anlegen
    for lbl in labels:
        os.makedirs(os.path.join(dst_root, lbl), exist_ok=True)

    # Jedes Bild klassifizieren und verschieben
    for filename in os.listdir(src_dir):
        if not filename.lower().endswith((".jpg", ".jpeg", ".png", ".bmp", ".gif")):
            continue

        image_path = os.path.join(src_dir, filename)
        try:
            with Image.open(image_path) as img:
                img = img.convert("RGB")
        except Exception as e:
            print(f"Bild konnte nicht geöffnet werden: {filename} – {e}")
            continue

        idx, confidence, probabilities = classify_image(interpreter, in_det, out_det, img, image_size)
        label = labels[idx]

        target_dir = os.path.join(dst_root, label)
        os.makedirs(target_dir, exist_ok=True)

        target_path = os.path.join(target_dir, filename)
        try:
            shutil.move(image_path, target_path)
            print(f"{filename} → {label} ({confidence:.2%}) – Verteilung: {probabilities}")
        except Exception as e:
            print(f"Fehler beim Verschieben von {filename}: {e}")

if __name__ == "__main__":
    if len(sys.argv) != 6:
        print("Usage: python sorter_tm.py <model.tflite> <labels.txt> "
              "<source_dir> <destination_root> <image_size>")
        print("Beispiel: python sorter_tm.py model/model.tflite model/labels.txt "
              "input_images sorted_images 224")
        sys.exit(1)

    model_file       = sys.argv[1]
    labels_file      = sys.argv[2]
    source_folder    = sys.argv[3]
    destination_root = sys.argv[4]
    size             = int(sys.argv[5])

    main(model_file, labels_file, source_folder, destination_root, (size, size))
```

</details>Notige Struktur:  
C:\\BildSorter\\  
├── model\\  
│ ├── model.tflite ← exportiertes Modell von Teachable Machine  
│ └── labels.txt ← labels  
├── input\_images\\ ← HIER liegen Ihre zu sortierenden Bilder  
├── sorted\_images\\ ← WIRD automatisch befüllt mit Unterordnern  
├── sorter\_tm.py ← das Python-Skript

Run:

python sorter\_tm.py model/model.tflite model/labels.txt input\_images sorted\_images 224

# BATCH Active Directory Gruppenvergleiche

Ihr seid sicher in Umgebungen unterwegs in denen Powershell gesperrt ist... Also brauchen wir Batchscript-Hexerei...

Das Script hat noch einen Haufen Breakpoints...

```powershell
@echo off
setlocal enabledelayedexpansion

REM ====================================
REM Autor: Knaab-Hinrichs
REM Datum: 2025-06-05
REM Beschreibung: Vergleicht Gruppenmitgliedschaften von zwei Domänenbenutzern
REM ====================================

REM Benutzer eingeben (Parameter oder Abfrage)
set "User1=%~1"
set "User2=%~2"

if "%User1%"=="" (
    set /p User1=Benutzer 1 eingeben:
)
if "%User2%"=="" (
    set /p User2=Benutzer 2 eingeben:
)

echo Usereingabe
pause

REM Pfade zu temporären Dateien
set "User1File=%TEMP%\user1.txt"
set "User2File=%TEMP%\user2.txt"
set "Groups1Clean=%TEMP%\CleanUser1Groups.txt"
set "Groups2Clean=%TEMP%\CleanUser2Groups.txt"

echo Gruppenpfade
pause

REM Benutzerinformationen abrufen
net user "%User1%" /DOMAIN > "%User1File%"
net user "%User2%" /DOMAIN > "%User2File%"

echo userabruf
pause


REM Gruppen extrahieren
call :ExtractGroups "%User1File%" "%Groups1Clean%"

echo Gruppenextrakt User 1
pause 

call :ExtractGroups "%User2File%" "%Groups2Clean%"

echo Gruppenextrakt User 2
pause


REM Vergleich durchführen
echo.
echo ============================================
echo Gruppen in %User1% aber nicht in %User2%:
echo ============================================
for /f "delims=" %%g in ('type "%Groups1Clean%"') do (
    set "found=no"
    for /f "delims=" %%h in ('type "%Groups2Clean%"') do (
        if /I "%%g"=="%%h" set "found=yes"
    )
    if "!found!"=="no" echo %%g
)

echo Gruppenvergleich
pause

REM Temporäre Dateien löschen
del "%User1File%" >nul 2>&1
del "%User2File%" >nul 2>&1
del "%Groups1Clean%" >nul 2>&1
del "%Groups2Clean%" >nul 2>&1

endlocal
pause
exit /b

REM =======================
REM Funktion: Gruppen extrahieren
REM =======================
:ExtractGroups
setlocal enabledelayedexpansion
set "InGroups=0"
set "OutFile=%~2"

> "%OutFile%" (
    for /f "usebackq tokens=* delims=" %%L in (%~1) do (
        set "Line=%%L"

        REM Start der Gruppenliste erkennen
        echo !Line! | findstr /C:"Globale Gruppenmitgliedschaften" >nul
        if !errorlevel! == 0 (
            set "InGroups=1"
            for /f "tokens=2,* delims=:" %%A in ("!Line!") do (
                echo %%B
            )
        ) else if !InGroups! == 1 (
            REM Nur Zeilen mit * am Anfang sind Gruppenzeilen
            echo !Line! | findstr /R "^ [ ]*\*" >nul
            if !errorlevel! == 0 (
                echo !Line!
            ) else (
                set "InGroups=0"
            )
        )
    )
)

endlocal
exit /b

```

# Fileee.com Upload manager

Fileee.com hat im kostenfreien Service eine Uploadgrenze von 10 Dokumenten, wir müssen also einen Watchdog haben der Fileee nach und nach je nach Erlaubnis füttert.

### Setup

Fileee schaut auf WebDav von Nextcloud, daher brauche ich keine Spielereien mit Webcrawlern. Dieses WebDav Folder ist natürlich synchronisiert.

Generator: Perplexity Pro - 2025-02-03

pip install watchdog

```python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Fileee Upload Manager - Windows Background Service
Verwaltet die Warteschlange für Fileee.com Uploads mit monatlichem Limit
"""

import os
import sys
import time
import json
import shutil
from datetime import datetime
from pathlib import Path
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

class FileeeUploadManager:
    def __init__(self, queue_folder, target_folder, monthly_limit=10):
        self.queue_folder = Path(queue_folder)
        self.target_folder = Path(target_folder)
        self.monthly_limit = monthly_limit
        self.state_file = self.queue_folder / '.fileee_state.json'
        self.queue = []
        self.monthly_distribution = {}
        
        # Erstelle Ordner falls nicht vorhanden
        self.queue_folder.mkdir(parents=True, exist_ok=True)
        self.target_folder.mkdir(parents=True, exist_ok=True)
        
        self.load_state()
        
    def get_current_month_key(self):
        """Gibt aktuellen Monat im Format MMYY zurück"""
        now = datetime.now()
        return f"{now.month:02d}{str(now.year)[-2:]}"
    
    def get_month_with_offset(self, offset):
        """Berechnet Monat mit Offset im Format MMYY"""
        now = datetime.now()
        month = now.month + offset
        year = now.year
        
        while month > 12:
            month -= 12
            year += 1
        
        return f"{month:02d}{str(year)[-2:]}"
    
    def count_existing_files_in_month(self, month_key):
        """Zählt vorhandene PDFs im Monatsordner"""
        month_folder = self.target_folder / month_key
        if not month_folder.exists():
            return 0
        pdf_files = list(month_folder.glob('*.pdf'))
        return len(pdf_files)
    
    def scan_queue(self):
        """Scannt Warteschlangen-Ordner nach PDFs"""
        pdf_files = list(self.queue_folder.glob('*.pdf'))
        
        for pdf_file in pdf_files:
            if not any(f['path'] == str(pdf_file) for f in self.queue):
                self.queue.append({
                    'path': str(pdf_file),
                    'name': pdf_file.name,
                    'added': datetime.now().isoformat(),
                    'status': 'pending',
                    'scheduled_month': None
                })
        
        self.distribute_files()
        self.save_state()
        print(f"[INFO] {len(self.queue)} Dateien in Warteschlange")
    
    def distribute_files(self):
        """Verteilt Dateien auf Monate basierend auf Limit"""
        self.monthly_distribution = {}
        month_offset = 0
        
        # Sortiere nach Hinzufügedatum
        self.queue.sort(key=lambda x: x['added'])
        
        for file in self.queue:
            target_month = self.get_month_with_offset(month_offset)
            
            if target_month not in self.monthly_distribution:
                self.monthly_distribution[target_month] = []
            
            # Wenn Monat voll, gehe zum nächsten
            if len(self.monthly_distribution[target_month]) >= self.monthly_limit:
                month_offset += 1
                target_month = self.get_month_with_offset(month_offset)
                if target_month not in self.monthly_distribution:
                    self.monthly_distribution[target_month] = []
            
            self.monthly_distribution[target_month].append(file)
            file['scheduled_month'] = target_month
            
            current_month = self.get_current_month_key()
            file['status'] = 'scheduled' if target_month == current_month else 'pending'
    
    def process_current_month(self):
        """Verschiebt Dateien für aktuellen Monat in Zielordner"""
        current_month = self.get_current_month_key()
        
        if current_month not in self.monthly_distribution:
            return
        
        month_folder = self.target_folder / current_month
        month_folder.mkdir(exist_ok=True)
        
        for file in self.monthly_distribution[current_month]:
            if file['status'] == 'scheduled':
                source = Path(file['path'])
                if source.exists():
                    dest = month_folder / source.name
                    try:
                        shutil.move(str(source), str(dest))
                        print(f"[SUCCESS] Verschoben: {source.name} → {current_month}/")
                        self.queue.remove(file)
                    except Exception as e:
                        print(f"[ERROR] Fehler beim Verschieben von {source.name}: {e}")
        
        self.distribute_files()
        self.save_state()
    
    def save_state(self):
        """Speichert aktuellen Zustand"""
        state = {
            'queue': self.queue,
            'monthly_distribution': self.monthly_distribution,
            'last_update': datetime.now().isoformat()
        }
        with open(self.state_file, 'w', encoding='utf-8') as f:
            json.dump(state, f, indent=2, ensure_ascii=False)
    
    def load_state(self):
        """Lädt gespeicherten Zustand"""
        if self.state_file.exists():
            try:
                with open(self.state_file, 'r', encoding='utf-8') as f:
                    state = json.load(f)
                    self.queue = state.get('queue', [])
                    self.monthly_distribution = state.get('monthly_distribution', {})
                    print(f"[INFO] Zustand geladen: {len(self.queue)} Dateien")
            except Exception as e:
                print(f"[WARNING] Fehler beim Laden des Zustands: {e}")
    
    def run(self):
        """Hauptschleife"""
        print("[INFO] Fileee Upload Manager gestartet")
        print(f"[INFO] Warteschlange: {self.queue_folder}")
        print(f"[INFO] Zielordner: {self.target_folder}")
        print(f"[INFO] Monatslimit: {self.monthly_limit}")
        
        self.scan_queue()
        
        # Verarbeite initial
        self.process_current_month()
        
        last_check_day = datetime.now().day
        
        while True:
            try:
                # Prüfe auf neuen Monat
                current_day = datetime.now().day
                if current_day != last_check_day and current_day == 1:
                    print("[INFO] Neuer Monat erkannt, verarbeite Warteschlange...")
                    self.scan_queue()
                    self.process_current_month()
                    last_check_day = current_day
                
                # Scanne alle 5 Minuten
                time.sleep(300)
                self.scan_queue()
                self.process_current_month()
                
            except KeyboardInterrupt:
                print("\n[INFO] Service wird beendet...")
                self.save_state()
                break
            except Exception as e:
                print(f"[ERROR] Unerwarteter Fehler: {e}")
                time.sleep(60)

if __name__ == '__main__':
    # Konfiguration
    QUEUE_FOLDER = r'WEBDAV-QUEUE'
    TARGET_FOLDER = r'WEBDAV-FOLDER'
    MONTHLY_LIMIT = 10
    
    # Starte Manager
    manager = FileeeUploadManager(QUEUE_FOLDER, TARGET_FOLDER, MONTHLY_LIMIT)
    manager.run()

```

Das ganze schreibt ihr in einen Ordner ohne Leerzeichen und könnt es dann als Windows Service definieren.  
Zum Beispiel mit dem [Non-Sucking-Service-Manager](https://nssm.cc/download) - geht sicher auch irgendwie ohne... Aber wie der Name schon sagt, der Weg würde sicher nicht so geil sein...

```powershell
nssm install FileeeUploadManager PYTHON.EXE SCRIPT.PY
nssm set FileeeUploadManager AppDirectory "SCRIPTORDNER"
nssm set FileeeUploadManager DisplayName "Fileee Upload Manager"
nssm set FileeeUploadManager Description "Verwaltet Fileee.com Upload-Warteschlange"
nssm set FileeeUploadManager Start SERVICE_AUTO_START

```

Bei Fehlern braucht ihr

```
nssm set FileeeUploadManager AppStderr LOGFILE
nssm set FileeeUploadManager AppStdout LOGFILE
```

Darauf seht ihr den neuen Dienst im Service Manager (services.msc) und ihr habt einen Hintergrundprozess der monatlich 10 Files von einem anderen Ordner in euren Webdav Ordner schiebt...

Ob man es brauchen kann oder nicht... War ein kleines Feierabendprojekt bei mir...

# OLED-Anpassungen

OLED brennt ein, wie Plasma damals auch. Aber ohne OLED gibts kein Farbspektrum oberhalb der 16 Millionen Farben, daher werden wir wohl irgendwann OLED verwenden müssen.

Bei TVs ist das wenig Problem, da brennt sich das Logo ein und gut ist. Bei Windows allerdings... Windows-Leiste, Browsertableiste, Favicons etc.

- Taskleiste automatisch ausblenden
- Desktop-Diashow aktivieren
- Manche empfehlen gar Wallpaper-Engine, falls keine mehrfach-Desktops gewünscht sind.
- Darkmode an. Weil hier dunklere OLED Pixel tatsächlich weniger Strom bedeutet
- Display-Timeout auf ein paar Minuten stellen
- Firefox-Symbolleiste "Firefox View" entfernen (oder Browser gleich im Vollbild laufen lassen)
- [ClearType deaktivieren](https://www.heise.de/meinung/Windows-Schrift-Renderer-ist-seit-4-Jahren-kaputt-und-Microsoft-juckt-s-nicht-9217225.html)

# Programme

# Alternative Kopiertools

- [FastCopy ](https://fastcopy.jp/)soll die Windows Kernelfunktionen weitestgehend umgehen und deshalb auch höhere Transferraten erreichen.
- [TeraCopy](https://www.codesector.com/teracopy) ist einfach irgendein anderes Programm, um Dinge zu kopieren. Habe ich bisher nur genutzt um zusätzlich noch Prüfsummenchecks zu haben.
- [Robocopy](https://docs.microsoft.com/de-de/windows-server/administration/windows-commands/robocopy) verwendet man eigentlich, weil in Windows mit drin.
- [DD](http://man.he.net/?topic=dd&section=all) kann unter Linux-Systemen auch bitweise kopieren. Damit hab ich ganz früher mal sogar Stick-Treiberchips versehentlich überschrieben.

### Prüfsummentools  


- [Checksum](https://github.com/ferventcoder/checksum) ist aus dem Chocolatey-Universum und tut eben das, was es unter Linux hätte getan, auch in Windows.
- [fciv.exe](https://support.microsoft.com/de-de/topic/d92a713f-d793-7bd8-b0a4-4db811e29559) wäre das offizielle Pendant zu checksum in Windows
- [HashCheck](https://github.com/gurnec/HashCheck/releases/) ist das Ganze in Klickibunti
- [HashTab](https://www.heise.de/download/product/hashtab-30191) hab ich damals immer mit HashCheck verwechselt, weil so ziemlich 1:1 das Gleiche ist. Nur eben nicht mehr weiterentwickelt.

# PowerToys

[PowerToys](https://docs.microsoft.com/de-de/windows/powertoys/) macht gewisse Funktionen, die man von Apple kennt, auch in Windows verfügbar. Das nette dabei: Ist halt auch von Microsoft programmiert, macht also womöglich eher weniger Probleme als Drittanbietersoftware.

PDF Miniaturansicht und Vorschau, Anpassen der Fenstersnapping-Grenzen (FancyZones) etc...

Irgendeine der Funktionen hat sicher jeder, der in der IT arbeitet, irgendwann schon mal benötigt und hat dann sicher irgendwas genommen, was nicht aus dem Hause Microsoft kommt...

- Powertoys Run = Mac OSX Spotlight
- Fancy Zones = Mac Window Snapping

MouseWithoutBorders beißt sich leider mit den Logitech MX Treibern.

# SG TCP Optimizer

Wegen [diesem Artikel bei Borncity](https://www.borncity.com/blog/2023/01/30/microsofts-tcp-murks-in-windows-10-und-11-optimierung-ist-mglich/)...

In short: Weil Microsoft bei so etwas dann doch sehr auf Abwärtskompatibilität setzt, läuft Windows 10 und Windows 11 auf TCP-Einstellungen die über zwei Jahrzehnte alt sind - um eine möglichst große Breite an Netzwerkanschlüssen gleichzeitig supporten zu können und es den Devs bei Microsoft leichter zu machen nicht gleich auf jedem Rechner alles anders haben zu müssen oder irgendein System zu finden sich automatisch an verschiedene Netzwerkeigenschaften anpassen zu müssen.

Übertrieben ausgedrückt: Nach diesen zwei Jahrzehnten arbeiten wir eben nicht mehr mit Dialup und an manchen Leitungen ist ein 8-Millisekunden-Ping echt viel, so dass es eigentlich nicht mehr jahrzehntealter Einstellungen benötigt.

Checkpoint-Computer versteht sich natürlich vor jeder einzelnen Anpassung hiermit.

Gibts von irgendwem [auch als Powershell-Script](https://github.com/MysticFoxDE/WINDOWS-OPTIMIZATIONS), aber eine GUI ist doch ein wenig schneller verstanden, oder?

### Download

-&gt; [für Chocolatey](choco://TCPOptimizer) ([Packageseite](https://community.chocolatey.org/packages/TCPOptimizer))

[Herstellerseite](https://www.speedguide.net/tcpoptimizer.php)

### Begriffserklärungen

aus ChatGPT. Wie immer ohne Gewähr...

1. TCP Window Auto-Tuning: Dies ist ein Mechanismus, der es dem TCP-Protokoll ermöglicht, die Größe des Empfangsfensters automatisch an die Netzwerkbedingungen anzupassen. Durch diese Anpassung kann die Effizienz der Datenübertragung verbessert werden, indem Engpässe im Netzwerk vermieden werden.
2. Windows Scaling Heuristics: Dies ist ein Mechanismus, der von TCP verwendet wird, um die Größe des Empfangsfensters zu erhöhen, wenn es Verzögerungen im Netzwerk gibt. Das Verfahren verwendet eine Heuristik, um zu bestimmen, wie viel das Fenster vergrößert werden soll.
3. Congestion Control Provider: Dies ist ein Software-Modul, das von TCP verwendet wird, um den Datenverkehr auf einer Verbindung zu steuern, um Überlastung und Netzwerkstaus zu vermeiden. Es gibt mehrere Congestion Control Provider, die auf einem Windows-System verfügbar sind, z. B. CUBIC und New Reno.
4. Receive-Side Scaling: Dies ist ein Mechanismus, der von Windows-Servern verwendet wird, um die Verarbeitung von Netzwerkverkehr auf mehrere Prozessoren oder Prozessorkerne zu verteilen. Dadurch kann die Verarbeitungsleistung verbessert werden.
5. R.Segment Coalescing: Dies ist ein Mechanismus, der von TCP verwendet wird, um mehrere kleine TCP-Segmente zu einem größeren Segment zusammenzufassen. Dadurch können die Übertragungseffizienz und die Netzwerklatenz verbessert werden.
6. Time to Live: Dies ist ein Feld im IP-Header, das angibt, wie viele Router ein IP-Paket durchlaufen darf, bevor es verworfen wird. Dies hilft, Endlos-Routen-Schleifen zu vermeiden.
7. ECN Capability: Dies ist eine Funktion, die von TCP und anderen Netzwerkprotokollen verwendet wird, um Endgeräte zu informieren, dass eine Verbindung überlastet ist und Congestion Control-Verfahren verwenden sollte. ECN steht für Explicit Congestion Notification.
8. Checksum Offloading: Dies ist ein Mechanismus, der von Netzwerkadaptern verwendet wird, um die Berechnung der Prüfsummen von IP- und TCP-Paketen zu beschleunigen. Die Prüfsummenberechnung wird vom Netzwerkadapter durchgeführt, anstatt vom Hostsystem.
9. TCP Chimney Offload: Dies ist ein Mechanismus, der von Windows-Servern verwendet wird, um die Verarbeitung von TCP/IP-Verkehr auf den Netzwerkadapter auszulagern. Dadurch kann die CPU-Last auf dem Hostsystem reduziert werden.
10. Large Send Offload: Dies ist ein Mechanismus, der von Netzwerkadaptern verwendet wird, um die Übertragung großer Datenmengen zu beschleunigen, indem die Daten in größeren Einheiten an den Netzwerkadapter gesendet werden.
11. TCP 1323 Timestamps: Dies ist eine Funktion von TCP, die es ermöglicht, Zeitstempel in TCP-Segmenten zu verwenden. Dadurch kann die Round-Trip-Time (RTT) besser bestimmt werden, was wiederum zur Verbesserung der Congestion Control beitragen kann.
12. MTU Latency: Die Maximum Transmission Unit (MTU) bezeichnet die maximale Größe eines Datengramms, das über ein Netzwerk übertragen werden kann. Eine höhere MTU bedeutet, dass größere Datenmengen in einem Paket übertragen werden können, was die Netzwerklatenz reduzieren kann. Dies liegt daran, dass weniger Pakete für die Übertragung von Daten benötigt werden, was insgesamt die Effizienz der Datenübertragung erhöht.
    
    Wenn jedoch die MTU zu groß wird, können Probleme bei der Datenübertragung auftreten, insbesondere wenn Router auf dem Weg zwischen den Endpunkten des Netzwerks kleinere MTUs haben. In solchen Fällen muss die MTU möglicherweise reduziert werden, um eine reibungslose Datenübertragung zu gewährleisten. Die MTU-Latenz bezieht sich also auf die Verzögerung, die durch eine zu große MTU verursacht werden kann.

# Fixes

# deutsche CSVs in Windows reparieren

Ich bin ja mehr so ein Fan davon sich an internationale Standards zu halten. Sprich CSV nur mit Semikolon als Trennzeichen erlaubt. Da bin ich aber in der Minderheit.  
Also:

Control.exe -&gt; "Datums-, Uhrzeit- oder Zahlenformat ändern" -&gt; "Weitere Einstellungen..." -&gt; Listentrennzeichen anpassen.

oder eben "sep=," (oder was auch immer - im Notepad++ vor jede CSV ran)

[Dank an Volker Weber und seinen irre materialistischen Blog](https://vowe.net/2022/02/06/excel-und-csv-richtig-konfigurieren/)

# Verrückten Umgang mit Laufwerksbuchstaben reparieren

Man muss sich ein wenig einlesen, aber richtig konfiguriert macht [USBDLM](https://www.uwe-sieber.de/usbdlm.html#download) all das, was Microsoft in der blödsinnigen Behandlung von Drives als Buchstaben aus dem Alphabet falsch macht oder zu Wünschen übrig lässt.

Das macht noch nichts dagegen, dass das Alphabet eh nur 26 Buchstaben hat und man &gt;26 Drives dann halt als Ordner mounten muss, aber zumindest gibts dann zuverlässige Buchstaben pro Gerät.

Einfach die .chm Files öffnen und einlesen.

Beispiel: Reservierte Drive Letters die USBDLM freihält, die Windows nicht belegen darf - und wenn doch - USBDLM automatisch mit anderen Buchstaben versieht...

# Image Management (DISM, Sysprep, SCCM, Autopilot etc.)

Prio mit Emojis:

⬇️ niedrig

ohne Emoji = normal

⬆️ wichtiger

# DISM Editionswechsel

Hier:

[https://notizlo.ch/eine-windows-10-enterprise-iso-herunterladen/](https://notizlo.ch/eine-windows-10-enterprise-iso-herunterladen/)   
Die Überschrift hat sich die letzten Jahre als gewinnbringender herausgestellt als direkt mit DISM zu machen. Hatte halt damals noch niemand als "Enterprise ISO download" verkauft.

In short:

bei laufenden Systemen

```
dism /online /Set-Edition:Enterprise /productkey:NPPR9-FWDCX-D2C8J-H872K-2YT43
```

bei ISO-Files

- Media Creation Tool Download
- ```Powershell
    Dism /Mount-Image /ImageFile:C:\temp\install.wim /MountDir:C:\wimfile
    Dism /Image:C:\wimfile /Set-Edition:Enterprise
    Dism /Unmount-Image /MountDir:C:\wimfile /Commit
    ```

Ist ganz praktisch, weil man dann auch gleich die künftige C-Platte manipulieren kann. Für direkte Programminjektionen würde ich aber Sysprep empfehlen. [Kreuzverweis auf c't 04/2014 hier](https://www.heise.de/select/ct/archiv/2014/4/seite-142), die haben mir damals die ganze Thematik schon näher gebracht.

Lose Weitersammlung zusammengehöriger Dinge:

Windows 10 KMS Keys: [https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/jj612867(v%3Dws.11)](https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/jj612867(v%3Dws.11))  
Windows 11 KMS Keys: [https://docs.microsoft.com/en-us/windows-server/get-started/kms-client-activation-keys](https://docs.microsoft.com/en-us/windows-server/get-started/kms-client-activation-keys)  
Aufgebohrtes MCT: [https://github.com/AveYo/MediaCreationTool.bat](https://github.com/AveYo/MediaCreationTool.bat)

# ⬇️ 3rd-party Programme für Betriebssystemimages

Kurze Liste der Dinge die ich mit in Windows-Images integrieren würde, weil sie doch recht zentrale Aufgaben erfüllen.  
Damit meine ich keine fetten Biester oder irgendwelche Anwender-Software, sondern Admin-Extrafunktionen, überwiegend Kommandozeile:

- [ffmpeg](https://ffmpeg.org/download.html)
- [yt-dlp](https://github.com/yt-dlp/yt-dlp)
- [usbdlm](https://www.uwe-sieber.de/usbdlm.html)
- [chocolatey](https://chocolatey.org/install) (eigentlich sollte man ja winget verwenden)
- [git](https://git-scm.com/)
- [gpg4win](https://www.gpg4win.de/download-de.html)
- [sudo](https://github.com/janhebnes/chocolatey-packages/tree/master/Sudo) 😋

# Platten für SCCM vorbereiten

<p class="callout warning">Bevor ihr so etwas macht überlegt euch ob ihr die Computer nicht mit dem bestehenden Image vom Hersteller ohne große Neuinstallationen an SCCM und Intune ranflanschen könnt.  
Spart ganz gehörig Zeit und damit Lohnkosten und geht auch manchen Problemen aus dem Weg, weil man sich dann nicht groß um Treiber Gedanken machen muss.  
Booten, Intune-Join, Autopilot oder anderes Softwarerollout ran und fertig.   
Windows bietet mit der OOBE (Erststart neuer Rechner) für Unternehmens-Konten genau das...  
</p>

Der Endpoint Configuration Manager braucht eine gewisse Plattenstruktur aus verschiedenen Partitionen. Existiert diese nicht schlägt die festgelegte Task Sequence fehl.  
Dies ist die Plattenstruktur die der MECM erwartet, als ausführbares Powershell script.  
Ihr müsst Diskpart ohne /s ausführen wenn ihr das händisch macht, EOF wäre das Endzeichen fürs Diskpart-Script.

<p class="callout info">F8 ist der Hotkey für die Kommandozeile  
</p>

Deutsches Keyboard Layout:

```
wpeutil SetKeyBoardLayout 0407:00000407
```

Doppelpunkt ist das große Ö auf der deutschen Tastatur wenn die Software ein englisches Layout interpretiert.  
Und fünf folgende Nullen...

```powershell
# PowerShell script
diskpart /s <<EOF
Select Disk 0
Clean
Convert gpt
Create partition efi size=200
Assign letter=s
Format quick fs=FAT32
Create partition msr size=128
Create Partition Primary
Select Partition 3
Active
Assign letter=c
Format quick fs=NTFS
Exit
EOF
```

Für Legacy lässt man die EFI-Partition weg

```powershell
diskpart /s <<EOF
Select Disk 0
Clean
Create Partition Primary
Select Partition 1
Active
Assign letter=c
Format quick fs=NTFS
Exit
EOF
```

gestohlen von [SCCM OSD DiskPart Commands Troubleshooting - Prajwal.org](https://www.prajwal.org/sccm-osd-diskpart-commands-troubleshooting/)

Dies ist meiner Erfahrung nach für ca. 70 % aller SCCM Rollout Fehler die Lösung, ist dies nicht erfolgreich überprüft die Treiber die vor Installation in das Image injected werden müssen. Dann fehlen dort Treiber   
(oder ihr habt die falschen Laptops, die solch Drittanbieter-Installation-Injects noch brauchen)

# WQL Abfragen

### Testen

Im Endpoint Configuration Manager müssen für die Tasksequenzen in den Optionen keine Shell Befehle eingetragen werden sondern WQL queries.

```powershell
wmic computersystem get model
```

wird also zu SQL ähnlichem:

```sql
SELECT * FROM Win32_Computersystem WHERE model LIKE "..."
```

mit wbemtest.exe kann dies auf den Geräten selbst geprüft werden.

Dazu muss die WMI Klasse gefunden werden, die ab und an nicht so heißt wie dessen Abfrage in der Shell.

### Tasksequenzen für Treiber

1. Treiberimport
2. Treiberpaket definieren
3. Tasksequenz für dieses Treiberpaket an WQL Query binden

#### Lenovo X1 Carbon

Eigentlich mag ich die Serie, halt auch wegen ihrer Treiber. Es ist äußerst selten dass Lenovo Treiber einen BSOD produzieren.  
  
Der Mensch der aber entschieden hat in mehreren Generationen nacheinander die Model numbers gleich zu belassen gehört gefeuert. Das ist sonst bei Lenovo nirgends der Fall, ist mir zumindest nicht untergekommen. Gen9, Gen10 und Gen11 in gleicher Ausstattungsstufe aber mit gleichen model numbers zu benennen... grr....

##### Tasksequenzoptionen

```sql
SELECT * FROM Win32_ComputerSystemProduct WHERE Vendor LIKE "%LENOVO%"
SELECT * FROM Win32_ComputerSystemProduct WHERE Version LIKE "%ThinkPad X1 Carbon Gen 11%"
```

Was genau zurückgegeben wird scheint wohl egal zu sein. Hier sind es ja ganze Tabellen. Hauptsache die Anfrage ist erfolgreich und das LIKE kann sich auflösen.

# Trojanerjagd

Ganz früher gab es mal HijackThis.   
Das wird auch noch weitergeführt, allerdings als Fork, und das von einem Ukrainer. Wegen des Krieges mit Russland bitte vorsichtig sein: [https://github.com/dragokas/hijackthis](https://github.com/dragokas/hijackthis) (bei Sicherheit zählt halt auch sowas …)  
Dort ist Krieg und keiner kann garantieren dass Hijackthis millitärisch durch Russland genutzt wird.

FRST – oder das Farbar Recovery Scan Tool – macht das Gleiche. [BleepingComputer-Downloadlink, weil Originaldownload hinter Adblock-Gemecker](https://www.bleepingcomputer.com/download/farbar-recovery-scan-tool/).

Wichtig ist, dass weder HijackThis noch FRST irgendwas tut, das erstellt euch ein File mit allen Daten aus allen relevanten Bereichen in denen Trojaner und Viren drin sein könnten. Dienste, laufende Prozesse, etc.

Früher gab es mal die berühmte Logfileauswertung, da konnte man dann alle Treffer mit Anomalien (z. B. Dienste deren Prozesse in einem anderen Speicherort liegen etc.) easy erkennen und die Dienste dann abschießen. Die gibts leider nicht mehr.

Wenn man also selbst die Zeit nicht investieren will – die Logs sind umfangreich, weil halt alle Details zu allen relevanten Bereichen – kann man stattdessen das [https://www.trojaner-board.de](https://www.trojaner-board.de) damit nerven.

Dort scheints i. d. R. folgender Prozess zu sein:

1. [https://www.malwarebytes.com/solutions/rootkit-scanner](https://www.malwarebytes.com/solutions/rootkit-scanner)
2. [adwCleaner](https://de.malwarebytes.com/adwcleaner/) und [Anleitung dazu](http://anleitung.trojaner-board.de/adwcleaner-anleitung-browser-viren-entfernen_24)
3. neues Logfile vom FRST fürs Forum.

Die Logfiles von FRST oder HijackThis enthalten wie gesagt sämtliche Einträge, die relevant sein könnten. Ob diese Einträge dort richtig sind oder eine Anomalie darstellen ist dann eure Aufgabe.

Das bedeutet in der Regel umfangreiche Recherchearbeit, um festzustellen, ob Einträge von Windows stammen, zu einem bestimmten Programm gehören oder von Drittanbieter-Software gewollt sind. Also ziemliche Fummelarbeit.

# Multifaktor-Authentifizierung

### Hardwarekeys

- IMMER pingelig auf die Lieferung achten:  
    Damals von Paypal die Karten (noch Symantec - auch um 2015 rum) hatten mehrfach Probleme: die erste ging gar nicht, die zweite kam in aufgerissenem und neu verklebtem Umschlag, die dritte kam okay an, hatte aber kaum noch Batterie, bis ich mir in Amerika persönlich ein anderes Symantec-Modell abgeholt hatte welches nicht im Scheckkartenformat ist.  
    Nun unterstützt PayPal die Symantec nicht mehr, nur noch FIDO2.
- YubiKey Neo 2015 war okay, alles Originalverpackt
- Der Google Titan Key von 2023 hatte ein Siegelsticker, der scheinbar nicht sonderlich fest klebte und eine Hälfte davon nicht mehr "ums Eck" klebte. Eine Kompromittierung wäre theoretisch möglich gewesen

### Microsoft Windows und Hardwarekeys

- Windows 11 kann 2-Faktor-Authentifizierung im Web per Windows Hello. Das ist eine ziemlich dumme Idee, wenn die Maschine nicht permanent am Mann ist. Windows Hello bindet sich an den TPM-Chip auf dem Mainboard. Daher kann man das als Hardwarekey sehen.
- genau so kann Dashlane inzwischen Passkeys speichern. Andere Passwortmanager sicherlich auch. Dashlane synchronisiert aber übers Internet.  
    Passwortmanager vermurksen also den Begriff Hardwarekey, weil sie das rein an Software binden.
- Blöd dazu ist dass Microsoft natürlich die Nutzung von Windows Hello als Passkey möglichst einfach gestaltet, während die echten USB-Securitykeys deutlich mehr Klicks zum Einrichten benötigen. Viele unbedarfte Anwender landen also in dem eigenen TPM-Chip auf dem Mainboard. (Psychologie: Weg des geringsten Widerstands bzw. ungelesenes Durchklicken der ersten Optionen)
- Es gibt mehrere FIDO-Standards, ein YubiKey Neo aus 2015 funktioniert z.B. wegen dem älteren Standard nicht mit Microsoft, dafür aber mit Google und und fast überall anders auch, weil diese den alten Standard noch zulassen.
- Auch Security Keys müssen also zumindest jedes Jahrzehnt ersetzt werden, weil sich die Standards und Sicherheitstechniken entwickeln.

# Programm-Listenleichen entfernen

```
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Uninstall
```

oder

```
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall
```

Einträge durchgehen, nicht mehr benötigte entfernen.

Das nimmt die Möglichkeit installierte Programme zu deinstallieren, also müsst ihr euch sicher sein dass ein Programm z.B. tatsächlich nicht mehr über die normalen Wege entfernt werden kann, oder schon entfernt ist, aber vergessen hat den Eintrag zu löschen.  
Die Uninstall.exe wird noch im Programmordner liegen, aber die in Windows integrierte Option wird dadurch verhindert.  
  
Können Hacker auch nutzen um Dinge zu installieren, sie aber nirgendswo anzeigen zu lassen.

Dieser Weg war nötig bei:

- [whalebird.social](https://whalebird.social/) - mehrere Einträge in der Programmliste nach Installation neuer Version
- [blender.org](https://www.blender.org/) - Deinstallation entfernte den Listeneintrag nicht, so dass winget und co Blender in kurzer Zeit wieder installiert hatten.

# Windows-Imaging und Deployment

### Voraussetzungen

- [Windows ADK](https://learn.microsoft.com/de-de/windows-hardware/get-started/adk-install#download-the-adk-101253981-september-2023) installiert
- [Windows 11 ISO](https://www.microsoft.com/de-de/software-download/windows11) auf kurzem Pfad im Unterordner von C:\\ vorliegend
- Lokale Administrationsrechte
- Am liebsten eine abgeschottete Maschine ohne Netzwerk. Arbeit mit WIM Images ist ein guter Sprungpunkt für Viren und Schadhaftes.

### Grundlagen

- ISO Images sind mit aktuellen Archiver Programmen wie ZIP-files behandelbar.
- Mir ist noch kein Weg ungergekommen den neuen Installationsprozess von Windows 11 mit angepassten WIM-Images zu nutzen. Man muss immer auf den Windows 10 Installer zurückfallen. Das OOBE aus dem Installer ist dann aber das moderne.
- Mit DISM macht ihr Dinge wie Treiber und Feature-Aktivierungen vorab, mit Sysprep bastelt ihr eigene Programme mit ins Image, wobei das eigentlich eine Intune-Softwareverteilung sein sollte.
- Es ist prinzipiell völlig egal welche Edition als Image gewählt wird, sofern man nicht mit den Education- oder N-Versionen (keine Medientreiber) arbeitet. Editionen sind hinterher alle mit dism /Online bügelfähig.
- DISM /Online bezeichnet immer das aktuell laufende System, bei allem anderen müssen die Pfade mitgegeben werden.

#### MediaCreationTool

[Hier](https://www.microsoft.com/de-de/software-download/windows11), Punkt "Erstellen von Installationsmedien für Windows 11".

- USB Modus: Schreibt MBR-FAT32
- ISO Modus: Lädt eine .ISO-Datei irgendwo hin

bis vor kurzem gab es auch noch einen direkten CD-Brenn-Modus.

<p class="callout info">Das MCT lädt euch bestenfalls die ISO runter. Software wie [balenaEtcher](https://etcher.balena.io/) oder [rufus](https://rufus.ie/de/) schreiben euch weitaus verwendbareres GPT-NTFS.</p>

#### Ventoy

[https://www.ventoy.net/](https://www.ventoy.net/)

Relativ Easy: Ein Stick formatieren, danach nur noch ISO-Files drauf, alle sind bootbar.  
Ich hab den Entwickler mal gefragt warum er wegen seiner Secure Boot Keys nicht den weg über die Hersteller nimmt um seinen Key in die Firmwares der Mainboardhersteller zu bringen, Antwort steht aus.

Tut aber erst mal nix zur Sache, nur wirkt eine recht [spärliche Erklärung zum Import des Secure Boot Keys](https://www.ventoy.net/en/doc_secure.html) jetzt auch nicht unbedingt vertrauenserweckend.  
Ist so die Kategorie "deutlich einfacher zu verwenden, fällt dir aber trotzdem irgendwann auf die Nase"-Software.

### Dateitypen

#### .swm

Splitted WIM

Vom Media Creation Tool erstellte USB sticks, wegen FAT32 gesplittet.  
Also Zusammenwalzen:

```
Dism.exe /export-image /sourceimagefile:install.swm /swmfile:install*.swm /sourceindex:1 /destinationimagefile:install.wim /CheckIntegrity
```

daraus kommt aber leider kein gültiges .wim image mit dem auch im Rahmen von dism.exe gearbeitet werden könnte.

<p class="callout info">der direkte ISO Download von Windows 11 hat noch install.wim</p>

#### .esd

erlaubt mit /Compress:Recovery bessere Kompression, sollte aber nahezu 1:1 mit .wim vergleichbar sein.  
Durchtauschbar mit:

```
dism /Export-Image /SourceImageFile:install.esd /SourceIndex:1 /DestinationImageFile:install.wim
```

Index muss mit dabei sein:

```
dism /Get-WimInfo /WimFile:install.esd
```

<p class="callout info">Ich würde wegen der Kompression eigentlich immer den Weg über die .esd gehen</p>

#### .wim

das klassische Imageformat mit dem man am liebsten arbeitet. Dahin bringe ich eigentlich immer alles bevor ich mit dem Mounting loslege.

### Vorbereitung

1. [ISO Download](https://www.microsoft.com/de-de/software-download/windows11) - Menüpunkt "Herunterladen eines Windows 11-Datenträgerimages (ISO)" - die anderen Optionen sind Köse.
2. ISO mit 7zip öffnen
3. Unterordner .\\sources öffnen
4. install.wim entpacken
5. Passendes Image raussuchen
    
    ```
    dism /Get-Wiminfo /wimFile:install.wim
    ```
6. mount Ordner erstellen
7. richtiges Image mounten ```
    Dism /Mount-Image /ImageFile:install.wim /index:5 /MountDir:.\mount
    ```

### Admin Tools ablegen

<p class="callout info">Merksatz: Dinge die ihr eventuell brauchen könntet... Keine Programme dahin direkt installieren, das ist später eine Sache von sysprep...</p>

Jetzt habt ihr sicher immer irgendwas, was ihr so oder so im laufenden Betrieb braucht. Erstellt euch in dem mount Ordner jetzt einfach einen inst-Ordner (oder wie auch immer ihr den nennt) und legt da euer Zeug ab. [Office MDT Kit](https://www.microsoft.com/en-us/download/details.aspx?id=49117) oder so...  
Das landet dann bei den Rechnern automatisch mit auf der Systempartition.

### Treiber reinbasteln

Kein Bock auf die Treibertools oder langwierige Optionale Updates?  
Ihr seid euch sicher dass ihr die Images oft auf Systemen des gleichen Herstellers installiert?  
Eure eigene Maschine ist zufällig genau eines dieser Systeme?

Dann los:

1. Treiber aus dem aktuellen System holen: ```
    dism /online /export-driver /destination:.\driver
    ```
    
    Die würden auch unter %windir%\\System32\\DriverStore liegen, aber wir arbeiten mit DISM, lieber auch durch das Tool vorbereiten lassen.
2. Die Treiber ins Image rein: ```
    dism /Image:.\mount /Add-Driver /Driver:.\driver /Recurse
    ```

### Editionswechsel

Falls das nicht durch Intune gemacht wird

```
dism /Image:.\mount /Set-Edition:Enterprise
```

### Platz für mehr

dism kann noch so viel mehr... Dafür ist aktuell keine Zeit das hier zu tippen. Kommt bei bedarf noch...

### Abschluss

```
dism /Unmount-Image /MountDir:.\mount /Commit
dism /Export-Image /SourceImageFile:install.wim /SourceIndex:1 /DestinationImageFile:install.esd /DestinationName:"EUER FANCY IMAGE NAME"  /Compress:recovery
```

Als ESD, weil Kompression. Durch die Bearbeitung wird das Image von ca. 4-5 GB doch recht gut aufgebläht.  
und dann mit dem Archiver Programm die install.wim durch die install.esd austauschen.

### Sysprep

tbd...

# Linux Subsystem

### Internetverbindung

WSL hat eine sehr dämliche Eigenschaft eigentlich nie den richtigen DNS Server integriert zu haben, zumindest nicht über VPN-Verbindungen. Man muss also eigentlich immer sich um DNS kümmern...

Also schreiben wir uns eine ganze Latte DNS Server in die /etc/resolv.conf, dann haben wir auch wieder Namensauflösung

```bash
#!/bin/bash

set -e
if lsattr /etc/resolv.conf | grep -q 'i'; then
   chattr -i /etc/resolv.conf
fi
rm -f /etc/resolv.conf
cat > /etc/resolv.conf << EOF
nameserver 8.8.8.8
nameserver 8.8.4.4
nameserver 1.1.1.1
nameserver 1.0.0.1
nameserver 9.9.9.9
nameserver 149.112.112.112
nameserver 84.200.69.80
nameserver 84.200.70.40
nameserver 45.90.28.123
nameserver 45.90.30.123
nameserver 134.102.20.20
nameserver 134.102.200.14
nameserver 134.155.96.51
nameserver 134.155.96.52
nameserver 134.155.96.53
nameserver 132.230.200.200
nameserver 132.230.201.111
nameserver 141.2.98.233
nameserver 141.2.149.10
nameserver 141.2.213.41
nameserver 141.2.149.10
nameserver 141.2.98.225
nameserver 141.2.149.10
nameserver 129.206.100.126
EOF
chattr +i /etc/resolv.conf
```

Die IPs der Nameserver sind alle öffentlich bekannt und am Ende noch ein paar Unis mit ihren DNS-Servern

# 4K-Filme und Windows

### Grundproblem

Es gibt quasi kaum eine Art Filme auf Windows anschauen zu können, wenn sie 4K oder gar HDR sind. Bietet kein Streamingdienst an, auch alle Shops zeigen Warnungen.

Einige vermuten, dass das es die letzte Bastion von DRM von den Medienhäusern ist, wenn sie schon bei FullHD nicht mehr auf DRM setzen können.

So scheint es Software wie [Cyberlink PowerDVD](https://de.cyberlink.com/products/powerdvd-ultra/features_de_DE.html) noch zu brauchen und auch von Laufwerken gibt es oft [separate UHD-Varianten](https://de.pioneer-blurayodd.eu/?lang=de) die sich letztlich nur in der Firmware unterscheiden. Das ist der einzig komplett gewollte Weg der Anbieter. Ein "Software-Krebsgeschwür" welches Jährlich für ca. 100€ erneuert werden muss und dann auch noch präparierte Laufwerke...

### "Grauzonen"-Betrieb von 4K-Filmen auf Windows

<p class="callout danger">Das ist keine Rechtsberatung, ich bin kein Anwalt und ihr handelt auf eigene Gefahr:</p>

Problem: ALLE Anbieter die 4K-Filme als Download oder Stream anbieten unterstützen kein Windows. Selbst Microsoft kann ausschließlich Xbox. Windows Nutzer schauen bei 4K Filmen also immer in die schwarze Röhre.

- Streaming Film in 4K kaufen.
- Warnungen über nicht unterstützte Systeme bestätigen.
- Normal bezahlen.
- Warez Foren im Internet suchen
- Gleicher Film, gleiche Auflösung, gleiche Sprachen wie im gekauften Streaming Film
- diesen Film aus dem Warez Forum herunterladen
- enjoy

Wichtig an der Sache ist, dass es ein Streaming-Kauf sein muss, Bluray Disks hängen an gewissen Lizenzkosten der verschiedenen Abspielgeräte und sind ein rechtlich noch größerer Fuckup als Streaming...

Anklage?

Wart ihr nicht so doof und habt Bittorrent verwendet (bei Bittorrent seid ihr immer Anbieter, also Uploader), weißt ihr der Anwaltskanzlei den Kauf nach und weißt nach dass ihr diesen Film wärend der 14-Tägigen Widerrufsfrist nicht zurückgegeben habt. Seid ihr noch in der Widerrufsfrist lasst euch den Fakt dass ihr nicht vorhabt diesen Streaming-Kauf zu Widerrufen von einem Freund bezeugen und unterzeichnen.   
Die Rechnung für den Streaming-Kauf und eventuelle Zeugenaussagen gehen zurück an die Anwaltskanzlei.  
Hier könnt ihr dann auch erläutern dass ihr mit dem Kauf ein Recht gekauft habt diesen Film unbegrenzt häufig sehen zu dürfen, euch dies der Händler aber nicht möglich machen konnte.

Dann müsst ihr schauen was passiert...

Das gleiche könnte man mit Streamingfilmen im Leih-Modus auch ausprobieren, hier solltet ihr nachweisen können den Film aus dem Warez-Forum von eurer Festplatte (auch aus dem Papierkorb) rechtzeitig vor Ablauf der Leihfrist auch wieder gelöscht zu haben.

#### "For your consideration" DVDs

Das ganze ist dann auch wieder so eine Sache mit Netflix- oder Streaming-Eigenproduktionen die es ansich nicht gibt. Wir gehen hierfür noch mehr in die Grauzone rein, aber zumindest solltet ihr zum Warez-Forendownload hier einen Kauf einer "Testscreening-DVD" (oft auch "For your consideration" genannt) nachweisen können, die oftmals auch einfach nicht ankommt weil sie über die halbe Weltkugel reisen muss oder beim Zoll vernichtet wird...

<p class="callout info">wer was dagegen hat, findet die Mastodon-Kontaktdaten unten im Footer... Auch gerne für Hinweise nutzen. Wir müssen das Gesetz gehörig verbiegen und interpretieren um 4K-Filme auf Windows-Computern, die kein optisches Laufwerk haben zu ermöglichen.</p>

# Lateral Movement Admin -> System

> **Lateral movement** refers to the techniques that cyber attackers, or [threat actors](https://en.wikipedia.org/wiki/Threat_actor "Threat actor"), use to progressively move through a [network](https://en.wikipedia.org/wiki/Computer_network "Computer network") as they search for the key data and assets that are ultimately the target of their attack campaigns.<sup class="reference" id="bkmrk-%5B1%5D">[<span class="cite-bracket">\[</span>1<span class="cite-bracket">\]</span>](https://en.wikipedia.org/wiki/Lateral_movement_(cybersecurity)#cite_note-Security_Week-1)</sup><sup class="reference" id="bkmrk-%5B2%5D">[<span class="cite-bracket">\[</span>2<span class="cite-bracket">\]</span>](https://en.wikipedia.org/wiki/Lateral_movement_(cybersecurity)#cite_note-2)</sup><sup class="reference" id="bkmrk-%5B3%5D">[<span class="cite-bracket">\[</span>3<span class="cite-bracket">\]</span>](https://en.wikipedia.org/wiki/Lateral_movement_(cybersecurity)#cite_note-3)</sup> While development of more sophisticated sequences of attack has helped threat actors develop better strategies and evade detection as compared to the past, similar to planning a [heist](https://en.wikipedia.org/wiki/Theft "Theft"), cyber defenders have also learned to use lateral movement against attackers in that they use it to detect their location and respond more effectively to an attack.<sup class="reference" id="bkmrk-%5B1%5D-1">[<span class="cite-bracket">\[</span>1<span class="cite-bracket">\]</span>](https://en.wikipedia.org/wiki/Lateral_movement_(cybersecurity)#cite_note-Security_Week-1)</sup>

[Lateral movement (cybersecurity) - Wikipedia](https://en.wikipedia.org/wiki/Lateral_movement_(cybersecurity))

### PSexec Alternativen

wenn es bereits bestehende Sicherheitsmaßnahmen gibt, wie z.B. den Aufruf von \\\\live.sysinternals.com\\tools\\psexec64.exe zu verhindern.

```powershell
schtasks /create /tn RunAsSystem /tr "cmd.exe /c your_command" /sc once /st 00:00 /ru SYSTEM /f
```

- [Impacket](https://github.com/fortra/impacket) -&gt; [psexec.py](https://github.com/fortra/impacket/blob/master/examples/psexec.py) &amp; [smbexec.py](https://github.com/fortra/impacket/blob/master/examples/smbexec.py) - SMBexec soll weniger AV triggern...

```powershell
Enable-PSRemoting
$sess = New-PSSession -ComputerName <Name>
Enter-PSSession -ComputerName <Name> OR -Sessions <SessionName>
```

### Drittanbieter

- [Windows Lateral Movement with smb, psexec and alternatives | nv2lt - Scratching the Surface](https://nv2lt.github.io/windows/smb-psexec-smbexec-winexe-how-to/)
- [GitHub - S1ckB0y1337/Active-Directory-Exploitation-Cheat-Sheet: A cheat sheet that contains common enumeration and attack methods for Windows Active Directory.](https://github.com/S1ckB0y1337/Active-Directory-Exploitation-Cheat-Sheet?tab=readme-ov-file#lateral-movement)

# autounattend.xml Generator

### [https://schneegans.de/windows/unattend-generator/](https://schneegans.de/windows/unattend-generator/)

# Intune / Entra / Azure

Microsoft Cloud

# Intune

Die große Umstellung von OnPrem Umgebungen ist kein technisches Problem, es ist ein konzeptuelles Problem:  
OnPrem Umgebungen sind überwiegend Gerätefokussiert, während Intune kaum bis gar nicht Gerätefokussiert sein kann.

Es ist also nicht möglich Richtlinien 1 zu 1 in Intune abzubilden ohne diese von Grund auf neu zu konzipieren.

Ich rede hier fast ausschließlich von Hybridumgebungen.

- Alles was nicht App-Installation ist (weil App-Installationen One-Time sind), ist User-Gebunden
- Auch die auf Computer gebundenen Richtlinien sind von Usern abhängig.  
    Maschinengebunden bedeutet, dass Intune bei jeder Useranmeldung prüft ob für das Gerät, an dem sich gerade angemeldet wird eine zugewiesene Policy hat und wendet diese bei jeder User-Anmeldung an.
- Der Zustand des angemeldeten Users wird abseits der Useranmeldung laut meines Wissens alle vier Stunden überprüft. (PRT-Token)
- Ist beim regelmäßigen Check kein User angemeldet (auch im gesperrten Zustand) kann der Lizenzcheck nicht vollzogen werden und die Intune Richtlinien werden entfernt.
- Nicht lizensierte User und lokale User haben keine Intune-Lizenz die bei der Anmeldung geprüft werden könnte, also können für diese auch keine Policies appliziert werden. Auch können lokale User nicht nachvollziehen ob auf dem Gerät Policies aktiv sind, weil auch deren Grundlagen (z.B. Registry-Keys) entfernt werden.  
    Dies trifft auch alle Richtlinien, die in Ihrer OnPrem-Variante komplett Maschinengebunden waren.  
    **Intune zeigt Policies an, die auf den SYSTEM-User appliziert worden sind. Das Vehikel dafür ist aber weiterhin der lizensierte Intune-User.**
- Da der Zustand ohne angemeldeten User im SYSTEM User-Kontext läuft - welcher nicht intune-lizensiert werden kann - wirken die konfigurierten Intune-Policies nicht im Zustand ohne jegliche User-Anmeldungen.

Die Lösung für das Dilemma ist leider eine Doppelkonfiguration OnPrem GPO und Intune, hier allerdings OnPrem GPOs nur noch dafür genutzt lokale User des Computers zu steuern.   
Dies bringt uns leider wieder in Richtung Konfliktregulierungen zwischen OnPrem und Intune.

# Win 11 ohne Restriktionen & Accountpflicht

Leidiges Thema

Sollte man mit sauber konfiguriertem User Setup in Intune tun, dann ist's der User der halt sein Passwort eintippt und der Admin der das halt freigeben muss. Wehrt sich aber jeder Alteingesessene davor weil es einer amerikanischen Logik folgt.

Warum auch immer das keiner tut... - weil es bedeuten würde mit viel mehr Azure Only Notebook Kram endlich von seiner OnPrem Domäne los zu kommen... Wie immer die verfügbare Zeit...

Also los mit den Umgehungsmethoden von sauberer Azure Konfiguration:

### wenns zu spät ist

Shift + F10

```
start ms-cxh://setaddlocalonly
```

Ab 25H2:

```
WinJS.Application.restart("ms-cxh://localonly")
```

Admin anlegen

#### bypassnro

##### neuer Weg

Shift + F10

```
reg add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE /v BypassNRO /t REG_DWORD /d 1 /f
shutdown /r /t 0
```

##### alter Weg

Netzwerk trennen

Shift + F10

```
oobe\bypassnro
```

### wenns noch davor ist

#### unattended.xml

[Generate autounattend.xml files for Windows 10/11](https://schneegans.de/windows/unattend-generator/)

Mit zur Setup.exe auf dem USB liegen

### setup.exe /product server

InPlace Upgrade  
Netzwerk trennen, ISO einbinden

```
setup.exe /product server
```

unabhängig von der Edition in der ISO

### ungetestete Dinge

# Vibe Coding

Nennt man so wenn man eigentlich nur KI dafür nutzt und wenig bis kaum eigenes einbringt.  
**Ihr solltet verstehen und lesen können was euch die KI hier generiert um die generelle Funktionsweise zumindest prüfen zu können.**

KI ist am stärksten in gut dokumentierten Scriptsprachen mit vielen Stack Overflow Beispielen: Also Python noch vor Powershell.

# Musiksortierung

### Künstler-Unterordner

#### Beispiel-Ausgangssituation

D:\\Musik\\ABBA - Dancing Queen.mp3  
D:\\Musik\\Culcha Candela.flac

#### Beispiel-Zielsituation

D:\\Musik\\ABBA\\ABBA - Dancing Queen.mp3  
D:\\Musik\\Culcha Candela\\Culcha Candela.flac

```powershell
# Musikdateien nach Künstler sortieren
# Unterstützt Formate: "Tracknummer - Künstler - Titel" oder "Künstler - Titel"

# Musik-Dateierweiterungen
$musikErweiterungen = @('.mp3', '.flac', '.m4a', '.wav', '.ogg', '.wma', '.aac')

# Funktion zum Bereinigen von Dateinamen für Ordner
function Get-CleanFolderName {
    param($name)
    # Entferne ungültige Zeichen für Ordnernamen
    $invalid = [System.IO.Path]::GetInvalidFileNameChars()
    $cleanName = $name
    foreach ($char in $invalid) {
        $cleanName = $cleanName.Replace([string]$char, '')
    }
    return $cleanName.Trim()
}

# Benutzer nach Quellordner fragen
Write-Host "=== Musikdateien-Sortierer ===" -ForegroundColor Cyan
Write-Host ""
$quellOrdner = Read-Host "Bitte gib den Pfad zum zu sortierenden Ordner ein"

# Prüfe ob Ordner existiert
if (-not (Test-Path $quellOrdner)) {
    Write-Host "FEHLER: Der angegebene Ordner existiert nicht!" -ForegroundColor Red
    Read-Host "Drücke Enter zum Beenden"
    exit
}

# Hole alle Musikdateien im Hauptordner (nicht in Unterordnern)
$dateien = Get-ChildItem -Path $quellOrdner -File | Where-Object {
    $musikErweiterungen -contains $_.Extension.ToLower()
}

if ($dateien.Count -eq 0) {
    Write-Host "Keine Musikdateien im Ordner gefunden." -ForegroundColor Yellow
    Read-Host "Drücke Enter zum Beenden"
    exit
}

Write-Host ""
Write-Host "Gefundene Musikdateien: $($dateien.Count)" -ForegroundColor Green
Write-Host ""

# Analysiere Dateien und zeige Vorschau
$verschiebePlan = @()

foreach ($datei in $dateien) {
    $dateiName = [System.IO.Path]::GetFileNameWithoutExtension($datei.Name)
    $kuenstler = $null
    
    # Versuche Format "Tracknummer - Künstler - Titel" (z.B. "01 - Beatles - Hey Jude")
    if ($dateiName -match '^(\d+)\s*-\s*(.+?)\s*-\s*(.+)$') {
        $kuenstler = $matches[2].Trim()
    }
    # Versuche Format "Künstler - Titel" (z.B. "Beatles - Hey Jude")
    elseif ($dateiName -match '^(.+?)\s*-\s*(.+)$') {
        $kuenstler = $matches[1].Trim()
    }
    
    if ($kuenstler) {
        $kuenstlerOrdner = Get-CleanFolderName $kuenstler
        $verschiebePlan += [PSCustomObject]@{
            Datei = $datei.Name
            Kuenstler = $kuenstlerOrdner
            QuellPfad = $datei.FullName
        }
    } else {
        Write-Host "WARNUNG: Kann Künstler nicht ermitteln für: $($datei.Name)" -ForegroundColor Yellow
    }
}

if ($verschiebePlan.Count -eq 0) {
    Write-Host "Keine Dateien mit erkennbarem Format gefunden." -ForegroundColor Yellow
    Read-Host "Drücke Enter zum Beenden"
    exit
}

# Zeige Vorschau gruppiert nach Künstler
Write-Host "Vorschau der Sortierung:" -ForegroundColor Cyan
Write-Host ("=" * 60)
$verschiebePlan | Group-Object Kuenstler | Sort-Object Name | ForEach-Object {
    Write-Host "`n[$($_.Name)]" -ForegroundColor Green
    $_.Group | ForEach-Object {
        Write-Host "  - $($_.Datei)" -ForegroundColor Gray
    }
}
Write-Host "`n" ("=" * 60)
Write-Host ""

# Bestätigung einholen
$bestaetigung = Read-Host "Möchtest du diese $($verschiebePlan.Count) Datei(en) sortieren? (J/N)"

if ($bestaetigung -notmatch '^[jJyY]') {
    Write-Host "Vorgang abgebrochen." -ForegroundColor Yellow
    Read-Host "Drücke Enter zum Beenden"
    exit
}

# Dateien verschieben
Write-Host ""
Write-Host "Sortiere Dateien..." -ForegroundColor Cyan
$erfolg = 0
$fehler = 0

foreach ($eintrag in $verschiebePlan) {
    try {
        $zielOrdner = Join-Path $quellOrdner $eintrag.Kuenstler
        
        # Erstelle Künstler-Ordner falls nicht vorhanden
        if (-not (Test-Path $zielOrdner)) {
            New-Item -Path $zielOrdner -ItemType Directory -Force | Out-Null
        }
        
        $zielPfad = Join-Path $zielOrdner (Split-Path $eintrag.QuellPfad -Leaf)
        
        # Verschiebe Datei
        Move-Item -Path $eintrag.QuellPfad -Destination $zielPfad -Force
        Write-Host "✓ $($eintrag.Datei) -> $($eintrag.Kuenstler)\" -ForegroundColor Green
        $erfolg++
    }
    catch {
        Write-Host "✗ FEHLER bei $($eintrag.Datei): $($_.Exception.Message)" -ForegroundColor Red
        $fehler++
    }
}

# Zusammenfassung
Write-Host ""
Write-Host ("=" * 60)
Write-Host "Fertig!" -ForegroundColor Cyan
Write-Host "Erfolgreich sortiert: $erfolg" -ForegroundColor Green
if ($fehler -gt 0) {
    Write-Host "Fehler: $fehler" -ForegroundColor Red
}
Write-Host ("=" * 60)
Read-Host "`nDrücke Enter zum Beenden"
```

### Festivals und Sets suchen und verschieben

#### Beispiel-Ausgangssituation

D:\\Musik\\Kraut Fusion Festival Sonnedeck 29.06.2018.mp3

#### Beispiel-Zielsituation

D:\\Musik\\Sets\_und\_Festivals\\Kraut Fusion Festival Sonnedeck 29.06.2018.mp3

```powershell
# Set/Festival Musikdateien in separaten Ordner verschieben
# Durchsucht Ordner inklusive Unterordner

# Musik-Dateierweiterungen
$musikErweiterungen = @('.mp3', '.flac', '.m4a', '.wav', '.ogg', '.wma', '.aac')

# Suchbegriffe
$suchbegriffe = @('Set', 'Festival')

Write-Host "=== Set/Festival Dateien-Mover ===" -ForegroundColor Cyan
Write-Host ""
$quellOrdner = Read-Host "Bitte gib den Pfad zum zu durchsuchenden Ordner ein"

# Prüfe ob Ordner existiert
if (-not (Test-Path $quellOrdner)) {
    Write-Host "FEHLER: Der angegebene Ordner existiert nicht!" -ForegroundColor Red
    Read-Host "Drücke Enter zum Beenden"
    exit
}

# Hole alle Musikdateien rekursiv
Write-Host "`nDurchsuche Ordner und Unterordner..." -ForegroundColor Yellow
$alleDateien = Get-ChildItem -Path $quellOrdner -File -Recurse | Where-Object {
    $musikErweiterungen -contains $_.Extension.ToLower()
}

if ($alleDateien.Count -eq 0) {
    Write-Host "Keine Musikdateien gefunden." -ForegroundColor Yellow
    Read-Host "Drücke Enter zum Beenden"
    exit
}

Write-Host "Gesamt gefundene Musikdateien: $($alleDateien.Count)" -ForegroundColor Gray

# Finde Dateien mit "Set" oder "Festival" im Namen
$gefundeneDateien = $alleDateien | Where-Object {
    $dateiName = $_.Name
    $gefunden = $false
    foreach ($begriff in $suchbegriffe) {
        if ($dateiName -match $begriff) {
            $gefunden = $true
            break
        }
    }
    $gefunden
}

if ($gefundeneDateien.Count -eq 0) {
    Write-Host "`nKeine Dateien mit 'Set' oder 'Festival' im Namen gefunden." -ForegroundColor Yellow
    Read-Host "Drücke Enter zum Beenden"
    exit
}

Write-Host "`nGefundene Dateien mit 'Set' oder 'Festival': $($gefundeneDateien.Count)" -ForegroundColor Green
Write-Host ""

# Zeige Vorschau
Write-Host "Vorschau der zu verschiebenden Dateien:" -ForegroundColor Cyan
Write-Host ("=" * 80)
foreach ($datei in $gefundeneDateien) {
    $relativerPfad = $datei.FullName.Replace($quellOrdner, '').TrimStart('\')
    Write-Host "  $relativerPfad" -ForegroundColor Gray
}
Write-Host ("=" * 80)
Write-Host ""

# Zielordner festlegen
$zielOrdnerName = Read-Host "Name für den Zielordner (Standard: 'Sets_und_Festivals')"
if ([string]::IsNullOrWhiteSpace($zielOrdnerName)) {
    $zielOrdnerName = "Sets_und_Festivals"
}

$zielOrdner = Join-Path $quellOrdner $zielOrdnerName

# Bestätigung einholen
$bestaetigung = Read-Host "`nMöchtest du diese $($gefundeneDateien.Count) Datei(en) nach '$zielOrdnerName' verschieben? (J/N)"

if ($bestaetigung -notmatch '^[jJyY]') {
    Write-Host "Vorgang abgebrochen." -ForegroundColor Yellow
    Read-Host "Drücke Enter zum Beenden"
    exit
}

# Erstelle Zielordner falls nicht vorhanden
if (-not (Test-Path $zielOrdner)) {
    New-Item -Path $zielOrdner -ItemType Directory -Force | Out-Null
    Write-Host "`nOrdner '$zielOrdnerName' wurde erstellt." -ForegroundColor Green
}

# Dateien verschieben
Write-Host "`nVerschiebe Dateien..." -ForegroundColor Cyan
$erfolg = 0
$fehler = 0

foreach ($datei in $gefundeneDateien) {
    try {
        # Prüfe ob Datei bereits im Zielordner ist
        if ($datei.DirectoryName -eq $zielOrdner) {
            Write-Host "○ $($datei.Name) ist bereits im Zielordner" -ForegroundColor Yellow
            continue
        }
        
        $zielPfad = Join-Path $zielOrdner $datei.Name
        
        # Prüfe auf Namenskonflikte
        if (Test-Path $zielPfad) {
            $counter = 1
            $nameOhneErweiterung = [System.IO.Path]::GetFileNameWithoutExtension($datei.Name)
            $erweiterung = $datei.Extension
            while (Test-Path $zielPfad) {
                $neuerName = "$nameOhneErweiterung ($counter)$erweiterung"
                $zielPfad = Join-Path $zielOrdner $neuerName
                $counter++
            }
        }
        
        # Verschiebe Datei
        Move-Item -Path $datei.FullName -Destination $zielPfad -Force
        Write-Host "✓ $($datei.Name)" -ForegroundColor Green
        $erfolg++
    }
    catch {
        Write-Host "✗ FEHLER bei $($datei.Name): $($_.Exception.Message)" -ForegroundColor Red
        $fehler++
    }
}

# Zusammenfassung
Write-Host ""
Write-Host ("=" * 80)
Write-Host "Fertig!" -ForegroundColor Cyan
Write-Host "Erfolgreich verschoben: $erfolg" -ForegroundColor Green
if ($fehler -gt 0) {
    Write-Host "Fehler: $fehler" -ForegroundColor Red
}
Write-Host "Zielordner: $zielOrdner" -ForegroundColor Gray
Write-Host ("=" * 80)
Read-Host "`nDrücke Enter zum Beenden"
```

# leere Doppel-Ordner

Ich bin mit meinem Nextcloud umgezogen, beim neuen Anbieter hatte der Client leere Ordner erstellt, welche den gleichen Namen des vorherigen Ordners trugen... Mag ein Bug gewesen sein

Generator: Claude Sonnet 4.5

Script starten, Ordnerpfad angeben, dann wird gelistet welche Ordner es trifft (leere Ordner die den gleichen Namen des vorherigen Ordners tragen) und deren Löschung wird angeboten.

**Die Löschung ist permanent. Technisch gesehen ist das Löschen von Windows in der GUI ein Verschieben in den Papierkorb. Wollt ihr das müsst ihr das Script von Claude anpassen lassen.**

```powershell
# Nextcloud - Leere doppelte Unterordner finden und löschen
# Findet Ordner, die den gleichen Namen wie ihr Elternordner haben und leer sind

# Farben für bessere Lesbarkeit
function Write-ColorOutput($ForegroundColor) {
    $fc = $host.UI.RawUI.ForegroundColor
    $host.UI.RawUI.ForegroundColor = $ForegroundColor
    if ($args) {
        Write-Output $args
    }
    $host.UI.RawUI.ForegroundColor = $fc
}

Write-Host "`n=== Nextcloud Ordner-Bereinigung ===" -ForegroundColor Cyan
Write-Host "Suche nach leeren Ordnern, die den gleichen Namen wie ihr Elternordner haben...`n" -ForegroundColor Yellow

# Frage nach dem zu durchsuchenden Pfad
$StartPath = Read-Host "Bitte gib den zu durchsuchenden Ordnerpfad ein"

# Prüfe ob Pfad existiert
if (-not (Test-Path $StartPath)) {
    Write-Host "`nFEHLER: Der Pfad '$StartPath' existiert nicht!" -ForegroundColor Red
    Read-Host "`nDrücke Enter zum Beenden"
    exit
}

# Finde alle Ordner rekursiv
$allFolders = Get-ChildItem -Path $StartPath -Directory -Recurse -ErrorAction SilentlyContinue

# Array für problematische Ordner
$duplicateFolders = @()

# Durchsuche alle Ordner
foreach ($folder in $allFolders) {
    $parentFolder = Split-Path $folder.FullName -Parent
    $parentFolderName = Split-Path $parentFolder -Leaf
    $currentFolderName = $folder.Name
    
    # Prüfe ob der Ordnername gleich dem Elternordner ist
    if ($currentFolderName -eq $parentFolderName) {
        # Prüfe ob der Ordner leer ist (keine Dateien und keine Unterordner)
        $itemCount = (Get-ChildItem -Path $folder.FullName -Force -ErrorAction SilentlyContinue | Measure-Object).Count
        
        if ($itemCount -eq 0) {
            $duplicateFolders += [PSCustomObject]@{
                Pfad = $folder.FullName
                Name = $currentFolderName
                Elternordner = $parentFolderName
            }
        }
    }
}

# Zeige Ergebnisse
if ($duplicateFolders.Count -eq 0) {
    Write-Host "✓ Keine problematischen leeren Ordner gefunden!" -ForegroundColor Green
    exit
}

Write-Host "Gefundene leere Ordner mit doppeltem Namen:" -ForegroundColor Yellow
Write-Host ("=" * 80) -ForegroundColor Gray

$counter = 1
foreach ($dup in $duplicateFolders) {
    Write-Host "`n[$counter] " -NoNewline -ForegroundColor Cyan
    Write-Host "Ordnername: " -NoNewline -ForegroundColor White
    Write-Host "$($dup.Name)" -ForegroundColor Yellow
    Write-Host "    Pfad: " -NoNewline -ForegroundColor White
    Write-Host "$($dup.Pfad)" -ForegroundColor Gray
    $counter++
}

Write-Host "`n" -NoNewline
Write-Host ("=" * 80) -ForegroundColor Gray
Write-Host "`nGesamt: " -NoNewline -ForegroundColor White
Write-Host "$($duplicateFolders.Count) leere Ordner gefunden" -ForegroundColor Yellow

# Frage ob gelöscht werden soll
Write-Host "`n"
$confirmation = Read-Host "Möchtest du diese Ordner LÖSCHEN? (ja/nein)"

if ($confirmation -eq "ja" -or $confirmation -eq "j" -or $confirmation -eq "yes" -or $confirmation -eq "y") {
    Write-Host "`nLösche Ordner..." -ForegroundColor Yellow
    
    $successCount = 0
    $errorCount = 0
    
    foreach ($dup in $duplicateFolders) {
        try {
            Remove-Item -Path $dup.Pfad -Force -ErrorAction Stop
            Write-Host "✓ Gelöscht: " -NoNewline -ForegroundColor Green
            Write-Host "$($dup.Pfad)" -ForegroundColor Gray
            $successCount++
        }
        catch {
            Write-Host "✗ FEHLER bei: " -NoNewline -ForegroundColor Red
            Write-Host "$($dup.Pfad)" -ForegroundColor Gray
            Write-Host "  Grund: $($_.Exception.Message)" -ForegroundColor Red
            $errorCount++
        }
    }
    
    Write-Host "`n" -NoNewline
    Write-Host ("=" * 80) -ForegroundColor Gray
    Write-Host "`nErgebnis:" -ForegroundColor Cyan
    Write-Host "  Erfolgreich gelöscht: " -NoNewline -ForegroundColor White
    Write-Host "$successCount" -ForegroundColor Green
    if ($errorCount -gt 0) {
        Write-Host "  Fehler: " -NoNewline -ForegroundColor White
        Write-Host "$errorCount" -ForegroundColor Red
    }
}
else {
    Write-Host "`nAbgebrochen. Keine Ordner wurden gelöscht." -ForegroundColor Yellow
}

Write-Host "`n" -ForegroundColor White
```