Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python

name: Python package

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
build:

runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11"]

steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest
122 changes: 122 additions & 0 deletions docs/audio_collect_command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Audio-Sammelskript für Windows und macOS

Dieses Repository enthält das PowerShell-Skript [`scripts/audio_collect.ps1`](../scripts/audio_collect.ps1),
mit dem Sie alle verfügbaren Laufwerke nach Audiodateien durchsuchen und diese in einen
zentralen Ordner `Audio_Quelle` kopieren können.

## Voraussetzungen

* Windows 10 oder neuer (PowerShell 5.1 oder PowerShell 7) **oder** macOS mit PowerShell 7.
* Lesezugriff auf die gewünschten Laufwerke (lokal oder Netzwerk).
* Schreibrechte für den Zielordner `Audio_Quelle` (standardmäßig im Benutzerprofil).

### PowerShell auf macOS installieren

macOS bringt PowerShell nicht standardmäßig mit. Installieren Sie zunächst Homebrew
und anschließend PowerShell:

```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install --cask powershell
```

Starten Sie PowerShell anschließend über `pwsh` im Terminal.

## Verwendung

1. Öffnen Sie eine PowerShell-Sitzung mit ausreichenden Berechtigungen.
2. Navigieren Sie in das Verzeichnis dieses Repositories.
3. Führen Sie das Skript mit folgendem Befehl aus:

```powershell
pwsh ./scripts/audio_collect.ps1
```

> Hinweis: Unter Windows PowerShell genügt `./scripts/audio_collect.ps1`.

Beim ersten Aufruf empfiehlt sich der Zusatz `-WhatIf`, um eine Vorschau der
Aktionen zu erhalten:

```powershell
pwsh ./scripts/audio_collect.ps1 -WhatIf
```

## Optionen

Das Skript akzeptiert optionale Parameter, die Sie bei Bedarf anpassen können:

```powershell
pwsh ./scripts/audio_collect.ps1 -Destination "D:\\MeinOrdner" -Extensions '*.mp3','*.wav'
```

* `-Destination` – Zielordner, in den alle gefundenen Dateien kopiert oder
verlinkt werden. Standard ist `%USERPROFILE%\Audio_Quelle`.
* `-Extensions` – Liste erlaubter Dateierweiterungen. Standardmäßig werden `mp3`,
`wav`, `aiff`, `flac`, `aac`, `ogg`, `wma` und `m4a` durchsucht.
* `-TransferMode` – legt fest, ob Dateien kopiert (`Copy`, Standard) oder als
Hardlink abgelegt werden (`HardLink`).

## Besonderheiten auf dem MacBook und externen Laufwerken

* Das Skript kopiert Dateien – es verschiebt nichts. Damit entspricht es der
Anforderung „vom MacBook 2011 soll nur kopiert werden“.
* Der Zielordner wird automatisch von der Suche ausgenommen. Wird also z. B.
`-Destination "/Volumes/T5 EVO/Audio_Quelle"` angegeben, durchsucht das Skript
diesen Pfad nicht erneut.
* macOS-Laufwerke werden über `/Volumes` erkannt. Alle dort eingebundenen Netzwerk-
oder USB-Volumes, die Sie im Finder sehen, fließen in die Suche ein.
* Hardlinks sind nur möglich, wenn Quelle und Ziel auf demselben Dateisystem
liegen. Viele externe SSDs mit exFAT unterstützen keine Hardlinks – in diesem
Fall fällt das Skript automatisch auf Kopieren zurück.

## iPad und iPhone einbinden

Apple erlaubt keinen direkten Dateizugriff auf iOS-Geräte wie bei einem USB-Stick.
So binden Sie dennoch Audiodateien ein:

