Repository files navigation

File Matcher

Deduplicate files by content, not name. Reclaim space with hardlinks/symlinks while preserving alternate filenames. Preview-first safety, audit logging. Built for media libraries.

Python 3.9+License: MIT

File Matcher Demo

Use Cases

  • Media Libraries — Deduplicate movies/TV shows across Plex, Sonarr, Radarr directories
  • Backups — Find and link identical files across backup drives to save space
  • Downloads — Clean up duplicate downloads while keeping organized copies

Features

  • Content-based matching — Find duplicates by hash, not filename
  • Preserve filenames — Duplicates become links but keep their original names and paths
  • Preview-first safety — See changes before executing (requires --execute flag)
  • Interactive mode — Confirm each action with y/n/a/q prompts
  • Audit logging — Full trail of all modifications
  • Fast mode — Sparse sampling for large files (>100MB)
  • No dependencies — Pure Python standard library

Installation

pip install .# Install
filematcher master_dir other_dir # Run# Or run directly
python file_matcher.py master_dir other_dir
# Development
pip install -e .

Quick Start

# Find matching files
filematcher master_dir other_dir
# Preview deduplication (safe - no changes made)
filematcher master_dir other_dir --action hardlink
# Execute deduplication (interactive confirmation)
filematcher master_dir other_dir --action hardlink --execute
# Execute without prompts (for scripts)
filematcher master_dir other_dir --action hardlink --execute --yes

Usage

Finding Duplicates

filematcher master_dir other_dir # Basic comparison
filematcher master_dir other_dir --different-names-only # Only different filenames
filematcher master_dir other_dir --show-unmatched # Include unmatched files
filematcher master_dir other_dir --summary # Counts only
filematcher master_dir other_dir --fast # Fast mode for large files
filematcher master_dir other_dir --hash sha256 # Use SHA-256 instead of MD5

Deduplicating

The first directory is the master (files preserved). Duplicates in the other directory are replaced/deleted.

# Preview (default - no changes)
filematcher master_dir other_dir --action hardlink
filematcher master_dir other_dir --action symlink
filematcher master_dir other_dir --action delete
# Execute
filematcher master_dir other_dir --action hardlink --execute
filematcher master_dir other_dir --action hardlink --execute --yes # Skip prompts
filematcher master_dir other_dir --action hardlink --execute --log changes.log

Interactive Mode

When running --execute without --yes, you're prompted for each group:

[1/5] Hardlink this group? [y/n/a/q]:
  • y — Execute on this group
  • n — Skip this group
  • a — Execute all remaining without prompting
  • q — Quit immediately

Advanced Options

# Cross-filesystem: fall back to symlink when hardlink fails
filematcher master_dir other_dir --action hardlink --fallback-symlink --execute
# Target directory: create links in a new location, preserving duplicate filenames# e.g., other_dir/movies/film.mkv → /backup/movies/film.mkv (linked to master)
filematcher master_dir other_dir --action hardlink --target-dir /backup --execute

Command-Line Reference

OptionShortDescription
--action-aAction: compare (default), hardlink, symlink, delete
--executeExecute changes (default: preview only)
--yes-ySkip confirmation prompts
--show-unmatched-uShow files with no matches
--different-names-only-dOnly show matches with different filenames
--summary-sShow counts only
--fast-fFast mode for large files (>100MB)
--hash-HHash algorithm: md5 (default), sha256
--verbose-vShow detailed progress
--log-lCustom audit log path
--fallback-symlinkUse symlink if hardlink fails (cross-filesystem)
--target-dir-tCreate links in new location (preserves other_dir structure/names)
--json-jJSON output (see JSON_SCHEMA.md)
--quiet-qSuppress progress messages
--colorForce color output
--no-colorDisable color output

JSON Output

Use --json for machine-readable output. See JSON_SCHEMA.md for full schema.

# Count matches and show space savings (human-readable)
filematcher master_dir other_dir --action hardlink --json | \
jq '{groups: .statistics.groupCount, files: .statistics.duplicateCount, savings_mb: (.statistics.spaceSavingsBytes / 1048576 | floor)}'# List all duplicate paths (one per line)
filematcher master_dir other_dir --action hardlink --json | \
jq -r '.duplicateGroups[].duplicates[].path'# Show master → duplicate mappings
filematcher master_dir other_dir --action hardlink --json | \
jq -r '.duplicateGroups[] | "\(.masterFile) -> \(.duplicates[].path)"'# Find large duplicates (>100MB)
filematcher master_dir other_dir --action hardlink --json | \
jq -r '.duplicateGroups[].duplicates[] | select(.sizeBytes > 104857600) | .path'# Get execution results
filematcher master_dir other_dir --action hardlink --execute --yes --json | \
jq '{success: .execution.successCount, failed: .execution.failureCount, saved_mb: (.execution.spaceSavedBytes / 1048576 | floor)}'# List failures with error messages
filematcher master_dir other_dir --action hardlink --execute --yes --json | \
jq -r '.execution.failures[] | "\(.path): \(.error)"'

Note: --json --execute requires --yes (no interactive prompts in JSON mode).

Actions

ActionDescription
compareFind matches only, no modifications (default)
hardlinkReplace duplicate with hard link to master (saves space)
symlinkReplace duplicate with symbolic link to master
deleteDelete duplicate file (irreversible)

Audit Logging

All modifications are logged:

=== File Matcher Audit Log ===
Timestamp: 2026-01-20T10:30:00
Action: hardlink
==============================
[2026-01-20T10:30:01] HARDLINK /other_dir/dup.txt -> /master_dir/file.txt (1.2 KB) OK
==============================
Completed: 2 successful, 0 failed
Space reclaimed: 2.4 KB

Exit Codes

CodeMeaning
0Success
1All operations failed
2Invalid arguments or partial failure
130User quit (q or Ctrl+C)

Output Options

Streams: Data goes to stdout, progress/errors to stderr. Use --quiet to suppress progress.

Colors: Auto-enabled for TTY, disabled when piped. Override with --color or --no-color. Respects NO_COLOR and FORCE_COLOR environment variables.

Testing

python3 run_tests.py # Run all 308 tests
python3 -m tests.test_actions # Run specific module

Package Structure

filematcher/
├── cli.py # Command-line interface
├── colors.py # TTY-aware color output
├── hashing.py # MD5/SHA-256 hashing
├── filesystem.py # Filesystem helpers
├── actions.py # Action execution, audit logging
├── formatters.py # Text and JSON formatters
└── directory.py # Directory indexing

Requirements

  • Python 3.9+
  • No external dependencies

License

MIT


See CHANGELOG.md for version history and breaking changes.

About

Deduplicate files by content, not name. Reclaim space with hardlinks/symlinks while preserving alternate filenames. Preview-first safety, audit logging. Built for media libraries.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

