From eb64e5281521ad270c5e768fc518c24f1c1b6a48 Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Sat, 27 Jun 2026 18:06:22 -0400 Subject: [PATCH] =?UTF-8?q?Add=20lidarr=5Frelease=5Ffixer.sh=20=E2=80=94?= =?UTF-8?q?=20daily=20fix=20for=20wrong=20MusicBrainz=20release=20editions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reads MUSICBRAINZ_ALBUMID from FLAC (vorbis block type 4) and MP3 (ID3v2 TXXX) files, matches against Lidarr's known releases, switches monitored=true to the correct one, and queues RefreshArtist. Runs before lidarr_cleanup.sh in the daily job list so the strike system doesn't act on files that just needed a release correction. --- Configurations/master.conf | 2 + Deployment/master.conf.template | 2 + Media/lidarr_release_fixer.sh | 462 ++++++++++++++++++++++++++++++++ Notes_To-Do.md | 48 ++++ 4 files changed, 514 insertions(+) create mode 100755 Media/lidarr_release_fixer.sh diff --git a/Configurations/master.conf b/Configurations/master.conf index 1930027..2e97b47 100644 --- a/Configurations/master.conf +++ b/Configurations/master.conf @@ -381,6 +381,7 @@ "Media/media_shares_permissions.sh" # apply permissions — runs before cleaners "Media/media_cleaner.sh anime" # remove junk from anime shares "Media/media_cleaner.sh media" # remove junk from media shares + "Media/lidarr_release_fixer.sh" # fix wrong release editions before cleanup runs "Media/lidarr_cleanup.sh" # remove orphaned music files "Media/sonarr_cleanup.sh" # remove orphaned TV files "Media/radarr_cleanup.sh" # remove orphaned movie files @@ -1063,6 +1064,7 @@ ARR_RADARR_DEFAULT_PROFILE="Any (mine)" # Lidarr shared settings + LIDARR_RELEASE_FIXER_ENABLED=true # set false to disable without removing from job list LIDARR_LOCK_WARN_AGE=3600 # 1hr — large libraries take time, not stuck LIDARR_ORPHAN_AGE=7 # days — files must be older than this before eligible for deletion LIDARR_MAX_DELETE_GB=5 # require --i-know-what-im-doing if deletion exceeds this diff --git a/Deployment/master.conf.template b/Deployment/master.conf.template index c197d31..fa85092 100644 --- a/Deployment/master.conf.template +++ b/Deployment/master.conf.template @@ -376,6 +376,7 @@ "Media/media_shares_permissions.sh" # apply permissions — runs before cleaners "Media/media_cleaner.sh anime" # remove junk from anime shares "Media/media_cleaner.sh media" # remove junk from media shares + #"Media/lidarr_release_fixer.sh" # fix wrong release editions before cleanup runs — enable when ready #"Media/lidarr_cleanup.sh" # remove orphaned music files — enable when ready #"Media/sonarr_cleanup.sh" # remove orphaned TV files — enable when ready "Media/radarr_cleanup.sh" # remove orphaned movie files @@ -1057,6 +1058,7 @@ ARR_RADARR_DEFAULT_PROFILE="Any (mine)" # Lidarr shared settings + LIDARR_RELEASE_FIXER_ENABLED=true # set false to disable without removing from job list LIDARR_LOCK_WARN_AGE=3600 # 1hr — large libraries take time, not stuck LIDARR_ORPHAN_AGE=7 # days — files must be older than this before eligible for deletion LIDARR_MAX_DELETE_GB=5 # require --i-know-what-im-doing if deletion exceeds this diff --git a/Media/lidarr_release_fixer.sh b/Media/lidarr_release_fixer.sh new file mode 100755 index 0000000..1115219 --- /dev/null +++ b/Media/lidarr_release_fixer.sh @@ -0,0 +1,462 @@ +#!/bin/bash +# ============================================================================================== +# ============================= Lidarr Release Fixer =========================================== +# ============================================================================================== +# +# PURPOSE +# ───────────────────────────────────────────────────────────────────────────── +# Fix Lidarr albums where the wrong MusicBrainz release edition was selected, +# causing on-disk files to appear as unimported despite being present. +# +# Root cause: Lidarr tracks one specific release edition per album using +# foreignReleaseId. When this doesn't match the MUSICBRAINZ_ALBUMID embedded +# in the actual files, track ID lookup fails and RescanFolders imports 0 tracks +# even with perfect, fully tagged files. +# +# Fix: Read the MusicBrainz Album ID from the first FLAC or MP3 found in each +# album directory, look that release up in Lidarr's known releases for that +# album, switch monitored=true to the correct one, then queue a RefreshArtist +# to re-sync track IDs and trigger Lidarr's own import scan. +# +# ============================================================================================== +# OPERATIONAL MODEL +# ============================================================================================== +# +# For each monitored album with 0 tracked files: +# 1. Locate the album directory under the artist's root path (title glob match) +# 2. Find the first FLAC or MP3 file in that directory +# 3. Read MUSICBRAINZ_ALBUMID from the file's tags +# 4. Fetch the album's available releases from Lidarr +# 5. If the file's release exists and differs from Lidarr's current selection: +# — PUT the album with the correct release set to monitored=true +# — Mark the artist for a RefreshArtist command +# +# RefreshArtist is batched — one per artist, even if multiple albums were fixed. +# Lidarr handles the post-refresh rescan and import automatically. +# +# ============================================================================================== +# TAG READING +# ============================================================================================== +# +# FLAC — Vorbis comment block (block type 4), key MUSICBRAINZ_ALBUMID +# MP3 — ID3v2 TXXX frame, description "MusicBrainz Album Id" +# Supports Latin-1, UTF-8 (enc 0/3) and UTF-16 (enc 1/2) encodings +# +# ============================================================================================== +# CONFIGURATION +# ============================================================================================== +# +# host*.conf +# HOST1_LIDARR_URL / HOST1_LIDARR_API_KEY / HOST1_LIDARR_MUSIC_ROOT +# HOST1_LIDARR_PATH_MAP — container path → host path translation +# +# master.conf +# LIDARR_RELEASE_FIXER_ENABLED — set false to disable without removing from job list +# LIDARR_VERSION_MAJOR — expected Lidarr major version for API safety check +# +# ============================================================================================== +# RUNTIME MODES +# ============================================================================================== +# +# lidarr_release_fixer.sh — normal run +# lidarr_release_fixer.sh --dry-run — preview, no API writes +# lidarr_release_fixer.sh --log — verbose output +# lidarr_release_fixer.sh --status — show config and exit +# +# ============================================================================================== + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/../load_config.sh" +parse_args "$@" + +# ── Setup ───────────────────────────────────────────────────────────────────────────────────── +if [[ "$EUID" -ne 0 ]]; then + error "Must be run as root" + exit 1 +fi + +for _cmd in curl jq perl; do + if ! command -v "$_cmd" >/dev/null 2>&1; then + error "$_cmd not found — required" + exit 1 + fi +done +unset _cmd + +acquire_lock "skip" +trap "_release_all_locks" EXIT + +detect_hosts + +if [[ -z "${LIDARR_URL:-}" ]] || [[ -z "${LIDARR_API_KEY:-}" ]]; then + info "Lidarr not configured on $MY_ID — skipping" + exit 0 +fi + +if [[ "${LIDARR_RELEASE_FIXER_ENABLED:-true}" == "false" ]]; then + info "LIDARR_RELEASE_FIXER_ENABLED=false — skipping" + exit 0 +fi + +declare -A ARR_PATH_MAP +local_path_map_var="${MY_ID}_LIDARR_PATH_MAP" +eval "for key in \"\${!${local_path_map_var}[@]}\"; do + ARR_PATH_MAP[\"\$key\"]=\"\${${local_path_map_var}[\$key]}\" +done" + +require_var LIDARR_URL +require_var LIDARR_API_KEY +require_var LIDARR_MUSIC_ROOT + +# ── Status ──────────────────────────────────────────────────────────────────────────────────── +if [[ "$SHOW_STATUS" == true ]]; then + echo "" + echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━" + echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)" + echo "$ICON_GEAR Lidarr URL: $LIDARR_URL" + echo "$ICON_GEAR Music root: $LIDARR_MUSIC_ROOT" + echo "$ICON_GEAR Enabled: ${LIDARR_RELEASE_FIXER_ENABLED:-true}" + echo "$ICON_GEAR Dry run: $DRY_RUN" + echo "━━━━━━━━━━━━━━━━━━━━━━━" + exit 0 +fi + +# ── API helpers ─────────────────────────────────────────────────────────────────────────────── +lidarr_api() { + local endpoint="$1" + local response http_code body + response=$(curl -sf --max-time 30 \ + -H "X-Api-Key: $LIDARR_API_KEY" \ + -w "\n%{http_code}" \ + "${LIDARR_URL}/api/v1/${endpoint}" 2>/dev/null) + http_code=$(echo "$response" | tail -1) + body=$(echo "$response" | head -n -1) + if [[ "$http_code" != "200" ]]; then + error "Lidarr API HTTP $http_code for: $endpoint" + return 1 + fi + echo "$body" +} + +lidarr_api_put() { + local endpoint="$1" + local payload="$2" + local http_code + http_code=$(curl -sf --max-time 30 -X PUT \ + -H "X-Api-Key: $LIDARR_API_KEY" \ + -H "Content-Type: application/json" \ + -d "$payload" \ + -w "%{http_code}" -o /dev/null \ + "${LIDARR_URL}/api/v1/${endpoint}" 2>/dev/null) + if [[ "$http_code" != "202" ]] && [[ "$http_code" != "200" ]]; then + error "Lidarr PUT HTTP $http_code for: $endpoint" + return 1 + fi +} + +lidarr_api_post() { + local endpoint="$1" + local payload="$2" + curl -sf --max-time 30 -X POST \ + -H "X-Api-Key: $LIDARR_API_KEY" \ + -H "Content-Type: application/json" \ + -d "$payload" \ + -o /dev/null \ + "${LIDARR_URL}/api/v1/${endpoint}" 2>/dev/null +} + +# ── Tag readers ─────────────────────────────────────────────────────────────────────────────── +# Read MUSICBRAINZ_ALBUMID from a FLAC file's Vorbis comment block (block type 4) +read_flac_mbid() { + perl -e ' + open(my $fh, "<:raw", $ARGV[0]) or exit; + read($fh, my $magic, 4); substr($magic, 0, 4) eq "fLaC" or exit; + while (1) { + read($fh, my $hdr, 4) == 4 or last; + my $w = unpack("N", $hdr); + my $last = ($w >> 31) & 1; + my $type = ($w >> 24) & 0x7f; + my $len = $w & 0xffffff; + read($fh, my $data, $len); + if ($type == 4) { + my $pos = 0; + my $vl = unpack("V", substr($data, $pos, 4)); $pos += 4 + $vl; + my $n = unpack("V", substr($data, $pos, 4)); $pos += 4; + for (1..$n) { + my $cl = unpack("V", substr($data, $pos, 4)); $pos += 4; + my $c = substr($data, $pos, $cl); $pos += $cl; + if ($c =~ /^MUSICBRAINZ_ALBUMID=(.+)$/i) { print "$1\n"; exit; } + } + } + last if $last; + } + ' "$1" 2>/dev/null +} + +# Read MusicBrainz Album Id from an MP3's ID3v2 TXXX frame. +# Handles Latin-1/UTF-8 (enc 0/3) with single-null separator and +# UTF-16 (enc 1/2) by stripping null bytes and pattern-matching. +read_mp3_mbid() { + perl -e ' + open(my $fh, "<:raw", $ARGV[0]) or exit; + read($fh, my $hdr, 10) == 10 or exit; + substr($hdr, 0, 3) eq "ID3" or exit; + my $ver = ord(substr($hdr, 3, 1)); + my $sz = 0; $sz = ($sz << 7) | ord($_) for split //, substr($hdr, 6, 4); + read($fh, my $data, $sz) == $sz or exit; + my $pos = 0; + while ($pos + 10 <= $sz) { + my $id = substr($data, $pos, 4); $pos += 4; + last unless $id =~ /^[A-Z][A-Z0-9]{3}$/; + my $fs; + if ($ver >= 4) { + my $n = 0; $n = ($n << 7) | ord($_) for split //, substr($data, $pos, 4); + $fs = $n; + } else { + $fs = unpack("N", substr($data, $pos, 4)); + } + $pos += 6; + last if $fs < 1 || $pos + $fs > $sz; + if ($id eq "TXXX") { + my $enc = ord(substr($data, $pos, 1)); + my $body = substr($data, $pos + 1, $fs - 1); + if ($enc == 1 || $enc == 2) { + # UTF-16: strip BOM, collapse to ASCII, pattern match + $body =~ s/^\xff\xfe|^\xfe\xff//; + (my $flat = $body) =~ s/\x00//g; + if ($flat =~ /^MusicBrainz Album Id([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i) { + print "$1\n"; exit; + } + } else { + my ($desc, $val) = split /\x00/, $body, 2; + if (defined $desc && lc($desc) eq "musicbrainz album id" && + defined $val && + $val =~ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i) { + print "$val\n"; exit; + } + } + } + $pos += $fs; + } + ' "$1" 2>/dev/null +} + +read_file_mbid() { + local file="$1" + case "${file##*.}" in + [Ff][Ll][Aa][Cc]) read_flac_mbid "$file" ;; + [Mm][Pp]3) read_mp3_mbid "$file" ;; + esac +} + +translate_container_path() { + local cpath="$1" + for cp in "${!ARR_PATH_MAP[@]}"; do + if [[ "$cpath" == "${cp}"* ]]; then + echo "${ARR_PATH_MAP[$cp]}${cpath#$cp}" + return + fi + done + echo "$cpath" +} + +# ── Safety checks ───────────────────────────────────────────────────────────────────────────── +echo "" +echo "━━━ $ICON_SHIELD Safety Checks ━━━" + +if ! check_api "$LIDARR_URL" "Lidarr" 10; then + notify "Lidarr release fixer aborted on $(hostname) — API unreachable" \ + "Lidarr Release Fixer" "warning" + exit 1 +fi +check_arr_version "$LIDARR_URL" "$LIDARR_API_KEY" "v1" "$LIDARR_VERSION_MAJOR" "Lidarr" || exit 1 +info "API reachable and version OK" + +# ── Fetch all artists and build path cache ──────────────────────────────────────────────────── +echo "" +echo "━━━ $ICON_SYNC Fetching artist paths ━━━" + +declare -A ARTIST_PATH_CACHE +declare -A ARTIST_NAME_CACHE + +ALL_ARTISTS=$(lidarr_api "artist") || { + error "Failed to fetch artists" + exit 1 +} + +while IFS= read -r artist; do + aid=$(echo "$artist" | jq -r '.id') + apath=$(echo "$artist" | jq -r '.path // empty') + aname=$(echo "$artist" | jq -r '.artistName // empty') + [[ -n "$apath" ]] && ARTIST_PATH_CACHE[$aid]=$(translate_container_path "$apath") + [[ -n "$aname" ]] && ARTIST_NAME_CACHE[$aid]="$aname" +done < <(echo "$ALL_ARTISTS" | jq -c '.[]' 2>/dev/null) +unset ALL_ARTISTS + +ARTIST_COUNT="${#ARTIST_PATH_CACHE[@]}" +info "Loaded $ARTIST_COUNT artists" + +if [[ "$ARTIST_COUNT" -eq 0 ]]; then + error "No artists returned — aborting" + exit 1 +fi + +# ── Fetch zero-file albums ──────────────────────────────────────────────────────────────────── +echo "" +echo "━━━ $ICON_SYNC Fetching zero-file monitored albums ━━━" + +ALL_ALBUMS=$(lidarr_api "album") || { + error "Failed to fetch albums" + exit 1 +} + +ZERO_FILE_ALBUMS=$(echo "$ALL_ALBUMS" | jq -c \ + '[.[] | select(.monitored == true and .statistics.trackFileCount == 0)]' 2>/dev/null) +unset ALL_ALBUMS + +TOTAL_ZERO=$(echo "$ZERO_FILE_ALBUMS" | jq 'length' 2>/dev/null) +info "Monitored albums with 0 tracked files: $TOTAL_ZERO" + +# ── Process albums ──────────────────────────────────────────────────────────────────────────── +START=$(date +%s) +FIXED=0 +SKIPPED_NO_DIR=0 +SKIPPED_NO_FILE=0 +SKIPPED_NO_MBID=0 +SKIPPED_NO_MATCH=0 +SKIPPED_CORRECT=0 +ERRORS=0 +declare -A ARTISTS_TO_REFRESH + +echo "" +echo "━━━ $ICON_GEAR Processing albums ━━━" + +while IFS= read -r album; do + album_id=$(echo "$album" | jq -r '.id') + album_title=$(echo "$album" | jq -r '.title') + artist_id=$(echo "$album" | jq -r '.artistId') + artist_name="${ARTIST_NAME_CACHE[$artist_id]:-artist $artist_id}" + + log "Checking: $artist_name — $album_title (album $album_id)" + + artist_host_path="${ARTIST_PATH_CACHE[$artist_id]:-}" + if [[ -z "$artist_host_path" ]] || [[ ! -d "$artist_host_path" ]]; then + log "Artist dir not found: $artist_host_path" + (( SKIPPED_NO_DIR++ )) + continue + fi + + # Find album directory — case-insensitive prefix match on title + album_dir=$(find "$artist_host_path" -maxdepth 1 -type d -iname "${album_title}*" \ + 2>/dev/null | head -1) + if [[ -z "$album_dir" ]]; then + log "Album dir not found: $album_title" + (( SKIPPED_NO_DIR++ )) + continue + fi + + # Find first FLAC or MP3 + music_file=$(find "$album_dir" -maxdepth 2 -type f \ + \( -iname "*.flac" -o -iname "*.mp3" \) 2>/dev/null | head -1) + if [[ -z "$music_file" ]]; then + log "No FLAC or MP3 in: $album_dir" + (( SKIPPED_NO_FILE++ )) + continue + fi + + # Read MBID from file tags + file_mbid=$(read_file_mbid "$music_file") + if [[ -z "$file_mbid" ]]; then + log "No MBID tag in: $music_file" + (( SKIPPED_NO_MBID++ )) + continue + fi + log "File MBID: $file_mbid" + + # Fetch full album JSON (includes releases array) + album_full=$(lidarr_api "album/${album_id}") || { (( ERRORS++ )); sleep 0.2; continue; } + sleep 0.1 + + # Currently selected release + current_release_id=$(echo "$album_full" | jq -r \ + '.releases[] | select(.monitored == true) | .foreignReleaseId' 2>/dev/null | head -1) + + if [[ "$current_release_id" == "$file_mbid" ]]; then + log "Release already correct: $file_mbid" + (( SKIPPED_CORRECT++ )) + continue + fi + + # Verify file's MBID is a known release for this album + release_known=$(echo "$album_full" | jq -r \ + --arg rid "$file_mbid" \ + '.releases[] | select(.foreignReleaseId == $rid) | .foreignReleaseId' 2>/dev/null) + if [[ -z "$release_known" ]]; then + log "File MBID $file_mbid not in Lidarr release list for: $album_title" + (( SKIPPED_NO_MATCH++ )) + continue + fi + + # Build updated album object with correct release selected + updated_album=$(echo "$album_full" | jq \ + --arg target "$file_mbid" \ + '.releases = [.releases[] | .monitored = (.foreignReleaseId == $target)]' 2>/dev/null) + + if [[ "$DRY_RUN" == true ]]; then + warn "DRY RUN — would fix: $artist_name — $album_title" + warn " $current_release_id → $file_mbid" + (( FIXED++ )) + continue + fi + + if lidarr_api_put "album/${album_id}" "$updated_album"; then + warn "$ICON_GEAR Fixed: $artist_name — $album_title" + warn " $current_release_id → $file_mbid" + ARTISTS_TO_REFRESH[$artist_id]="$artist_id" + (( FIXED++ )) + else + (( ERRORS++ )) + fi + sleep 0.2 + +done < <(echo "$ZERO_FILE_ALBUMS" | jq -c '.[]') + +# ── Queue RefreshArtist for all fixed artists ───────────────────────────────────────────────── +if [[ "${#ARTISTS_TO_REFRESH[@]}" -gt 0 ]] && [[ "$DRY_RUN" == false ]]; then + echo "" + echo "━━━ $ICON_SYNC Queuing RefreshArtist ━━━" + for artist_id in "${!ARTISTS_TO_REFRESH[@]}"; do + if lidarr_api_post "command" \ + "{\"name\": \"RefreshArtist\", \"artistId\": ${artist_id}}"; then + log "Queued RefreshArtist for: ${ARTIST_NAME_CACHE[$artist_id]:-$artist_id}" + else + error "Failed to queue RefreshArtist for artist $artist_id" + fi + sleep 0.1 + done + info "Refresh queued for ${#ARTISTS_TO_REFRESH[@]} artist(s)" +fi + +# ── Summary ─────────────────────────────────────────────────────────────────────────────────── +END=$(date +%s) +echo "" +echo "━━━━━ $ICON_SUMMARY LIDARR RELEASE FIXER SUMMARY ━━━━━" +echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)" +echo "$ICON_GEAR Candidates: $TOTAL_ZERO zero-file albums" +echo "$ICON_DONE Fixed: $FIXED" +echo "$ICON_SKIP Already correct: $SKIPPED_CORRECT" +echo "$ICON_SKIP No dir on disk: $SKIPPED_NO_DIR" +echo "$ICON_SKIP No music file: $SKIPPED_NO_FILE" +echo "$ICON_SKIP No MBID tag: $SKIPPED_NO_MBID" +echo "$ICON_SKIP MBID not in list: $SKIPPED_NO_MATCH" +[[ "$ERRORS" -gt 0 ]] && echo " Errors: $ERRORS" +echo "$ICON_TIME Duration: $(format_duration $(( END - START )))" +[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes made" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + +if [[ "$FIXED" -gt 0 ]] && [[ "$DRY_RUN" == false ]]; then + notify "Lidarr release fixer on $(hostname) — corrected $FIXED album release(s)" \ + "Lidarr Release Fixer" "normal" +fi + +exit 0 diff --git a/Notes_To-Do.md b/Notes_To-Do.md index bcab091..659c363 100755 --- a/Notes_To-Do.md +++ b/Notes_To-Do.md @@ -4,8 +4,17 @@ claude +need a script that check file locations. like if a kids movie is in movies. it needs to detect this and update the location in the arrs. so it would tell radarr to move shrek from movies to kids movies and trck the new file +look at a way to better determine when a container was manually shutdown for docker watchdog + + + +we may need to add this to our preflight before lidarr cleanup. check if lidarr need corrected before + removal. since we have a strike list we can save results of previous scan then run this script then + continue. lidarr has been wierd for the last year. maybe we +──────────────────────────────────────────────────────────────── @@ -27,6 +36,45 @@ claude + + + + + + + + +Saved. Here's where we land and what to pick up: + +What happened: Lidarr was silently discarding valid files because it selected the wrong MusicBrainz release edition (Brazil CD, Japan Digital, etc.) while your files were tagged for a different one (US CD, Vinyl). The track IDs don't match across editions, so RescanFolders always returned "Importing 0 tracks." Not a file problem — a database mismatch. The 5GB threshold in lidarr_cleanup.sh was doing exactly what it should. + +Fixed this session: 123 albums bulk-corrected (read MUSICBRAINZ_ALBUMID from FLAC tags, switched Lidarr's release selection, RefreshArtist). Full root rescan still running in the background. + +Next session — pick up here: +1. Design the daily fixer script (Media/lidarr_release_fixer.sh) — FLAC + MP3 (ID3 TXXX) tag reading, same bulk-fix logic, runs from daily orchestrator. Strike system in lidarr_cleanup.sh already provides the protection window. +2. 42 albums where file MBID isn't in Lidarr at all (unofficial/re-tagged) — manual import or ignore. +3. 217 MP3 albums — need ID3 TXXX frame reader added to the fixer. +4. Check the full root rescan completed and see if zero-file count dropped further below 927. + + + + + + + + + + + + + + + + + + + +