1. Öffnen Sie den Finder, wählen Sie Ihr iPhone oder iPad aus und aktivieren Sie
unter „Dateifreigabe“ die gewünschten Apps. Kopieren Sie deren Dateien in einen
lokalen Ordner (z. B. `~/Music/Import`).
2. Alternativ können Sie Tools wie [ifuse](https://github.com/libimobiledevice/ifuse)
nutzen (`brew install ifuse`), um das Gerät als FUSE-Volume nach `/Volumes/<Name>`
einzubinden. Das Skript durchsucht das eingebundene Volume anschließend wie ein
gewöhnliches Laufwerk.

Sobald die Dateien lokal oder auf einem gemounteten Volume liegen, sammelt das
Skript sie im Zielordner `Audio_Quelle`.

## Funktionsweise

* Alle Dateisystemlaufwerke (`Get-PSDrive -PSProvider FileSystem`) sowie unter
macOS erkannte Volumes (`/Volumes/...`) werden rekursiv durchsucht.
* Verzeichnisse, deren Pfad `Ableton` enthält, werden ignoriert.
* Zielpfade erhalten bei Bedarf ein numerisches Suffix (z. B. `Datei (1).wav`),
sodass bestehende Dateien nicht überschrieben werden – unabhängig davon, ob sie
kopiert oder verlinkt werden.
* Im Modus `HardLink` wird für Dateien auf demselben Laufwerk ein Hardlink erstellt.
Liegt Quelle oder Ziel auf unterschiedlichen Laufwerken, fällt das Skript automatisch
auf Kopieren zurück und informiert über den Grund.

## Hardlinks vs. Kopieren

Hardlinks sind besonders dann hilfreich, wenn Programme wie Traktor oder Apple Music
auf denselben Datenträger zugreifen sollen: Die Musikdatei bleibt nur einmal vorhanden,
alle Hardlinks weisen auf dieselbe physische Datei. Beachten Sie jedoch:

* Hardlinks sind nur innerhalb desselben Laufwerks/Volumes möglich.
* Netzwerkshares oder externe Laufwerke mit anderem Laufwerksbuchstaben bzw.
anderem Volume-Namen werden deshalb automatisch kopiert.
* Für Hardlinks sind die gleichen Berechtigungen erforderlich wie für gewöhnliche
Dateien.

Wenn Sie sicherstellen möchten, dass eine Software stets Zugriff auf eine unabhängige
Dateikopie hat (z. B. zur Archivierung oder für Backups), verwenden Sie den
Standardmodus `Copy`.

## Fehlersuche

* Stellen Sie sicher, dass Sie die Ausführungsrichtlinie für Skripte ggf. mit
`Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` angepasst haben.
* Nutzen Sie den Parameter `-Extensions`, falls Sie zusätzliche Audioformate durchsuchen möchten.
* Verwenden Sie `-Destination`, um den Zielordner auf ein externes Laufwerk oder einen Netzwerkspeicher zu legen.
Der Ordner wird bei der Suche übersprungen, um Endlosschleifen zu verhindern.
233 changes: 233 additions & 0 deletions scripts/audio_collect.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
<#
.SYNOPSIS
Durchsucht alle verfügbaren lokalen und Netzwerk-Laufwerke nach Audiodateien
und kopiert sie in einen zentralen Ordner "Audio_Quelle".

.DESCRIPTION
- Alle Dateisystemlaufwerke (lokal und gemountete Netzwerkshares) werden rekursiv durchsucht.
- Audiodateien mit den gebräuchlichsten Erweiterungen werden berücksichtigt.
- Ordner, deren Pfad "Ableton" enthält, werden übersprungen.
- Bereits vorhandene Dateien werden nicht überschrieben; stattdessen wird
ein eindeutiger Dateiname erzeugt, sodass beide Versionen erhalten bleiben.

.NOTES
Speichern Sie dieses Skript als audio_collect.ps1 und führen Sie es in einer
PowerShell-Sitzung mit ausreichenden Berechtigungen aus.
#>

[CmdletBinding(SupportsShouldProcess = $true)]
param(
[string]$Destination = (Join-Path -Path $env:USERPROFILE -ChildPath 'Audio_Quelle'),

[string[]]$Extensions = @('*.mp3', '*.wav', '*.aiff', '*.flac', '*.aac', '*.ogg', '*.wma', '*.m4a'),

[ValidateSet('Copy', 'HardLink')]
[string]$TransferMode = 'Copy'
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

if (-not (Test-Path -LiteralPath $Destination)) {
New-Item -ItemType Directory -Path $Destination | Out-Null
}

function Resolve-NormalizedPath {
param(
[Parameter(Mandatory)]
[string]$Path
)

try {
$resolved = Resolve-Path -LiteralPath $Path -ErrorAction Stop
return [System.IO.Path]::GetFullPath($resolved.ProviderPath)
}
catch {
return $null
}
}

$destinationFullPath = Resolve-NormalizedPath -Path $Destination

if (-not $destinationFullPath) {
throw "Zielpfad '$Destination' konnte nicht aufgelöst werden."
}

function Test-IsDescendantPath {
param(
[Parameter(Mandatory)]
[string]$Candidate,

[Parameter(Mandatory)]
[string]$Ancestor
)

try {
$candidateFull = [System.IO.Path]::GetFullPath($Candidate)
}
catch {
return $false
}

$ancestorFull = $Ancestor

if (-not $ancestorFull.EndsWith([System.IO.Path]::DirectorySeparatorChar)) {
$ancestorFull += [System.IO.Path]::DirectorySeparatorChar
}

return $candidateFull.Equals($Ancestor, [System.StringComparison]::OrdinalIgnoreCase) -or
$candidateFull.StartsWith($ancestorFull, [System.StringComparison]::OrdinalIgnoreCase)
}

function Test-IsAbletonPath {
param(
[Parameter(Mandatory)]
[string]$Path
)

if (-not $Path) {
return $false
}

return $Path -match '(?i)(?:^|[\\/])Ableton(?:$|[\\/])'
}

function Get-UniqueTargetPath {
param(
[Parameter(Mandatory)]
[string]$DestinationDirectory,

[Parameter(Mandatory)]
[string]$FileName
)

$baseName = [System.IO.Path]::GetFileNameWithoutExtension($FileName)
$extension = [System.IO.Path]::GetExtension($FileName)
$targetPath = Join-Path -Path $DestinationDirectory -ChildPath $FileName
$suffix = 1

while (Test-Path -LiteralPath $targetPath) {
$newName = "{0} ({1}){2}" -f $baseName, $suffix, $extension
$targetPath = Join-Path -Path $DestinationDirectory -ChildPath $newName
$suffix += 1
}

return $targetPath
}

function Copy-AudioFile {
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[Parameter(Mandatory)]
[System.IO.FileInfo]$Source,

[Parameter(Mandatory)]
[string]$DestinationDirectory
)

$targetPath = Get-UniqueTargetPath -DestinationDirectory $DestinationDirectory -FileName $Source.Name

if ($PSCmdlet.ShouldProcess($Source.FullName, "Kopieren nach $targetPath")) {
Copy-Item -LiteralPath $Source.FullName -Destination $targetPath
}
}

function New-HardLinkOrCopy {
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[Parameter(Mandatory)]
[System.IO.FileInfo]$Source,

[Parameter(Mandatory)]
[string]$DestinationDirectory
)

$targetPath = Get-UniqueTargetPath -DestinationDirectory $DestinationDirectory -FileName $Source.Name

$sourceRoot = [System.IO.Path]::GetPathRoot($Source.FullName)
$destinationRoot = [System.IO.Path]::GetPathRoot((Resolve-Path -LiteralPath $DestinationDirectory).Path)

if ($sourceRoot -ieq $destinationRoot) {
if ($PSCmdlet.ShouldProcess($Source.FullName, "Hardlink nach $targetPath")) {
New-Item -ItemType HardLink -Path $targetPath -Value $Source.FullName | Out-Null
}
}
else {
Write-Warning "Hardlinks sind nur innerhalb desselben Laufwerks möglich. Datei wird kopiert: $($Source.FullName)"
if ($PSCmdlet.ShouldProcess($Source.FullName, "Kopieren nach $targetPath")) {
Copy-Item -LiteralPath $Source.FullName -Destination $targetPath
}
}
}