File Matcher

Deduplicate files by content, not name. Reclaim space with hardlinks/symlinks while preserving alternate filenames. Preview-first safety, audit logging. Built for media libraries.

Python 3.9+License: MIT

File Matcher Demo

Use Cases

  • Media Libraries — Deduplicate movies/TV shows across Plex, Sonarr, Radarr directories
  • Backups — Find and link identical files across backup drives to save space
  • Downloads — Clean up duplicate downloads while keeping organized copies

Features

  • Content-based matching — Find duplicates by hash, not filename
  • Preserve filenames — Duplicates become links but keep their original names and paths
  • Preview-first safety — See changes before executing (requires --execute flag)
  • Interactive mode — Confirm each action with y/n/a/q prompts
  • Audit logging — Full trail of all modifications
  • Fast mode — Sparse sampling for large files (>100MB)
  • No dependencies — Pure Python standard library

Installation

pip install .# Install
filematcher master_dir other_dir # Run# Or run directly
python file_matcher.py master_dir other_dir
# Development
pip install -e .

Quick Start

# Find matching files
filematcher master_dir other_dir
# Preview deduplication (safe - no changes made)
filematcher master_dir other_dir --action hardlink
# Execute deduplication (interactive confirmation)
filematcher master_dir other_dir --action hardlink --execute
# Execute without prompts (for scripts)
filematcher master_dir other_dir --action hardlink --execute --yes

Usage

Finding Duplicates

filematcher master_dir other_dir # Basic comparison
filematcher master_dir other_dir --different-names-only # Only different filenames
filematcher master_dir other_dir --show-unmatched # Include unmatched files
filematcher master_dir other_dir --summary # Counts only
filematcher master_dir other_dir --fast # Fast mode for large files
filematcher master_dir other_dir --hash sha256 # Use SHA-256 instead of MD5

Deduplicating

The first directory is the master (files preserved). Duplicates in the other directory are replaced/deleted.

# Preview (default - no changes)
filematcher master_dir other_dir --action hardlink
filematcher master_dir other_dir --action symlink
filematcher master_dir other_dir --action delete
# Execute
filematcher master_dir other_dir --action hardlink --execute
filematcher master_dir other_dir --action hardlink --execute --yes # Skip prompts
filematcher master_dir other_dir --action hardlink --execute --log changes.log

Interactive Mode

When running --execute without --yes, you're prompted for each group:

[1/5] Hardlink this group? [y/n/a/q]:
  • y — Execute on this group
  • n — Skip this group
  • a — Execute all remaining without prompting
  • q — Quit immediately

Advanced Options

# Cross-filesystem: fall back to symlink when hardlink fails
filematcher master_dir other_dir --action hardlink --fallback-symlink --execute
# Target directory: create links in a new location, preserving duplicate filenames# e.g., other_dir/movies/film.mkv → /backup/movies/film.mkv (linked to master)
filematcher master_dir other_dir --action hardlink --target-dir /backup --execute

Command-Line Reference

OptionShortDescription
--action-aAction: compare (default), hardlink, symlink, delete
--executeExecute changes (default: preview only)
--yes-ySkip confirmation prompts
--show-unmatched-uShow files with no matches
--different-names-only-dOnly show matches with different filenames
--summary-sShow counts only
--fast-fFast mode for large files (>100MB)
--hash-HHash algorithm: md5 (default), sha256
--verbose-vShow detailed progress
--log-lCustom audit log path
--fallback-symlinkUse symlink if hardlink fails (cross-filesystem)
--target-dir-tCreate links in new location (preserves other_dir structure/names)
--json-jJSON output (see JSON_SCHEMA.md)
--quiet-qSuppress progress messages
--colorForce color output
--no-colorDisable color output

JSON Output

Use --json for machine-readable output. See JSON_SCHEMA.md for full schema.

# Count matches and show space savings (human-readable)
filematcher master_dir other_dir --action hardlink --json | \
jq '{groups: .statistics.groupCount, files: .statistics.duplicateCount, savings_mb: (.statistics.spaceSavingsBytes / 1048576 | floor)}'# List all duplicate paths (one per line)
filematcher master_dir other_dir --action hardlink --json | \
jq -r '.duplicateGroups[].duplicates[].path'# Show master → duplicate mappings
filematcher master_dir other_dir --action hardlink --json | \
jq -r '.duplicateGroups[] | "\(.masterFile) -> \(.duplicates[].path)"'# Find large duplicates (>100MB)
filematcher master_dir other_dir --action hardlink --json | \
jq -r '.duplicateGroups[].duplicates[] | select(.sizeBytes > 104857600) | .path'# Get execution results
filematcher master_dir other_dir --action hardlink --execute --yes --json | \
jq '{success: .execution.successCount, failed: .execution.failureCount, saved_mb: (.execution.spaceSavedBytes / 1048576 | floor)}'# List failures with error messages
filematcher master_dir other_dir --action hardlink --execute --yes --json | \
jq -r '.execution.failures[] | "\(.path): \(.error)"'

Note: --json --execute requires --yes (no interactive prompts in JSON mode).

Actions

ActionDescription
compareFind matches only, no modifications (default)
hardlinkReplace duplicate with hard link to master (saves space)
symlinkReplace duplicate with symbolic link to master
deleteDelete duplicate file (irreversible)

Audit Logging

All modifications are logged:

=== File Matcher Audit Log ===
Timestamp: 2026-01-20T10:30:00
Action: hardlink
==============================
[2026-01-20T10:30:01] HARDLINK /other_dir/dup.txt -> /master_dir/file.txt (1.2 KB) OK
==============================
Completed: 2 successful, 0 failed
Space reclaimed: 2.4 KB

Exit Codes

CodeMeaning
0Success
1All operations failed
2Invalid arguments or partial failure
130User quit (q or Ctrl+C)

Output Options

Streams: Data goes to stdout, progress/errors to stderr. Use --quiet to suppress progress.

Colors: Auto-enabled for TTY, disabled when piped. Override with --color or --no-color. Respects NO_COLOR and FORCE_COLOR environment variables.

Testing

python3 run_tests.py # Run all 308 tests
python3 -m tests.test_actions # Run specific module

Package Structure

filematcher/
├── cli.py # Command-line interface
├── colors.py # TTY-aware color output
├── hashing.py # MD5/SHA-256 hashing
├── filesystem.py # Filesystem helpers
├── actions.py # Action execution, audit logging
├── formatters.py # Text and JSON formatters
└── directory.py # Directory indexing

Requirements

  • Python 3.9+
  • No external dependencies

License

MIT


See CHANGELOG.md for version history and breaking changes.

About

Deduplicate files by content, not name. Reclaim space with hardlinks/symlinks while preserving alternate filenames. Preview-first safety, audit logging. Built for media libraries.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

