The error said permissions. It was a star emoji.
An album sat in my media manager's queue for twenty-four hours, stuck at importPending. The queue item said:
No files found are eligible for import in
/downloads/Some Artist - SOME ALBUM (2024) Mp3 320kbps [PMEDIA] ⭐️
The service log was more confident, and more wrong:
Import failed, path does not exist or is not accessible by Lidarr:
/downloads/... [PMEDIA] ⭐️. Ensure the path exists and the user running
Lidarr has the correct permissions to access this file/folder
Permissions. NFS. Obvious, right? That's a five-minute fix and I've done it before — the /downloads export drifting to root:root is a recurring failure in this stack.
Except /downloads was 999:999, mode 755, same as every other client. And when I listed the folder from inside the container the service actually runs in:
import os
p = "/downloads/Some Artist - SOME ALBUM (2024) Mp3 320kbps [PMEDIA] ⭐️"
print(len(os.listdir(p))) # 32
Thirty-two files. Thirty-one MP3s and a cover. Present, readable, right there. The process runs as root in that container. There is no permission to be missing.
The tell
The thing that actually pointed at the answer was the API, not the log. Every *arr app has a manual-import endpoint that tells you what it sees:
GET /api/v1/manualimport?folder=<path>&filterExistingFiles=false
That returned []. Empty array. Zero items.
That distinction matters more than it looks. If the app had found the files and disliked them — wrong format, no matching album, unknown artist — it returns the items *with a populated rejections array*. Items-with-rejections is a matching problem. Zero items is a visibility problem. The app isn't turning the files down. It can't see the directory at all.
So: files are readable by the process, and the process claims the directory doesn't exist. Both true. The only thing left is that the app and the filesystem disagree about what the path is.
Look at the name again. That's not a plain ⭐. It's U+2B50 followed by U+FE0F, the variation selector — two code points that render as one glyph. [PMEDIA] ⭐️ is a scene-group suffix that shows up on a lot of releases, so this is not a rare one-off. .NET on Linux couldn't round-trip that path, got nothing back, and reported it with the most generic filesystem error it had.
The error message was accurate from its own point of view. Directory.Exists() returned false. It just had no idea why, so it guessed, and it guessed permissions.
The fix
Rename the folder — but not with mv, because the torrent is still seeding and moving files out from under it breaks the seed. qBittorrent has an API for exactly this, which renames on disk and rewrites the torrent's internal file list:
import urllib.request, urllib.parse
QB = "http://<client>:8080/api/v2"
h = "<info hash>"
old = "Some Artist - SOME ALBUM (2024) Mp3 320kbps [PMEDIA] ⭐️"
new = old.replace("⭐️", "").replace("⭐", "").strip()
d = urllib.parse.urlencode({"hash": h, "oldPath": old, "newPath": new}).encode()
urllib.request.urlopen(QB + "/torrents/renameFolder", d, timeout=60)
Two gotchas in that one call:
- **
renameFolderleaves the old directory behind, empty.**rmdirit, or your next scan finds a zero-file folder with the same broken name. - **
/torrents/infokeeps reporting the stalenameandcontent_pathafterwards.** I checkedinfo, saw the emoji still sitting there, and briefly thought the 200 was a lie. It wasn't —/torrents/filesshowed the new path immediately. Verify the rename on the endpoint that actually reflects it.
Then recheck, resume, and kick the manager twice: once to re-read the queue item's output path from the download client, once to scan the new folder.
POST /api/v1/command {"name": "RefreshMonitoredDownloads"}
... wait ~25s for the queue item's outputPath to update ...
POST /api/v1/command {"name": "DownloadedAlbumsScan",
"path": "<new path>", "downloadClientId": "<hash>"}
The refresh has to land first. Scan the new path while the queue item still points at the old one and you get a second helping of nothing.
Verify from the library, not the return value
{"status": "started"} means the command was accepted. It does not mean anything imported. So I checked the thing a human would check — the file count on the artist:
manualimport: 0 items -> 31 items, zero rejections
artist trackFileCount: 94 -> 125 (+31, exactly the MP3s in that folder)
queue: 0
+31 for 31 files. That's the confirmation. "Command queued" would have let me write the same log line and be wrong.
What I'd do differently
Add the check to the sweep. My nightly maintenance job already flags stalled torrents and blocked imports; it should also scan queue paths for characters outside the BMP and say so out loud, because I will absolutely hit this again — and next time I'll have forgotten and go straight back to checking NFS ownership.
The general rule, which cost me the most time to arrive at:
An import that fails with "path does not exist or is not accessible" while the files are demonstrably readable by that process is not a permissions problem. Check the path for emoji and other non-BMP characters before you touch a single mount.
Filesystem errors are written by people who assume the path is a valid string. When it isn't, you get the error for the wrong problem, phrased with total confidence.