$searchRoots = @()

function Add-SearchRoot {
param(
[Parameter(Mandatory)]
[string]$Path
)

$normalized = Resolve-NormalizedPath -Path $Path

if (-not $normalized) {
return
}

if (Test-IsDescendantPath -Candidate $normalized -Ancestor $destinationFullPath) {
return
}

if (-not ($searchRoots | Where-Object { $_.Normalized -eq $normalized })) {
$searchRoots += [pscustomobject]@{ Original = $Path; Normalized = $normalized }
}
}

function Get-PlatformInfo {
$osx = [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::OSX)
$linux = [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)

return [pscustomobject]@{
IsMacOS = $osx
IsLinux = $linux
}
}

$platformInfo = Get-PlatformInfo

Get-PSDrive -PSProvider FileSystem |
Where-Object { $_.Root } |
ForEach-Object { Add-SearchRoot -Path $_.Root }

if ($platformInfo.IsMacOS -or $platformInfo.IsLinux) {
$volumeRoot = '/Volumes'
if (Test-Path -LiteralPath $volumeRoot) {
Get-ChildItem -LiteralPath $volumeRoot -Directory -ErrorAction SilentlyContinue |
ForEach-Object { Add-SearchRoot -Path $_.FullName }
}
}

foreach ($root in $searchRoots) {
foreach ($pattern in $Extensions) {
Get-ChildItem -Path $root.Original -Filter $pattern -File -Recurse -ErrorAction SilentlyContinue |
Where-Object {
-not (Test-IsAbletonPath -Path $_.DirectoryName) -and
-not (Test-IsDescendantPath -Candidate $_.FullName -Ancestor $destinationFullPath)
} |
ForEach-Object {
if ($TransferMode -eq 'HardLink') {
New-HardLinkOrCopy -Source $_ -DestinationDirectory $Destination
}
else {
Copy-AudioFile -Source $_ -DestinationDirectory $Destination
}
}
}
}

if ($TransferMode -eq 'HardLink') {
Write-Host "Audiodateien wurden nach '$Destination' verlinkt oder kopiert (falls Hardlink nicht möglich war)." -ForegroundColor Green
}
else {
Write-Host "Audiodateien wurden nach '$Destination' kopiert." -ForegroundColor Green
}