File Matcher

Deduplicate files by content, not name. Reclaim space with hardlinks/symlinks while preserving alternate filenames. Preview-first safety, audit logging. Built for media libraries.

Python 3.9+License: MIT

File Matcher Demo

Use Cases

  • Media Libraries — Deduplicate movies/TV shows across Plex, Sonarr, Radarr directories
  • Backups — Find and link identical files across backup drives to save space
  • Downloads — Clean up duplicate downloads while keeping organized copies

Features

  • Content-based matching — Find duplicates by hash, not filename
  • Preserve filenames — Duplicates become links but keep their original names and paths
  • Preview-first safety — See changes before executing (requires --execute flag)
  • Interactive mode — Confirm each action with y/n/a/q prompts
  • Audit logging — Full trail of all modifications
  • Fast mode — Sparse sampling for large files (>100MB)
  • No dependencies — Pure Python standard library

Installation

pip install .# Install
filematcher master_dir other_dir # Run# Or run directly
python file_matcher.py master_dir other_dir
# Development
pip install -e .

Quick Start

# Find matching files
filematcher master_dir other_dir
# Preview deduplication (safe - no changes made)
filematcher master_dir other_dir --action hardlink
# Execute deduplication (interactive confirmation)
filematcher master_dir other_dir --action hardlink --execute
# Execute without prompts (for scripts)
filematcher master_dir other_dir --action hardlink --execute --yes

Usage

Finding Duplicates

filematcher master_dir other_dir # Basic comparison
filematcher master_dir other_dir --different-names-only # Only different filenames
filematcher master_dir other_dir --show-unmatched # Include unmatched files
filematcher master_dir other_dir --summary # Counts only
filematcher master_dir other_dir --fast # Fast mode for large files
filematcher master_dir other_dir --hash sha256 # Use SHA-256 instead of MD5

Deduplicating

The first directory is the master (files preserved). Duplicates in the other directory are replaced/deleted.

# Preview (default - no changes)
filematcher master_dir other_dir --action hardlink
filematcher master_dir other_dir --action symlink
filematcher master_dir other_dir --action delete
# Execute
filematcher master_dir other_dir --action hardlink --execute
filematcher master_dir other_dir --action hardlink --execute --yes # Skip prompts
filematcher master_dir other_dir --action hardlink --execute --log changes.log

Interactive Mode

When running --execute without --yes, you're prompted for each group:

[1/5] Hardlink this group? [y/n/a/q]:
  • y — Execute on this group
  • n — Skip this group
  • a — Execute all remaining without prompting
  • q — Quit immediately

Advanced Options

# Cross-filesystem: fall back to symlink when hardlink fails
filematcher master_dir other_dir --action hardlink --fallback-symlink --execute
# Target directory: create links in a new location, preserving duplicate filenames# e.g., other_dir/movies/film.mkv → /backup/movies/film.mkv (linked to master)
filematcher master_dir other_dir --action hardlink --target-dir /backup --execute

Command-Line Reference

OptionShortDescription
--action-aAction: compare (default), hardlink, symlink, delete
--executeExecute changes (default: preview only)
--yes-ySkip confirmation prompts
--show-unmatched-uShow files with no matches
--different-names-only-dOnly show matches with different filenames
--summary-sShow counts only
--fast-fFast mode for large files (>100MB)
--hash-HHash algorithm: md5 (default), sha256
--verbose-vShow detailed progress
--log-lCustom audit log path
--fallback-symlinkUse symlink if hardlink fails (cross-filesystem)
--target-dir-tCreate links in new location (preserves other_dir structure/names)
--json-jJSON output (see JSON_SCHEMA.md)
--quiet-qSuppress progress messages
--colorForce color output
--no-colorDisable color output

JSON Output

Use --json for machine-readable output. See JSON_SCHEMA.md for full schema.

# Count matches and show space savings (human-readable)
filematcher master_dir other_dir --action hardlink --json | \
jq '{groups: .statistics.groupCount, files: .statistics.duplicateCount, savings_mb: (.statistics.spaceSavingsBytes / 1048576 | floor)}'# List all duplicate paths (one per line)
filematcher master_dir other_dir --action hardlink --json | \
jq -r '.duplicateGroups[].duplicates[].path'# Show master → duplicate mappings
filematcher master_dir other_dir --action hardlink --json | \
jq -r '.duplicateGroups[] | "\(.masterFile) -> \(.duplicates[].path)"'# Find large duplicates (>100MB)
filematcher master_dir other_dir --action hardlink --json | \
jq -r '.duplicateGroups[].duplicates[] | select(.sizeBytes > 104857600) | .path'# Get execution results
filematcher master_dir other_dir --action hardlink --execute --yes --json | \
jq '{success: .execution.successCount, failed: .execution.failureCount, saved_mb: (.execution.spaceSavedBytes / 1048576 | floor)}'# List failures with error messages
filematcher master_dir other_dir --action hardlink --execute --yes --json | \
jq -r '.execution.failures[] | "\(.path): \(.error)"'

Note: --json --execute requires --yes (no interactive prompts in JSON mode).

Actions

ActionDescription
compareFind matches only, no modifications (default)
hardlinkReplace duplicate with hard link to master (saves space)
symlinkReplace duplicate with symbolic link to master
deleteDelete duplicate file (irreversible)

Audit Logging

All modifications are logged:

=== File Matcher Audit Log ===
Timestamp: 2026-01-20T10:30:00
Action: hardlink
==============================
[2026-01-20T10:30:01] HARDLINK /other_dir/dup.txt -> /master_dir/file.txt (1.2 KB) OK
==============================
Completed: 2 successful, 0 failed
Space reclaimed: 2.4 KB

Exit Codes

CodeMeaning
0Success
1All operations failed
2Invalid arguments or partial failure
130User quit (q or Ctrl+C)

Output Options

Streams: Data goes to stdout, progress/errors to stderr. Use --quiet to suppress progress.

Colors: Auto-enabled for TTY, disabled when piped. Override with --color or --no-color. Respects NO_COLOR and FORCE_COLOR environment variables.

Testing

python3 run_tests.py # Run all 308 tests
python3 -m tests.test_actions # Run specific module

Package Structure

filematcher/
├── cli.py # Command-line interface
├── colors.py # TTY-aware color output
├── hashing.py # MD5/SHA-256 hashing
├── filesystem.py # Filesystem helpers
├── actions.py # Action execution, audit logging
├── formatters.py # Text and JSON formatters
└── directory.py # Directory indexing

Requirements

  • Python 3.9+
  • No external dependencies

License

MIT


See CHANGELOG.md for version history and breaking changes.

About

Deduplicate files by content, not name. Reclaim space with hardlinks/symlinks while preserving alternate filenames. Preview-first safety, audit logging. Built for media libraries.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

