diff --git a/.github/workflows/trivy_scan.yml b/.github/workflows/trivy_scan.yml index 7bf116ac0..c08ea8306 100644 --- a/.github/workflows/trivy_scan.yml +++ b/.github/workflows/trivy_scan.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@v4 - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@0.24.0 + uses: aquasecurity/trivy-action@0.28.0 with: scan-type: 'fs' format: 'sarif' diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/MediaFileService.java b/airsonic-main/src/main/java/org/airsonic/player/service/MediaFileService.java index b178a18e1..2b7ba18c2 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/MediaFileService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/MediaFileService.java @@ -134,6 +134,10 @@ public class MediaFileService { private final Map> lastPlayed = new ConcurrentHashMap<>(); + private boolean hasBOM(byte[] bom, int bytesRead) { + return bytesRead == 3 && bom[0] == (byte) 0xEF && bom[1] == (byte) 0xBB && bom[2] == (byte) 0xBF; + } + public MediaFile getMediaFile(String pathName) { return getMediaFile(Paths.get(pathName)); } @@ -1349,13 +1353,27 @@ private CueSheet getCueSheet(@Nonnull Path cueFile) { if (cm != null && cm.getConfidence() > THRESHOLD) { cs = Charset.forName(cm.getName()); } + LOG.debug("Detected charset for cuesheet file {}: Charset detected as {}", cueFile, cs); + bis.mark(3); + + // check for BOM + byte[] bom = new byte[3]; + int bytesRead = bis.read(bom, 0, 3); + if (!hasBOM(bom, bytesRead)) { + bis.reset(); + } + cueSheet = CueParser.parse(bis, cs); } catch (IOException e) { LOG.warn("Defaulting to UTF-8 for cuesheet {}", cueFile); } - cueSheet = CueParser.parse(cueFile, cs); - if (cueSheet.getMessages().stream().filter(m -> m.toString().toLowerCase().contains("warning")).findFirst().isPresent()) { - LOG.warn("Error parsing cuesheet {}", cueFile); - return null; + if (cueSheet != null) { + if (cueSheet.getMessages().stream().filter(m -> m.toString().toLowerCase().contains("warning")) + .map(m -> { + LOG.warn("Parsing {} at line {} : {}", cueFile, m.getLineNumber(), m.getMessage()); + return m; + }).findFirst().isPresent()) { + cueSheet = null; + } } break; case "flac": diff --git a/airsonic-main/src/test/java/org/airsonic/player/service/MediaScannerServiceTestCase.java b/airsonic-main/src/test/java/org/airsonic/player/service/MediaScannerServiceTestCase.java index 975f1a4d0..e95edde06 100644 --- a/airsonic-main/src/test/java/org/airsonic/player/service/MediaScannerServiceTestCase.java +++ b/airsonic-main/src/test/java/org/airsonic/player/service/MediaScannerServiceTestCase.java @@ -319,6 +319,70 @@ public void testMusicCue() { assertEquals(0.0d, track1.getStartPosition(), 0.0d); } + @Test + public void testMusicCueWithBOM() { + LOG.info("start testMusicCueWithBOM"); + + // Add the "cue" folder to the database + Path musicFolderFile = MusicFolderTestData.resolveMusicCueWithBOMFolderPath(); + MusicFolder musicFolder = new MusicFolder(musicFolderFile, "Cue", Type.MEDIA, true, Instant.now().truncatedTo(ChronoUnit.MICROS)); + testFolders.add(musicFolder); + musicFolderRepository.saveAll(testFolders); + TestCaseUtils.execScan(mediaScannerService); + + // Retrieve the "Cue" folder from the database to make + // sure that we don't accidentally operate on other folders + // from previous tests. + musicFolder = musicFolderRepository.findById(musicFolder.getId()).get(); + List folders = new ArrayList<>(); + folders.add(musicFolder); + + // Test that the artist is correctly imported + List allArtists = artistService.getAlphabeticalArtists(folders); + assertEquals(1, allArtists.size()); + Artist artist = allArtists.get(0); + assertEquals("TestCueArtist", artist.getName()); + assertEquals(1, artist.getAlbumCount()); + + + // Test that the album is correctly imported + List allAlbums = albumService.getAlphabeticalAlbums(true, true, folders); + assertEquals(1, allAlbums.size()); + Album album = allAlbums.get(0); + assertEquals("AirsonicTest", album.getName()); + assertEquals("TestCueArtist", album.getArtist()); + assertEquals(2, album.getSongCount()); + + // Test that the music file is correctly imported + List albumFiles = mediaFileRepository.findByFolderAndParentPath(allAlbums.get(0).getFolder(), allAlbums.get(0).getPath(), Sort.by("startPosition")); + assertEquals(3, albumFiles.size()); + MediaFile file = albumFiles.get(0); + assertEquals("airsonic-test", file.getTitle()); + assertEquals("wav", file.getFormat()); + assertNull(file.getAlbumName()); + assertNull(file.getArtist()); + assertNull(file.getAlbumArtist()); + assertNull(file.getTrackNumber()); + assertNull(file.getYear()); + assertEquals(album.getPath(), file.getParentPath()); + assertEquals(Paths.get(album.getPath()).resolve("airsonic-test.wav").toString(), file.getPath()); + assertTrue(file.getIndexPath().contains("airsonic-test.cue")); + assertEquals(-1.0d, file.getStartPosition(), 0.0d); + + MediaFile track1 = albumFiles.get(1); + assertEquals("Handel", track1.getTitle()); + assertEquals("wav", track1.getFormat()); + assertEquals(track1.getAlbumName(), "AirsonicTest"); + assertEquals("Beecham", track1.getArtist()); + assertEquals("TestCueArtist", track1.getAlbumArtist()); + assertEquals(1L, (long)track1.getTrackNumber()); + assertNull(track1.getYear()); + assertEquals(album.getPath(), track1.getParentPath()); + assertEquals(Paths.get(album.getPath()).resolve("airsonic-test.wav").toString(), track1.getPath()); + assertNull(track1.getIndexPath()); + assertEquals(0.0d, track1.getStartPosition(), 0.0d); + } + @Test public void testMusicCueWithDisableCueIndexing() { LOG.info("start testMusicCueWithDisableCueIndexing"); diff --git a/airsonic-main/src/test/java/org/airsonic/player/util/MusicFolderTestData.java b/airsonic-main/src/test/java/org/airsonic/player/util/MusicFolderTestData.java index 23ac2c0a9..3c22d779c 100644 --- a/airsonic-main/src/test/java/org/airsonic/player/util/MusicFolderTestData.java +++ b/airsonic-main/src/test/java/org/airsonic/player/util/MusicFolderTestData.java @@ -43,6 +43,10 @@ public static Path resolveMusicCueFolderPath() { return resolveBaseMediaPath().resolve("cue"); } + public static Path resolveMusicCueWithBOMFolderPath() { + return resolveBaseMediaPath().resolve("cueBom"); + } + public static Path resolveMusicDisableCueFolderPath() { return resolveBaseMediaPath().resolve("disableCue"); } diff --git a/airsonic-main/src/test/resources/MEDIAS/cueBom/airsonic-test.cue b/airsonic-main/src/test/resources/MEDIAS/cueBom/airsonic-test.cue new file mode 100644 index 000000000..25acaaa6b --- /dev/null +++ b/airsonic-main/src/test/resources/MEDIAS/cueBom/airsonic-test.cue @@ -0,0 +1,13 @@ +FILE "airsonic-test.wav" MOTOROLA +PERFORMER "TestCueArtist" +TITLE "AirsonicTest" +REM DATE 2023 +REM GENRE "Classic" +TRACK 01 AUDIO + TITLE "Handel" + PERFORMER "Beecham" + INDEX 01 00:00:00 +TRACK 02 AUDIO + TITLE "Jesu, Joy of Man's Desiring" + PERFORMER "Lipatti" + INDEX 01 04:01:31 diff --git a/airsonic-main/src/test/resources/MEDIAS/cueBom/airsonic-test.wav b/airsonic-main/src/test/resources/MEDIAS/cueBom/airsonic-test.wav new file mode 100644 index 000000000..02305e20e Binary files /dev/null and b/airsonic-main/src/test/resources/MEDIAS/cueBom/airsonic-test.wav differ diff --git a/pom.xml b/pom.xml index 70fcf617b..88ea4f0eb 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ 17 4.0.5 2.3 - 9.11.1 + 9.12.0 ghcr.io/kagemomiji/airsonic-advanced 17.0.10_7 @@ -111,7 +111,7 @@ com.google.guava guava - 33.3.0-jre + 33.3.1-jre jakarta.xml.bind @@ -161,7 +161,7 @@ com.google.errorprone error_prone_annotations - 2.32.0 + 2.36.0 net.java.dev.jna @@ -222,7 +222,7 @@ io.fabric8 docker-maven-plugin - 0.45.0 + 0.45.1 org.apache.maven.plugins