From f56c96cfa396ae70254a066cc8e799de39b7a049 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 21:44:53 +0000 Subject: [PATCH] Never save a downloaded song outside the download location A playlist, artist, album or file name of only dots, such as "..", named the folder above, so a song from a Clementine with such names was saved outside Clementine Remote's folder on Android 9 and earlier, where downloads are files. Such names are now left out, as empty ones are, and a file name left empty falls back to the song's title, as in the iOS client. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01R2bAq6kZW6eyymkA3zsd4p --- .../downloader/ClementineSongDownloader.java | 15 +++++++++++++-- .../backend/downloader/DownloadStorageTest.java | 15 +++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/de/qspool/clementineremote/backend/downloader/ClementineSongDownloader.java b/app/src/main/java/de/qspool/clementineremote/backend/downloader/ClementineSongDownloader.java index 9cdb9d67..2832f9a8 100644 --- a/app/src/main/java/de/qspool/clementineremote/backend/downloader/ClementineSongDownloader.java +++ b/app/src/main/java/de/qspool/clementineremote/backend/downloader/ClementineSongDownloader.java @@ -401,14 +401,25 @@ String buildRelativeDir(ResponseSongFileChunk chunk) { } private static void appendDir(StringBuilder sb, String name) { - String dir = Utilities.removeInvalidFileCharacters(name).trim(); + String dir = safeName(name).trim(); if (!dir.isEmpty()) { sb.append(dir).append('/'); } } + /** The song's file name, or its title if that leaves nothing. */ static String buildFileName(ResponseSongFileChunk chunk) { - return Utilities.removeInvalidFileCharacters(chunk.getSongMetadata().getFilename()); + String name = safeName(chunk.getSongMetadata().getFilename()); + return name.isEmpty() ? safeName(chunk.getSongMetadata().getTitle()) : name; + } + + /** + * [name] without the characters file names can't have. A name of only dots, such as "..", is + * dropped too, so no song is saved outside the download location. + */ + private static String safeName(String name) { + String safe = Utilities.removeInvalidFileCharacters(name); + return safe.trim().matches("\\.*") ? "" : safe; } public DownloadItem getItem() { diff --git a/app/src/test/java/de/qspool/clementineremote/backend/downloader/DownloadStorageTest.java b/app/src/test/java/de/qspool/clementineremote/backend/downloader/DownloadStorageTest.java index d721fe9a..d99d1afb 100644 --- a/app/src/test/java/de/qspool/clementineremote/backend/downloader/DownloadStorageTest.java +++ b/app/src/test/java/de/qspool/clementineremote/backend/downloader/DownloadStorageTest.java @@ -199,6 +199,21 @@ public void emptyTagsDoNotMakeEmptyFolders() { assertEquals("", downloader.buildRelativeDir(chunk("", "", ""))); } + @Test + public void savesNothingOutsideTheDownloadLocation() { + ClementineSongDownloader downloader = new ClementineSongDownloader(); + downloader.setCreateArtistDir(true); + downloader.setCreateAlbumDir(true); + downloader.setIsPlaylist(true, "..."); + downloader.setCreatePlaylistDir(true); + assertEquals("", downloader.buildRelativeDir(chunk("..", "", " .. "))); + + ResponseSongFileChunk chunk = ResponseSongFileChunk.newBuilder() + .setSongMetadata(SongMetadata.newBuilder().setFilename("..").setTitle("Aria")) + .build(); + assertEquals("Aria", ClementineSongDownloader.buildFileName(chunk)); + } + @Test public void unknownFileTypesAreSavedAsAudio() { assertEquals("audio/mpeg", MediaStoreDownloadStorage.mimeType("song"));