File Matcher

Deduplicate files by content, not name. Reclaim space with hardlinks/symlinks while preserving alternate filenames. Preview-first safety, audit logging. Built for media libraries.

Python 3.9+License: MIT

File Matcher Demo

Use Cases

  • Media Libraries — Deduplicate movies/TV shows across Plex, Sonarr, Radarr directories
  • Backups — Find and link identical files across backup drives to save space
  • Downloads — Clean up duplicate downloads while keeping organized copies

Features

  • Content-based matching — Find duplicates by hash, not filename
  • Preserve filenames — Duplicates become links but keep their original names and paths
  • Preview-first safety — See changes before executing (requires --execute flag)
  • Interactive mode — Confirm each action with y/n/a/q prompts
  • Audit logging — Full trail of all modifications
  • Fast mode — Sparse sampling for large files (>100MB)
  • No dependencies — Pure Python standard library

Installation

pip install .# Install
filematcher master_dir other_dir # Run# Or run directly
python file_matcher.py master_dir other_dir
# Development
pip install -e .

Quick Start

# Find matching files
filematcher master_dir other_dir
# Preview deduplication (safe - no changes made)
filematcher master_dir other_dir --action hardlink
# Execute deduplication (interactive confirmation)
filematcher master_dir other_dir --action hardlink --execute
# Execute without prompts (for scripts)
filematcher master_dir other_dir --action hardlink --execute --yes

Usage

Finding Duplicates

filematcher master_dir other_dir # Basic comparison
filematcher master_dir other_dir --different-names-only # Only different filenames
filematcher master_dir other_dir --show-unmatched # Include unmatched files
filematcher master_dir other_dir --summary # Counts only
filematcher master_dir other_dir --fast # Fast mode for large files
filematcher master_dir other_dir --hash sha256 # Use SHA-256 instead of MD5

Deduplicating

The first directory is the master (files preserved). Duplicates in the other directory are replaced/deleted.

# Preview (default - no changes)
filematcher master_dir other_dir --action hardlink
filematcher master_dir other_dir --action symlink
filematcher master_dir other_dir --action delete
# Execute
filematcher master_dir other_dir --action hardlink --execute
filematcher master_dir other_dir --action hardlink --execute --yes # Skip prompts
filematcher master_dir other_dir --action hardlink --execute --log changes.log

Interactive Mode

When running --execute without --yes, you're prompted for each group:

[1/5] Hardlink this group? [y/n/a/q]:
  • y — Execute on this group
  • n — Skip this group
  • a — Execute all remaining without prompting
  • q — Quit immediately

Advanced Options

# Cross-filesystem: fall back to symlink when hardlink fails
filematcher master_dir other_dir --action hardlink --fallback-symlink --execute
# Target directory: create links in a new location, preserving duplicate filenames# e.g., other_dir/movies/film.mkv → /backup/movies/film.mkv (linked to master)
filematcher master_dir other_dir --action hardlink --target-dir /backup --execute

Command-Line Reference

OptionShortDescription
--action-aAction: compare (default), hardlink, symlink, delete
--executeExecute changes (default: preview only)
--yes-ySkip confirmation prompts
--show-unmatched-uShow files with no matches
--different-names-only-dOnly show matches with different filenames
--summary-sShow counts only
--fast-fFast mode for large files (>100MB)
--hash-HHash algorithm: md5 (default), sha256
--verbose-vShow detailed progress
--log-lCustom audit log path
--fallback-symlinkUse symlink if hardlink fails (cross-filesystem)
--target-dir-tCreate links in new location (preserves other_dir structure/names)
--json-jJSON output (see JSON_SCHEMA.md)
--quiet-qSuppress progress messages
--colorForce color output
--no-colorDisable color output

JSON Output

Use --json for machine-readable output. See JSON_SCHEMA.md for full schema.

# Count matches and show space savings (human-readable)
filematcher master_dir other_dir --action hardlink --json | \
jq '{groups: .statistics.groupCount, files: .statistics.duplicateCount, savings_mb: (.statistics.spaceSavingsBytes / 1048576 | floor)}'# List all duplicate paths (one per line)
filematcher master_dir other_dir --action hardlink --json | \
jq -r '.duplicateGroups[].duplicates[].path'# Show master → duplicate mappings
filematcher master_dir other_dir --action hardlink --json | \
jq -r '.duplicateGroups[] | "\(.masterFile) -> \(.duplicates[].path)"'# Find large duplicates (>100MB)
filematcher master_dir other_dir --action hardlink --json | \
jq -r '.duplicateGroups[].duplicates[] | select(.sizeBytes > 104857600) | .path'# Get execution results
filematcher master_dir other_dir --action hardlink --execute --yes --json | \
jq '{success: .execution.successCount, failed: .execution.failureCount, saved_mb: (.execution.spaceSavedBytes / 1048576 | floor)}'# List failures with error messages
filematcher master_dir other_dir --action hardlink --execute --yes --json | \
jq -r '.execution.failures[] | "\(.path): \(.error)"'

Note: --json --execute requires --yes (no interactive prompts in JSON mode).

Actions

ActionDescription
compareFind matches only, no modifications (default)
hardlinkReplace duplicate with hard link to master (saves space)
symlinkReplace duplicate with symbolic link to master
deleteDelete duplicate file (irreversible)

Audit Logging

All modifications are logged:

=== File Matcher Audit Log ===
Timestamp: 2026-01-20T10:30:00
Action: hardlink
==============================
[2026-01-20T10:30:01] HARDLINK /other_dir/dup.txt -> /master_dir/file.txt (1.2 KB) OK
==============================
Completed: 2 successful, 0 failed
Space reclaimed: 2.4 KB

Exit Codes

CodeMeaning
0Success
1All operations failed
2Invalid arguments or partial failure
130User quit (q or Ctrl+C)

Output Options

Streams: Data goes to stdout, progress/errors to stderr. Use --quiet to suppress progress.

Colors: Auto-enabled for TTY, disabled when piped. Override with --color or --no-color. Respects NO_COLOR and FORCE_COLOR environment variables.

Testing

python3 run_tests.py # Run all 308 tests
python3 -m tests.test_actions # Run specific module

Package Structure

filematcher/
├── cli.py # Command-line interface
├── colors.py # TTY-aware color output
├── hashing.py # MD5/SHA-256 hashing
├── filesystem.py # Filesystem helpers
├── actions.py # Action execution, audit logging
├── formatters.py # Text and JSON formatters
└── directory.py # Directory indexing

Requirements

  • Python 3.9+
  • No external dependencies

License

MIT


See CHANGELOG.md for version history and breaking changes.

About

Deduplicate files by content, not name. Reclaim space with hardlinks/symlinks while preserving alternate filenames. Preview-first safety, audit logging. Built for media libraries.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

File Matcher

Deduplicate files by content, not name. Reclaim space with hardlinks/symlinks while preserving alternate filenames. Preview-first safety, audit logging. Built for media libraries.

Python 3.9+License: MIT

File Matcher Demo

Use Cases

  • Media Libraries — Deduplicate movies/TV shows across Plex, Sonarr, Radarr directories
  • Backups — Find and link identical files across backup drives to save space
  • Downloads — Clean up duplicate downloads while keeping organized copies

Features

  • Content-based matching — Find duplicates by hash, not filename
  • Preserve filenames — Duplicates become links but keep their original names and paths
  • Preview-first safety — See changes before executing (requires --execute flag)
  • Interactive mode — Confirm each action with y/n/a/q prompts
  • Audit logging — Full trail of all modifications
  • Fast mode — Sparse sampling for large files (>100MB)
  • No dependencies — Pure Python standard library

Installation

pip install .# Install
filematcher master_dir other_dir # Run# Or run directly
python file_matcher.py master_dir other_dir
# Development
pip install -e .

Quick Start

# Find matching files
filematcher master_dir other_dir
# Preview deduplication (safe - no changes made)
filematcher master_dir other_dir --action hardlink
# Execute deduplication (interactive confirmation)
filematcher master_dir other_dir --action hardlink --execute
# Execute without prompts (for scripts)
filematcher master_dir other_dir --action hardlink --execute --yes

Usage

Finding Duplicates

filematcher master_dir other_dir # Basic comparison
filematcher master_dir other_dir --different-names-only # Only different filenames
filematcher master_dir other_dir --show-unmatched # Include unmatched files
filematcher master_dir other_dir --summary # Counts only
filematcher master_dir other_dir --fast # Fast mode for large files
filematcher master_dir other_dir --hash sha256 # Use SHA-256 instead of MD5

Deduplicating

The first directory is the master (files preserved). Duplicates in the other directory are replaced/deleted.

# Preview (default - no changes)
filematcher master_dir other_dir --action hardlink
filematcher master_dir other_dir --action symlink
filematcher master_dir other_dir --action delete
# Execute
filematcher master_dir other_dir --action hardlink --execute
filematcher master_dir other_dir --action hardlink --execute --yes # Skip prompts
filematcher master_dir other_dir --action hardlink --execute --log changes.log

Interactive Mode

When running --execute without --yes, you're prompted for each group:

[1/5] Hardlink this group? [y/n/a/q]:
  • y — Execute on this group
  • n — Skip this group
  • a — Execute all remaining without prompting
  • q — Quit immediately

Advanced Options

# Cross-filesystem: fall back to symlink when hardlink fails
filematcher master_dir other_dir --action hardlink --fallback-symlink --execute
# Target directory: create links in a new location, preserving duplicate filenames# e.g., other_dir/movies/film.mkv → /backup/movies/film.mkv (linked to master)
filematcher master_dir other_dir --action hardlink --target-dir /backup --execute

Command-Line Reference

OptionShortDescription
--action-aAction: compare (default), hardlink, symlink, delete
--executeExecute changes (default: preview only)
--yes-ySkip confirmation prompts
--show-unmatched-uShow files with no matches
--different-names-only-dOnly show matches with different filenames
--summary-sShow counts only
--fast-fFast mode for large files (>100MB)
--hash-HHash algorithm: md5 (default), sha256
--verbose-vShow detailed progress
--log-lCustom audit log path
--fallback-symlinkUse symlink if hardlink fails (cross-filesystem)
--target-dir-tCreate links in new location (preserves other_dir structure/names)
--json-jJSON output (see JSON_SCHEMA.md)
--quiet-qSuppress progress messages
--colorForce color output
--no-colorDisable color output

JSON Output

Use --json for machine-readable output. See JSON_SCHEMA.md for full schema.

# Count matches and show space savings (human-readable)
filematcher master_dir other_dir --action hardlink --json | \
jq '{groups: .statistics.groupCount, files: .statistics.duplicateCount, savings_mb: (.statistics.spaceSavingsBytes / 1048576 | floor)}'# List all duplicate paths (one per line)
filematcher master_dir other_dir --action hardlink --json | \
jq -r '.duplicateGroups[].duplicates[].path'# Show master → duplicate mappings
filematcher master_dir other_dir --action hardlink --json | \
jq -r '.duplicateGroups[] | "\(.masterFile) -> \(.duplicates[].path)"'# Find large duplicates (>100MB)
filematcher master_dir other_dir --action hardlink --json | \
jq -r '.duplicateGroups[].duplicates[] | select(.sizeBytes > 104857600) | .path'# Get execution results
filematcher master_dir other_dir --action hardlink --execute --yes --json | \
jq '{success: .execution.successCount, failed: .execution.failureCount, saved_mb: (.execution.spaceSavedBytes / 1048576 | floor)}'# List failures with error messages
filematcher master_dir other_dir --action hardlink --execute --yes --json | \
jq -r '.execution.failures[] | "\(.path): \(.error)"'

Note: --json --execute requires --yes (no interactive prompts in JSON mode).

Actions

ActionDescription
compareFind matches only, no modifications (default)
hardlinkReplace duplicate with hard link to master (saves space)
symlinkReplace duplicate with symbolic link to master
deleteDelete duplicate file (irreversible)

Audit Logging

All modifications are logged:

=== File Matcher Audit Log ===
Timestamp: 2026-01-20T10:30:00
Action: hardlink
==============================
[2026-01-20T10:30:01] HARDLINK /other_dir/dup.txt -> /master_dir/file.txt (1.2 KB) OK
==============================
Completed: 2 successful, 0 failed
Space reclaimed: 2.4 KB

Exit Codes

CodeMeaning
0Success
1All operations failed
2Invalid arguments or partial failure
130User quit (q or Ctrl+C)

Output Options

Streams: Data goes to stdout, progress/errors to stderr. Use --quiet to suppress progress.

Colors: Auto-enabled for TTY, disabled when piped. Override with --color or --no-color. Respects NO_COLOR and FORCE_COLOR environment variables.

Testing

python3 run_tests.py # Run all 308 tests
python3 -m tests.test_actions # Run specific module

Package Structure

filematcher/
├── cli.py # Command-line interface
├── colors.py # TTY-aware color output
├── hashing.py # MD5/SHA-256 hashing
├── filesystem.py # Filesystem helpers
├── actions.py # Action execution, audit logging
├── formatters.py # Text and JSON formatters
└── directory.py # Directory indexing

Requirements

  • Python 3.9+
  • No external dependencies

License

MIT


See CHANGELOG.md for version history and breaking changes.

About

Deduplicate files by content, not name. Reclaim space with hardlinks/symlinks while preserving alternate filenames. Preview-first safety, audit logging. Built for media libraries.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

File Matcher

Deduplicate files by content, not name. Reclaim space with hardlinks/symlinks while preserving alternate filenames. Preview-first safety, audit logging. Built for media libraries.

Python 3.9+License: MIT

File Matcher Demo

Use Cases

  • Media Libraries — Deduplicate movies/TV shows across Plex, Sonarr, Radarr directories
  • Backups — Find and link identical files across backup drives to save space
  • Downloads — Clean up duplicate downloads while keeping organized copies

Features

  • Content-based matching — Find duplicates by hash, not filename
  • Preserve filenames — Duplicates become links but keep their original names and paths
  • Preview-first safety — See changes before executing (requires --execute flag)
  • Interactive mode — Confirm each action with y/n/a/q prompts
  • Audit logging — Full trail of all modifications
  • Fast mode — Sparse sampling for large files (>100MB)
  • No dependencies — Pure Python standard library

Installation

pip install .# Install
filematcher master_dir other_dir # Run# Or run directly
python file_matcher.py master_dir other_dir
# Development
pip install -e .

Quick Start

# Find matching files
filematcher master_dir other_dir
# Preview deduplication (safe - no changes made)
filematcher master_dir other_dir --action hardlink
# Execute deduplication (interactive confirmation)
filematcher master_dir other_dir --action hardlink --execute
# Execute without prompts (for scripts)
filematcher master_dir other_dir --action hardlink --execute --yes

Usage

Finding Duplicates

filematcher master_dir other_dir # Basic comparison
filematcher master_dir other_dir --different-names-only # Only different filenames
filematcher master_dir other_dir --show-unmatched # Include unmatched files
filematcher master_dir other_dir --summary # Counts only
filematcher master_dir other_dir --fast # Fast mode for large files
filematcher master_dir other_dir --hash sha256 # Use SHA-256 instead of MD5

Deduplicating

The first directory is the master (files preserved). Duplicates in the other directory are replaced/deleted.

# Preview (default - no changes)
filematcher master_dir other_dir --action hardlink
filematcher master_dir other_dir --action symlink
filematcher master_dir other_dir --action delete
# Execute
filematcher master_dir other_dir --action hardlink --execute
filematcher master_dir other_dir --action hardlink --execute --yes # Skip prompts
filematcher master_dir other_dir --action hardlink --execute --log changes.log

Interactive Mode

When running --execute without --yes, you're prompted for each group:

[1/5] Hardlink this group? [y/n/a/q]:
  • y — Execute on this group
  • n — Skip this group
  • a — Execute all remaining without prompting
  • q — Quit immediately

Advanced Options

# Cross-filesystem: fall back to symlink when hardlink fails
filematcher master_dir other_dir --action hardlink --fallback-symlink --execute
# Target directory: create links in a new location, preserving duplicate filenames# e.g., other_dir/movies/film.mkv → /backup/movies/film.mkv (linked to master)
filematcher master_dir other_dir --action hardlink --target-dir /backup --execute

Command-Line Reference

OptionShortDescription
--action-aAction: compare (default), hardlink, symlink, delete
--executeExecute changes (default: preview only)
--yes-ySkip confirmation prompts
--show-unmatched-uShow files with no matches
--different-names-only-dOnly show matches with different filenames
--summary-sShow counts only
--fast-fFast mode for large files (>100MB)
--hash-HHash algorithm: md5 (default), sha256
--verbose-vShow detailed progress
--log-lCustom audit log path
--fallback-symlinkUse symlink if hardlink fails (cross-filesystem)
--target-dir-tCreate links in new location (preserves other_dir structure/names)
--json-jJSON output (see JSON_SCHEMA.md)
--quiet-qSuppress progress messages
--colorForce color output
--no-colorDisable color output

JSON Output

Use --json for machine-readable output. See JSON_SCHEMA.md for full schema.

# Count matches and show space savings (human-readable)
filematcher master_dir other_dir --action hardlink --json | \
jq '{groups: .statistics.groupCount, files: .statistics.duplicateCount, savings_mb: (.statistics.spaceSavingsBytes / 1048576 | floor)}'# List all duplicate paths (one per line)
filematcher master_dir other_dir --action hardlink --json | \
jq -r '.duplicateGroups[].duplicates[].path'# Show master → duplicate mappings
filematcher master_dir other_dir --action hardlink --json | \
jq -r '.duplicateGroups[] | "\(.masterFile) -> \(.duplicates[].path)"'# Find large duplicates (>100MB)
filematcher master_dir other_dir --action hardlink --json | \
jq -r '.duplicateGroups[].duplicates[] | select(.sizeBytes > 104857600) | .path'# Get execution results
filematcher master_dir other_dir --action hardlink --execute --yes --json | \
jq '{success: .execution.successCount, failed: .execution.failureCount, saved_mb: (.execution.spaceSavedBytes / 1048576 | floor)}'# List failures with error messages
filematcher master_dir other_dir --action hardlink --execute --yes --json | \
jq -r '.execution.failures[] | "\(.path): \(.error)"'

Note: --json --execute requires --yes (no interactive prompts in JSON mode).

Actions

ActionDescription
compareFind matches only, no modifications (default)
hardlinkReplace duplicate with hard link to master (saves space)
symlinkReplace duplicate with symbolic link to master
deleteDelete duplicate file (irreversible)

Audit Logging

All modifications are logged:

=== File Matcher Audit Log ===
Timestamp: 2026-01-20T10:30:00
Action: hardlink
==============================
[2026-01-20T10:30:01] HARDLINK /other_dir/dup.txt -> /master_dir/file.txt (1.2 KB) OK
==============================
Completed: 2 successful, 0 failed
Space reclaimed: 2.4 KB

Exit Codes

CodeMeaning
0Success
1All operations failed
2Invalid arguments or partial failure
130User quit (q or Ctrl+C)

Output Options

Streams: Data goes to stdout, progress/errors to stderr. Use --quiet to suppress progress.

Colors: Auto-enabled for TTY, disabled when piped. Override with --color or --no-color. Respects NO_COLOR and FORCE_COLOR environment variables.

Testing

python3 run_tests.py # Run all 308 tests
python3 -m tests.test_actions # Run specific module

Package Structure

filematcher/
├── cli.py # Command-line interface
├── colors.py # TTY-aware color output
├── hashing.py # MD5/SHA-256 hashing
├── filesystem.py # Filesystem helpers
├── actions.py # Action execution, audit logging
├── formatters.py # Text and JSON formatters
└── directory.py # Directory indexing

Requirements

  • Python 3.9+
  • No external dependencies

License

MIT


See CHANGELOG.md for version history and breaking changes.

About

Deduplicate files by content, not name. Reclaim space with hardlinks/symlinks while preserving alternate filenames. Preview-first safety, audit logging. Built for media libraries.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

File Matcher

Deduplicate files by content, not name. Reclaim space with hardlinks/symlinks while preserving alternate filenames. Preview-first safety, audit logging. Built for media libraries.

Python 3.9+License: MIT

File Matcher Demo

Use Cases

  • Media Libraries — Deduplicate movies/TV shows across Plex, Sonarr, Radarr directories
  • Backups — Find and link identical files across backup drives to save space
  • Downloads — Clean up duplicate downloads while keeping organized copies

Features

  • Content-based matching — Find duplicates by hash, not filename
  • Preserve filenames — Duplicates become links but keep their original names and paths
  • Preview-first safety — See changes before executing (requires --execute flag)
  • Interactive mode — Confirm each action with y/n/a/q prompts
  • Audit logging — Full trail of all modifications
  • Fast mode — Sparse sampling for large files (>100MB)
  • No dependencies — Pure Python standard library

Installation

pip install .# Install
filematcher master_dir other_dir # Run# Or run directly
python file_matcher.py master_dir other_dir
# Development
pip install -e .

Quick Start

# Find matching files
filematcher master_dir other_dir
# Preview deduplication (safe - no changes made)
filematcher master_dir other_dir --action hardlink
# Execute deduplication (interactive confirmation)
filematcher master_dir other_dir --action hardlink --execute
# Execute without prompts (for scripts)
filematcher master_dir other_dir --action hardlink --execute --yes

Usage

Finding Duplicates

filematcher master_dir other_dir # Basic comparison
filematcher master_dir other_dir --different-names-only # Only different filenames
filematcher master_dir other_dir --show-unmatched # Include unmatched files
filematcher master_dir other_dir --summary # Counts only
filematcher master_dir other_dir --fast # Fast mode for large files
filematcher master_dir other_dir --hash sha256 # Use SHA-256 instead of MD5

Deduplicating

The first directory is the master (files preserved). Duplicates in the other directory are replaced/deleted.

# Preview (default - no changes)
filematcher master_dir other_dir --action hardlink
filematcher master_dir other_dir --action symlink
filematcher master_dir other_dir --action delete
# Execute
filematcher master_dir other_dir --action hardlink --execute
filematcher master_dir other_dir --action hardlink --execute --yes # Skip prompts
filematcher master_dir other_dir --action hardlink --execute --log changes.log

Interactive Mode

When running --execute without --yes, you're prompted for each group:

[1/5] Hardlink this group? [y/n/a/q]:
  • y — Execute on this group
  • n — Skip this group
  • a — Execute all remaining without prompting
  • q — Quit immediately

Advanced Options

# Cross-filesystem: fall back to symlink when hardlink fails
filematcher master_dir other_dir --action hardlink --fallback-symlink --execute
# Target directory: create links in a new location, preserving duplicate filenames# e.g., other_dir/movies/film.mkv → /backup/movies/film.mkv (linked to master)
filematcher master_dir other_dir --action hardlink --target-dir /backup --execute

Command-Line Reference

OptionShortDescription
--action-aAction: compare (default), hardlink, symlink, delete
--executeExecute changes (default: preview only)
--yes-ySkip confirmation prompts
--show-unmatched-uShow files with no matches
--different-names-only-dOnly show matches with different filenames
--summary-sShow counts only
--fast-fFast mode for large files (>100MB)
--hash-HHash algorithm: md5 (default), sha256
--verbose-vShow detailed progress
--log-lCustom audit log path
--fallback-symlinkUse symlink if hardlink fails (cross-filesystem)
--target-dir-tCreate links in new location (preserves other_dir structure/names)
--json-jJSON output (see JSON_SCHEMA.md)
--quiet-qSuppress progress messages
--colorForce color output
--no-colorDisable color output

JSON Output

Use --json for machine-readable output. See JSON_SCHEMA.md for full schema.

# Count matches and show space savings (human-readable)
filematcher master_dir other_dir --action hardlink --json | \
jq '{groups: .statistics.groupCount, files: .statistics.duplicateCount, savings_mb: (.statistics.spaceSavingsBytes / 1048576 | floor)}'# List all duplicate paths (one per line)
filematcher master_dir other_dir --action hardlink --json | \
jq -r '.duplicateGroups[].duplicates[].path'# Show master → duplicate mappings
filematcher master_dir other_dir --action hardlink --json | \
jq -r '.duplicateGroups[] | "\(.masterFile) -> \(.duplicates[].path)"'# Find large duplicates (>100MB)
filematcher master_dir other_dir --action hardlink --json | \
jq -r '.duplicateGroups[].duplicates[] | select(.sizeBytes > 104857600) | .path'# Get execution results
filematcher master_dir other_dir --action hardlink --execute --yes --json | \
jq '{success: .execution.successCount, failed: .execution.failureCount, saved_mb: (.execution.spaceSavedBytes / 1048576 | floor)}'# List failures with error messages
filematcher master_dir other_dir --action hardlink --execute --yes --json | \
jq -r '.execution.failures[] | "\(.path): \(.error)"'

Note: --json --execute requires --yes (no interactive prompts in JSON mode).

Actions

ActionDescription
compareFind matches only, no modifications (default)
hardlinkReplace duplicate with hard link to master (saves space)
symlinkReplace duplicate with symbolic link to master
deleteDelete duplicate file (irreversible)

Audit Logging

All modifications are logged:

=== File Matcher Audit Log ===
Timestamp: 2026-01-20T10:30:00
Action: hardlink
==============================
[2026-01-20T10:30:01] HARDLINK /other_dir/dup.txt -> /master_dir/file.txt (1.2 KB) OK
==============================
Completed: 2 successful, 0 failed
Space reclaimed: 2.4 KB

Exit Codes

CodeMeaning
0Success
1All operations failed
2Invalid arguments or partial failure
130User quit (q or Ctrl+C)

Output Options

Streams: Data goes to stdout, progress/errors to stderr. Use --quiet to suppress progress.

Colors: Auto-enabled for TTY, disabled when piped. Override with --color or --no-color. Respects NO_COLOR and FORCE_COLOR environment variables.

Testing

python3 run_tests.py # Run all 308 tests
python3 -m tests.test_actions # Run specific module

Package Structure

filematcher/
├── cli.py # Command-line interface
├── colors.py # TTY-aware color output
├── hashing.py # MD5/SHA-256 hashing
├── filesystem.py # Filesystem helpers
├── actions.py # Action execution, audit logging
├── formatters.py # Text and JSON formatters
└── directory.py # Directory indexing

Requirements

  • Python 3.9+
  • No external dependencies

License

MIT


See CHANGELOG.md for version history and breaking changes.

About

Deduplicate files by content, not name. Reclaim space with hardlinks/symlinks while preserving alternate filenames. Preview-first safety, audit logging. Built for media libraries.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

File Matcher

Deduplicate files by content, not name. Reclaim space with hardlinks/symlinks while preserving alternate filenames. Preview-first safety, audit logging. Built for media libraries.

Python 3.9+License: MIT

File Matcher Demo

Use Cases

  • Media Libraries — Deduplicate movies/TV shows across Plex, Sonarr, Radarr directories
  • Backups — Find and link identical files across backup drives to save space
  • Downloads — Clean up duplicate downloads while keeping organized copies

Features

  • Content-based matching — Find duplicates by hash, not filename
  • Preserve filenames — Duplicates become links but keep their original names and paths
  • Preview-first safety — See changes before executing (requires --execute flag)
  • Interactive mode — Confirm each action with y/n/a/q prompts
  • Audit logging — Full trail of all modifications
  • Fast mode — Sparse sampling for large files (>100MB)
  • No dependencies — Pure Python standard library

Installation

pip install .# Install
filematcher master_dir other_dir # Run# Or run directly
python file_matcher.py master_dir other_dir
# Development
pip install -e .

Quick Start

# Find matching files
filematcher master_dir other_dir
# Preview deduplication (safe - no changes made)
filematcher master_dir other_dir --action hardlink
# Execute deduplication (interactive confirmation)
filematcher master_dir other_dir --action hardlink --execute
# Execute without prompts (for scripts)
filematcher master_dir other_dir --action hardlink --execute --yes

Usage

Finding Duplicates

filematcher master_dir other_dir # Basic comparison
filematcher master_dir other_dir --different-names-only # Only different filenames
filematcher master_dir other_dir --show-unmatched # Include unmatched files
filematcher master_dir other_dir --summary # Counts only
filematcher master_dir other_dir --fast # Fast mode for large files
filematcher master_dir other_dir --hash sha256 # Use SHA-256 instead of MD5

Deduplicating

The first directory is the master (files preserved). Duplicates in the other directory are replaced/deleted.

# Preview (default - no changes)
filematcher master_dir other_dir --action hardlink
filematcher master_dir other_dir --action symlink
filematcher master_dir other_dir --action delete
# Execute
filematcher master_dir other_dir --action hardlink --execute
filematcher master_dir other_dir --action hardlink --execute --yes # Skip prompts
filematcher master_dir other_dir --action hardlink --execute --log changes.log

Interactive Mode

When running --execute without --yes, you're prompted for each group:

[1/5] Hardlink this group? [y/n/a/q]:
  • y — Execute on this group
  • n — Skip this group
  • a — Execute all remaining without prompting
  • q — Quit immediately

Advanced Options

# Cross-filesystem: fall back to symlink when hardlink fails
filematcher master_dir other_dir --action hardlink --fallback-symlink --execute
# Target directory: create links in a new location, preserving duplicate filenames# e.g., other_dir/movies/film.mkv → /backup/movies/film.mkv (linked to master)
filematcher master_dir other_dir --action hardlink --target-dir /backup --execute

Command-Line Reference

OptionShortDescription
--action-aAction: compare (default), hardlink, symlink, delete
--executeExecute changes (default: preview only)
--yes-ySkip confirmation prompts
--show-unmatched-uShow files with no matches
--different-names-only-dOnly show matches with different filenames
--summary-sShow counts only
--fast-fFast mode for large files (>100MB)
--hash-HHash algorithm: md5 (default), sha256
--verbose-vShow detailed progress
--log-lCustom audit log path
--fallback-symlinkUse symlink if hardlink fails (cross-filesystem)
--target-dir-tCreate links in new location (preserves other_dir structure/names)
--json-jJSON output (see JSON_SCHEMA.md)
--quiet-qSuppress progress messages
--colorForce color output
--no-colorDisable color output

JSON Output

Use --json for machine-readable output. See JSON_SCHEMA.md for full schema.

# Count matches and show space savings (human-readable)
filematcher master_dir other_dir --action hardlink --json | \
jq '{groups: .statistics.groupCount, files: .statistics.duplicateCount, savings_mb: (.statistics.spaceSavingsBytes / 1048576 | floor)}'# List all duplicate paths (one per line)
filematcher master_dir other_dir --action hardlink --json | \
jq -r '.duplicateGroups[].duplicates[].path'# Show master → duplicate mappings
filematcher master_dir other_dir --action hardlink --json | \
jq -r '.duplicateGroups[] | "\(.masterFile) -> \(.duplicates[].path)"'# Find large duplicates (>100MB)
filematcher master_dir other_dir --action hardlink --json | \
jq -r '.duplicateGroups[].duplicates[] | select(.sizeBytes > 104857600) | .path'# Get execution results
filematcher master_dir other_dir --action hardlink --execute --yes --json | \
jq '{success: .execution.successCount, failed: .execution.failureCount, saved_mb: (.execution.spaceSavedBytes / 1048576 | floor)}'# List failures with error messages
filematcher master_dir other_dir --action hardlink --execute --yes --json | \
jq -r '.execution.failures[] | "\(.path): \(.error)"'

Note: --json --execute requires --yes (no interactive prompts in JSON mode).

Actions

ActionDescription
compareFind matches only, no modifications (default)
hardlinkReplace duplicate with hard link to master (saves space)
symlinkReplace duplicate with symbolic link to master
deleteDelete duplicate file (irreversible)

Audit Logging

All modifications are logged:

=== File Matcher Audit Log ===
Timestamp: 2026-01-20T10:30:00
Action: hardlink
==============================
[2026-01-20T10:30:01] HARDLINK /other_dir/dup.txt -> /master_dir/file.txt (1.2 KB) OK
==============================
Completed: 2 successful, 0 failed
Space reclaimed: 2.4 KB

Exit Codes

CodeMeaning
0Success
1All operations failed
2Invalid arguments or partial failure
130User quit (q or Ctrl+C)

Output Options

Streams: Data goes to stdout, progress/errors to stderr. Use --quiet to suppress progress.

Colors: Auto-enabled for TTY, disabled when piped. Override with --color or --no-color. Respects NO_COLOR and FORCE_COLOR environment variables.

Testing

python3 run_tests.py # Run all 308 tests
python3 -m tests.test_actions # Run specific module

Package Structure

filematcher/
├── cli.py # Command-line interface
├── colors.py # TTY-aware color output
├── hashing.py # MD5/SHA-256 hashing
├── filesystem.py # Filesystem helpers
├── actions.py # Action execution, audit logging
├── formatters.py # Text and JSON formatters
└── directory.py # Directory indexing

Requirements

  • Python 3.9+
  • No external dependencies

License

MIT


See CHANGELOG.md for version history and breaking changes.

About

Deduplicate files by content, not name. Reclaim space with hardlinks/symlinks while preserving alternate filenames. Preview-first safety, audit logging. Built for media libraries.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages