diff --git a/airsonic-main/src/main/java/org/airsonic/player/config/AirsonicHomeConfig.java b/airsonic-main/src/main/java/org/airsonic/player/config/AirsonicHomeConfig.java index e804b9f8b..6ea778d59 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/config/AirsonicHomeConfig.java +++ b/airsonic-main/src/main/java/org/airsonic/player/config/AirsonicHomeConfig.java @@ -103,7 +103,7 @@ public String getDefaultLogFile() { * @return The default database URL. Never {@code null}. */ public String getDefaultJDBCUrl() { - return "jdbc:hsqldb:file:" + getAirsonicHome().resolve("db").resolve(getFileSystemAppName()).toString() + ";hsqldb.tx=mvcc;sql.enforce_size=false;sql.char_literal=false;sql.nulls_first=false;sql.pad_space=false;hsqldb.defrag_limit=50;shutdown=true"; + return "jdbc:hsqldb:file:" + getAirsonicHome().resolve("db").resolve(getFileSystemAppName()).toString() + ";hsqldb.tx=mvcc;sql.enforce_size=false;sql.char_literal=false;sql.nulls_first=false;sql.pad_space=false;hsqldb.defrag_limit=50;hsqldb.default_table_type=CACHED;shutdown=true"; } /** diff --git a/airsonic-main/src/main/java/org/airsonic/player/config/AirsonicScanConfig.java b/airsonic-main/src/main/java/org/airsonic/player/config/AirsonicScanConfig.java index 2afcd3b25..adfaf30f0 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/config/AirsonicScanConfig.java +++ b/airsonic-main/src/main/java/org/airsonic/player/config/AirsonicScanConfig.java @@ -37,8 +37,8 @@ public class AirsonicScanConfig { // Logger private static final Logger LOG = LoggerFactory.getLogger(AirsonicHomeConfig.class); - private static final int DEFAULT_SCAN = 10 * 60; - private static final int DEFAULT_FULLSCAN = 60 * 60; + private static final int DEFAULT_SCAN = 60 * 60; + private static final int DEFAULT_FULLSCAN = 4 * 60 * 60; @Positive private final Integer fullTimeout; diff --git a/airsonic-main/src/main/java/org/airsonic/player/domain/Album.java b/airsonic-main/src/main/java/org/airsonic/player/domain/Album.java index a69d241eb..dfc5fbaa3 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/domain/Album.java +++ b/airsonic-main/src/main/java/org/airsonic/player/domain/Album.java @@ -29,6 +29,7 @@ import java.time.Instant; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; @@ -89,7 +90,6 @@ public class Album { @OneToMany(mappedBy = "album") private List starredAlbums = new ArrayList<>(); - // TODO: add relation to music folder table @ManyToOne @JoinColumn(name = "folder_id", referencedColumnName = "id") private MusicFolder folder; @@ -287,4 +287,21 @@ public void setArt(CoverArt art) { this.art = art; } + @Override + public int hashCode() { + return Objects.hash(path, folder); + } + + @Override + public boolean equals(Object obj) { + + if (obj == null || !(obj instanceof Album)) { + return false; + } + + Album other = (Album) obj; + + return Objects.equals(path, other.path) && Objects.equals(folder, other.folder); + } + } diff --git a/airsonic-main/src/main/java/org/airsonic/player/domain/Artist.java b/airsonic-main/src/main/java/org/airsonic/player/domain/Artist.java index fc202adcc..91851aea3 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/domain/Artist.java +++ b/airsonic-main/src/main/java/org/airsonic/player/domain/Artist.java @@ -25,6 +25,7 @@ import javax.persistence.*; import java.time.Instant; +import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; @@ -127,4 +128,20 @@ public CoverArt getArt() { public void setArt(CoverArt art) { this.art = art; } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + @Override + public boolean equals(Object obj) { + + if (obj == null || !(obj instanceof Artist)) { + return false; + } + + Artist other = (Artist) obj; + return Objects.equals(name, other.name); + } } diff --git a/airsonic-main/src/main/java/org/airsonic/player/domain/Bookmark.java b/airsonic-main/src/main/java/org/airsonic/player/domain/Bookmark.java index 43ab4cf35..7f88ff721 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/domain/Bookmark.java +++ b/airsonic-main/src/main/java/org/airsonic/player/domain/Bookmark.java @@ -23,6 +23,7 @@ import javax.persistence.*; import java.time.Instant; +import java.util.Objects; /** * A bookmark within a media file, for a given user. @@ -135,4 +136,19 @@ public void setChanged(Instant changed) { this.changed = changed; } + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public boolean equals(Object obj) { + + if (obj == null || !(obj instanceof Bookmark)) { + return false; + } + + return Objects.equals(id, ((Bookmark) obj).id); + } + } diff --git a/airsonic-main/src/main/java/org/airsonic/player/domain/Genre.java b/airsonic-main/src/main/java/org/airsonic/player/domain/Genre.java index a4c919465..4c977ed0f 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/domain/Genre.java +++ b/airsonic-main/src/main/java/org/airsonic/player/domain/Genre.java @@ -24,6 +24,7 @@ import javax.persistence.*; +import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; /** @@ -80,4 +81,20 @@ public void incrementAlbumCount() { public void incrementSongCount() { songCount.incrementAndGet(); } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + @Override + public boolean equals(Object obj) { + + if (obj == null || !(obj instanceof Genre)) { + return false; + } + + Genre other = (Genre) obj; + return Objects.equals(name, other.name); + } } \ No newline at end of file diff --git a/airsonic-main/src/main/java/org/airsonic/player/domain/Player.java b/airsonic-main/src/main/java/org/airsonic/player/domain/Player.java index 552ad1fe1..35103be69 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/domain/Player.java +++ b/airsonic-main/src/main/java/org/airsonic/player/domain/Player.java @@ -27,6 +27,7 @@ import java.time.Instant; import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * Represents a remote player. A player has a unique ID, a user-defined name, a @@ -421,4 +422,19 @@ public String toString() { return getDescription(); } + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || !(obj instanceof Player)) { + return false; + } + Player other = (Player) obj; + return Objects.equals(id, other.id); + } + } diff --git a/airsonic-main/src/main/java/org/airsonic/player/domain/Playlist.java b/airsonic-main/src/main/java/org/airsonic/player/domain/Playlist.java index f43bb30cf..1432f9ff5 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/domain/Playlist.java +++ b/airsonic-main/src/main/java/org/airsonic/player/domain/Playlist.java @@ -26,6 +26,7 @@ import java.time.Instant; import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * @author Sindre Mehus @@ -211,4 +212,24 @@ public void setMediaFiles(List mediaFiles) { } } + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public boolean equals(Object o) { + + if (this == o) { + return true; + } + + if (o == null || !(o instanceof Playlist)) { + return false; + } + + Playlist p = (Playlist) o; + return Objects.equals(id, p.id); + } + } diff --git a/airsonic-main/src/main/java/org/airsonic/player/domain/PodcastChannel.java b/airsonic-main/src/main/java/org/airsonic/player/domain/PodcastChannel.java index ea8e1c4e8..e3a1fa344 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/domain/PodcastChannel.java +++ b/airsonic-main/src/main/java/org/airsonic/player/domain/PodcastChannel.java @@ -26,6 +26,7 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * A Podcast channel. Each channel contain several episodes. @@ -153,4 +154,24 @@ public MediaFile getMediaFile() { return mediaFile; } + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public boolean equals(Object other) { + + if (this == other) { + return true; + } + + if (other == null || !(other instanceof PodcastChannel)) { + return false; + } + + PodcastChannel otherChannel = (PodcastChannel) other; + return Objects.equals(id, otherChannel.id); + } + } \ No newline at end of file diff --git a/airsonic-main/src/main/java/org/airsonic/player/domain/PodcastChannelRule.java b/airsonic-main/src/main/java/org/airsonic/player/domain/PodcastChannelRule.java index b73312b73..15d4c33a5 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/domain/PodcastChannelRule.java +++ b/airsonic-main/src/main/java/org/airsonic/player/domain/PodcastChannelRule.java @@ -1,8 +1,9 @@ package org.airsonic.player.domain; - import javax.persistence.*; +import java.util.Objects; + @Entity @Table(name = "podcast_channel_rules") public class PodcastChannelRule { @@ -69,4 +70,24 @@ public void setDownloadCount(Integer downloadCount) { this.downloadCount = downloadCount; } + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public boolean equals(Object other) { + + if (this == other) { + return true; + } + + if (other == null || !(other instanceof PodcastChannelRule)) { + return false; + } + + PodcastChannelRule otherRule = (PodcastChannelRule) other; + return Objects.equals(id, otherRule.id); + } + } diff --git a/airsonic-main/src/main/java/org/airsonic/player/domain/PodcastEpisode.java b/airsonic-main/src/main/java/org/airsonic/player/domain/PodcastEpisode.java index 3568e5e5e..9e7bd8672 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/domain/PodcastEpisode.java +++ b/airsonic-main/src/main/java/org/airsonic/player/domain/PodcastEpisode.java @@ -22,6 +22,7 @@ import javax.persistence.*; import java.time.Instant; +import java.util.Objects; /** * A Podcast episode belonging to a channel. @@ -211,4 +212,20 @@ public void setEpisodeGuid(String episodeGuid) { this.episodeGuid = episodeGuid; } + @Override + public int hashCode() { + return Objects.hash(url); + } + + @Override + public boolean equals(Object obj) { + + if (obj == null || !(obj instanceof PodcastEpisode)) { + return false; + } + + PodcastEpisode other = (PodcastEpisode) obj; + return Objects.equals(url, other.url); + } + } diff --git a/airsonic-main/src/main/java/org/airsonic/player/domain/SavedPlayQueue.java b/airsonic-main/src/main/java/org/airsonic/player/domain/SavedPlayQueue.java index e7736f3f7..7bbfc6beb 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/domain/SavedPlayQueue.java +++ b/airsonic-main/src/main/java/org/airsonic/player/domain/SavedPlayQueue.java @@ -21,6 +21,7 @@ import java.time.Instant; import java.util.List; +import java.util.Objects; /** * Used to save the play queue state for a user. @@ -107,4 +108,24 @@ public String getChangedBy() { public void setChangedBy(String changedBy) { this.changedBy = changedBy; } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public boolean equals(Object other) { + + if (this == other) { + return true; + } + + if (other == null || !(other instanceof SavedPlayQueue)) { + return false; + } + + SavedPlayQueue otherQueue = (SavedPlayQueue) other; + return Objects.equals(id, otherQueue.id); + } } diff --git a/airsonic-main/src/main/java/org/airsonic/player/repository/MediaFileRepository.java b/airsonic-main/src/main/java/org/airsonic/player/repository/MediaFileRepository.java index b7f8e70dd..5dc34fa9a 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/repository/MediaFileRepository.java +++ b/airsonic-main/src/main/java/org/airsonic/player/repository/MediaFileRepository.java @@ -23,7 +23,11 @@ import org.airsonic.player.domain.MusicFolder; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @@ -90,9 +94,22 @@ public List findByAlbumArtistAndAlbumNameAndMediaTypeInAndPresentTrue public int countByFolderInAndMediaTypeAndPlayCountGreaterThanAndPresentTrue(List folders, MediaType mediaType, Integer playCount); + public List findAll(Specification spec, Pageable page); + @Transactional public void deleteAllByPresentFalse(); public List findByFolderInAndMediaTypeInAndPresentTrue(List folders, Iterable playableTypes, Pageable offsetBasedPageRequest); + + @Modifying + @Transactional + @Query("UPDATE MediaFile m SET m.present = true, m.lastScanned = :lastScanned WHERE m.folder = :folder AND m.path IN :paths") + public int markPresent(@Param("folder") MusicFolder folder, @Param("paths") Iterable paths, @Param("lastScanned") Instant lastScanned); + + @Modifying + @Transactional + @Query("UPDATE MediaFile m SET m.present = false, m.childrenLastUpdated = :childrenLastUpdated WHERE m.lastScanned < :lastScanned") + public void markNonPresent(@Param("childrenLastUpdated") Instant childrenLastUpdated, @Param("lastScanned") Instant lastScanned); + } diff --git a/airsonic-main/src/main/java/org/airsonic/player/repository/MediaFileSpecifications.java b/airsonic-main/src/main/java/org/airsonic/player/repository/MediaFileSpecifications.java new file mode 100644 index 000000000..151de52a7 --- /dev/null +++ b/airsonic-main/src/main/java/org/airsonic/player/repository/MediaFileSpecifications.java @@ -0,0 +1,132 @@ +package org.airsonic.player.repository; + +import org.airsonic.player.domain.MediaFile; +import org.airsonic.player.domain.MediaFile.MediaType; +import org.airsonic.player.domain.RandomSearchCriteria; +import org.airsonic.player.domain.entity.StarredMediaFile; +import org.airsonic.player.domain.entity.UserRating; +import org.springframework.data.jpa.domain.Specification; + +import javax.persistence.criteria.*; + +import java.util.ArrayList; +import java.util.List; + +public class MediaFileSpecifications { + + public static Specification matchCriteria(RandomSearchCriteria criteria, String username, String databaseType) { + return (Root root, CriteriaQuery query, CriteriaBuilder cb) -> { + List predicates = new ArrayList<>(); + + // base conditions + predicates.add(cb.isTrue(root.get("present"))); + predicates.add(cb.equal(root.get("mediaType"), MediaType.MUSIC)); + predicates.add(cb.isNull(root.get("indexPath"))); // exclude indexed files + + // starred conditions + boolean joinStarred = criteria.isShowStarredSongs() ^ criteria.isShowUnstarredSongs(); + if (joinStarred) { + Subquery subquery = query.subquery(StarredMediaFile.class); + Root starredRoot = subquery.from(StarredMediaFile.class); + subquery.select(starredRoot); + + Predicate userPredicate = cb.equal(starredRoot.get("username"), username); + Predicate mediaFilePredicate = cb.equal(starredRoot.get("mediaFile"), root); + + if (criteria.isShowStarredSongs()) { + subquery.where(cb.and(userPredicate, mediaFilePredicate)); + predicates.add(cb.exists(subquery)); + } else if (criteria.isShowUnstarredSongs()) { + subquery.where(cb.and(userPredicate, mediaFilePredicate)); + predicates.add(cb.not(cb.exists(subquery))); + } + } + // album rating conditions + boolean joinAlbumRating = criteria.getMinAlbumRating() != null || criteria.getMaxAlbumRating() != null; + if (joinAlbumRating) { + Subquery albumSubquery = query.subquery(String.class); + Root ratingRoot = albumSubquery.from(UserRating.class); + Root albumRoot = albumSubquery.from(MediaFile.class); + albumSubquery.select(albumRoot.get("path")); + + List ratingPredicates = new ArrayList<>(); + ratingPredicates.add(cb.equal(ratingRoot.get("username"), username)); + ratingPredicates.add(cb.equal(albumRoot.get("mediaType"), MediaType.ALBUM)); + ratingPredicates.add(cb.equal(ratingRoot.get("mediaFileId"), albumRoot.get("id"))); + + if (criteria.getMinAlbumRating() != null) { + ratingPredicates.add(cb.greaterThanOrEqualTo(ratingRoot.get("rating"), criteria.getMinAlbumRating())); + } + if (criteria.getMaxAlbumRating() != null) { + ratingPredicates.add(cb.lessThanOrEqualTo(ratingRoot.get("rating"), criteria.getMaxAlbumRating())); + } + + albumSubquery.where(cb.and(ratingPredicates.toArray(new Predicate[0]))); + + predicates.add(cb.in(root.get("parentPath")).value(albumSubquery)); + } + + // folder conditions + if (!criteria.getMusicFolders().isEmpty()) { + predicates.add(root.get("folder").in(criteria.getMusicFolders())); + } + + // genre conditions + if (criteria.getGenre() != null) { + predicates.add(cb.equal(root.get("genre"), criteria.getGenre())); + } + // year conditions + if (criteria.getFromYear() != null) { + predicates.add(cb.greaterThanOrEqualTo(root.get("year"), criteria.getFromYear())); + } + if (criteria.getToYear() != null) { + predicates.add(cb.lessThanOrEqualTo(root.get("year"), criteria.getToYear())); + } + // format conditions + if (criteria.getFormat() != null) { + predicates.add(cb.equal(root.get("format"), criteria.getFormat())); + } + // last played conditions + if (criteria.getMinLastPlayedDate() != null) { + predicates.add(cb.greaterThanOrEqualTo(root.get("lastPlayed"), criteria.getMinLastPlayedDate())); + } + if (criteria.getMaxLastPlayedDate() != null) { + if (criteria.getMinLastPlayedDate() == null) { + predicates.add(cb.or(cb.isNull(root.get("lastPlayed")), + cb.lessThanOrEqualTo(root.get("lastPlayed"), criteria.getMaxLastPlayedDate()))); + } else { + predicates.add(cb.lessThanOrEqualTo(root.get("lastPlayed"), criteria.getMaxLastPlayedDate())); + } + } + + // play count conditions + if (criteria.getMinPlayCount() != null) { + predicates.add(cb.greaterThanOrEqualTo(root.get("playCount"), criteria.getMinPlayCount())); + } + + if (criteria.getMaxPlayCount() != null) { + if (criteria.getMinPlayCount() == null) { + predicates.add(cb.or(cb.isNull(root.get("playCount")), + cb.lessThanOrEqualTo(root.get("playCount"), criteria.getMaxPlayCount()))); + } else { + predicates.add(cb.lessThanOrEqualTo(root.get("playCount"), criteria.getMaxPlayCount())); + } + } + String randomFunctionName; + switch (databaseType.toLowerCase()) { + case "postgresql": + randomFunctionName = "RANDOM"; + break; + case "mysql": + case "mariadb": + case "hsqldb": + default: + randomFunctionName = "RAND"; + break; + } + Expression randomFunction = cb.function(randomFunctionName, Double.class); + query.orderBy(cb.asc(randomFunction)); + return cb.and(predicates.toArray(new Predicate[0])); + }; + } +} diff --git a/airsonic-main/src/main/java/org/airsonic/player/repository/PodcastEpisodeRepository.java b/airsonic-main/src/main/java/org/airsonic/player/repository/PodcastEpisodeRepository.java index a66496bbc..faa42e962 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/repository/PodcastEpisodeRepository.java +++ b/airsonic-main/src/main/java/org/airsonic/player/repository/PodcastEpisodeRepository.java @@ -23,7 +23,9 @@ public Optional findByChannelAndTitleAndPublishDate(PodcastChann public List findByChannel(PodcastChannel channel); + public List findByChannelAndStatus(PodcastChannel channel, PodcastStatus status); + public List findByStatusAndPublishDateNotNullAndMediaFilePresentTrueOrderByPublishDateDesc( - PodcastStatus status); + PodcastStatus status); } diff --git a/airsonic-main/src/main/java/org/airsonic/player/repository/RandomMediaFileRepository.java b/airsonic-main/src/main/java/org/airsonic/player/repository/RandomMediaFileRepository.java deleted file mode 100644 index d3c7825a0..000000000 --- a/airsonic-main/src/main/java/org/airsonic/player/repository/RandomMediaFileRepository.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - This file is part of Airsonic. - - Airsonic is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Airsonic is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Airsonic. If not, see . - - Copyright 2023 (C) Y.Tory - */ -package org.airsonic.player.repository; - -import org.airsonic.player.domain.MediaFile; -import org.airsonic.player.domain.RandomSearchCriteria; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Repository; - -import javax.persistence.*; -import javax.persistence.criteria.*; - -import java.util.ArrayList; -import java.util.List; - -@Repository -public class RandomMediaFileRepository { - - @Autowired - private EntityManager entityManager; - - public List getRandomMediaFiles(String username, RandomSearchCriteria criteria, List mediaFileIds) { - - CriteriaBuilder cb = entityManager.getCriteriaBuilder(); - CriteriaQuery query = cb.createQuery(MediaFile.class); - Root mediaFile = query.from(MediaFile.class); - - List predicates = new ArrayList<>(); - - predicates.add(cb.isNull(mediaFile.get("indexPath"))); - predicates.add(cb.in(mediaFile.get("id")).value(mediaFileIds)); - - addConditionalPredicates(criteria, cb, mediaFile, predicates); - - query.where(predicates.toArray(new Predicate[predicates.size()])); - query.select(mediaFile); - - TypedQuery typedQuery = entityManager.createQuery(query); - - return typedQuery.getResultList(); - } - - private void addConditionalPredicates(RandomSearchCriteria criteria, CriteriaBuilder cb, Root mediaFile, - List predicates) { - - if (criteria.getGenre() != null) { - predicates.add(cb.equal(mediaFile.get("genre"), criteria.getGenre())); - } - - if (criteria.getFormat() != null) { - predicates.add(cb.equal(mediaFile.get("format"), criteria.getFormat())); - } - - if (criteria.getFromYear() != null) { - predicates.add(cb.greaterThanOrEqualTo(mediaFile.get("year"), criteria.getFromYear())); - } - - if (criteria.getToYear() != null) { - predicates.add(cb.lessThanOrEqualTo(mediaFile.get("year"), criteria.getToYear())); - } - - if (criteria.getMinLastPlayedDate() != null) { - predicates.add(cb.greaterThanOrEqualTo(mediaFile.get("lastPlayed"), criteria.getMinLastPlayedDate())); - } - - if (criteria.getMaxLastPlayedDate() != null) { - if (criteria.getMinLastPlayedDate() == null) { - predicates.add(cb.or(cb.isNull(mediaFile.get("lastPlayed")), - cb.lessThanOrEqualTo(mediaFile.get("lastPlayed"), criteria.getMaxLastPlayedDate()))); - } else { - predicates.add(cb.lessThanOrEqualTo(mediaFile.get("lastPlayed"), criteria.getMaxLastPlayedDate())); - } - } - - if (criteria.getMinPlayCount() != null) { - predicates.add(cb.greaterThanOrEqualTo(mediaFile.get("playCount"), criteria.getMinPlayCount())); - } - - if (criteria.getMaxPlayCount() != null) { - if (criteria.getMinPlayCount() == null) { - predicates.add(cb.or(cb.isNull(mediaFile.get("playCount")), - cb.lessThanOrEqualTo(mediaFile.get("playCount"), criteria.getMaxPlayCount()))); - } else { - predicates.add(cb.lessThanOrEqualTo(mediaFile.get("playCount"), criteria.getMaxPlayCount())); - } - } - } - -} diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/AlbumService.java b/airsonic-main/src/main/java/org/airsonic/player/service/AlbumService.java index 404fb6670..2cbd4ec58 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/AlbumService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/AlbumService.java @@ -17,9 +17,9 @@ import java.time.Instant; import java.util.Collections; import java.util.List; +import java.util.Optional; @Service -@Transactional public class AlbumService { private final AlbumRepository albumRepository; @@ -40,6 +40,20 @@ public Album getAlbum(int albumId) { return albumRepository.findById(albumId).orElse(null); } + /** + * Get album by artist and name + * + * @param artist artist name to get album for + * @param name album name to get album for + * @return album or null if not found + */ + public Optional getAlbumByArtistAndName(String artist, String name) { + if (!StringUtils.hasLength(artist) || !StringUtils.hasLength(name)) { + return Optional.ofNullable(null); + } + return albumRepository.findByArtistAndName(artist, name); + } + /** * Get album by artist and name * @@ -234,6 +248,7 @@ public List getAlbumsByYear(int offset, int count, int startYear, int end * @param star true to star, false to unstar * @return true if success, false if otherwise */ + @Transactional public boolean starOrUnstar(int albumId, String username, boolean star) { if (!StringUtils.hasLength(username)) { return false; @@ -260,6 +275,7 @@ public boolean starOrUnstar(int albumId, String username, boolean star) { * @param username username to get star date for * @return date of album star or null if not starred */ + @Transactional public Instant getAlbumStarredDate(Integer albumId, String username) { if (albumId == null || !StringUtils.hasLength(username)) { return null; @@ -276,6 +292,7 @@ public Instant getAlbumStarredDate(Integer albumId, String username) { * @param musicFolders music folders to search in * @return list of starred albums */ + @Transactional public List getStarredAlbums(String username, List musicFolders) { if (!StringUtils.hasLength(username) || CollectionUtils.isEmpty(musicFolders)) { return Collections.emptyList(); @@ -295,6 +312,7 @@ public List getStarredAlbums(String username, List musicFold * @param musicFolders music folders to search in * @return list of starred albums */ + @Transactional public List getStarredAlbums(int offset, int size, String username, List musicFolders) { if (!StringUtils.hasLength(username) || CollectionUtils.isEmpty(musicFolders)) { return Collections.emptyList(); @@ -310,6 +328,7 @@ public List getStarredAlbums(int offset, int size, String username, List< /** * delete all albums that are not present */ + @Transactional public void expunge() { albumRepository.deleteAllByPresentFalse(); } @@ -341,6 +360,7 @@ public int getAlbumCount(List musicFolders) { * * @param lastScanned last scanned date */ + @Transactional public void markNonPresent(Instant lastScanned) { albumRepository.markNonPresent(lastScanned); } @@ -350,6 +370,7 @@ public void markNonPresent(Instant lastScanned) { * * @param album album to save */ + @Transactional public Album save(Album album) { return albumRepository.save(album); } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/ArtistService.java b/airsonic-main/src/main/java/org/airsonic/player/service/ArtistService.java index dec00fdc8..9571ab8c1 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/ArtistService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/ArtistService.java @@ -37,7 +37,6 @@ import java.util.List; @Service -@Transactional public class ArtistService { private final ArtistRepository artistRepository; @@ -124,6 +123,7 @@ public List getAlphabeticalArtists(final List musicFolders) * @param musicFolders music folders. If null or empty, return empty list * @return list of starred artists or empty list. Sorted by starred date descending. */ + @Transactional public List getStarredArtists(String username, List musicFolders) { if (CollectionUtils.isEmpty(musicFolders) || !StringUtils.hasLength(username)) { LOG.warn("getStarredArtists: musicFolders or username is null"); @@ -140,6 +140,7 @@ public List getStarredArtists(String username, List musicFo * @param star true to star, false to unstar * @return true if success, false otherwise */ + @Transactional public boolean starOrUnstar(Integer artistId, String username, boolean star) { if (artistId == null || !StringUtils.hasLength(username)) { LOG.warn("star: artistId or username is null"); @@ -162,6 +163,7 @@ public boolean starOrUnstar(Integer artistId, String username, boolean star) { * @param username username to check. If null or empty, return null * @return starred date or null */ + @Transactional public Instant getStarredDate(Integer artistId, String username) { if (artistId == null || !StringUtils.hasLength(username)) { LOG.warn("getStarredDate: artistId or username is null"); @@ -173,6 +175,7 @@ public Instant getStarredDate(Integer artistId, String username) { /** * Expunge artists that are not present */ + @Transactional public void expunge() { artistRepository.deleteAllByPresentFalse(); } @@ -182,6 +185,7 @@ public void expunge() { * * @param lastScanned last scanned date */ + @Transactional public void markNonPresent(Instant lastScanned) { artistRepository.markNonPresent(lastScanned); } @@ -192,6 +196,7 @@ public void markNonPresent(Instant lastScanned) { * @param artist artist to save * @return saved artist */ + @Transactional public Artist save(Artist artist) { return artistRepository.save(artist); } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/BookmarkService.java b/airsonic-main/src/main/java/org/airsonic/player/service/BookmarkService.java index 81c351a0b..52cd5b333 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/BookmarkService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/BookmarkService.java @@ -29,7 +29,6 @@ public BookmarkService(BookmarkRepository repository, MediaFileService mediaFile this.brokerTemplate = brokerTemplate; } - @Transactional public Optional getBookmark(String username, int mediaFileId) { return repository.findOptByUsernameAndMediaFileId(username, mediaFileId); } @@ -61,7 +60,7 @@ public boolean setBookmark(String username, int mediaFileId, long positionMillis bookmark.setComment(comment); bookmark.setPositionMillis(positionMillis); try { - repository.saveAndFlush(bookmark); + repository.save(bookmark); } catch (DataIntegrityViolationException e) { LOG.debug("duplicate registeration happend"); return false; @@ -78,7 +77,6 @@ public void deleteBookmark(String username, int mediaFileId) { brokerTemplate.convertAndSendToUser(username, "/queue/bookmarks/deleted", mediaFileId); } - @Transactional public List getBookmarks(String username) { return repository.findByUsername(username); } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/CoverArtService.java b/airsonic-main/src/main/java/org/airsonic/player/service/CoverArtService.java index 44a3f3068..644da30c6 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/CoverArtService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/CoverArtService.java @@ -33,7 +33,6 @@ @Service @CacheConfig(cacheNames = "coverArtCache") -@Transactional public class CoverArtService { @Autowired MediaFolderService mediaFolderService; @@ -47,6 +46,7 @@ public class CoverArtService { private static final Logger LOG = LoggerFactory.getLogger(CoverArtService.class); @CacheEvict(key = "#art.entityType.toString().concat('-').concat(#art.entityId)") + @Transactional public void upsert(CoverArt art) { coverArtRepository.save(art); } @@ -111,11 +111,13 @@ public Path getFullPath(CoverArt art) { } @CacheEvict(key = "#type.toString().concat('-').concat(#id)") + @Transactional public void delete(EntityType type, int id) { coverArtRepository.deleteByEntityTypeAndEntityId(type, id); } @CacheEvict(allEntries = true) + @Transactional public void expunge() { List expungeCoverArts = coverArtRepository.findAll().stream() .filter(art -> diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/LastFmService.java b/airsonic-main/src/main/java/org/airsonic/player/service/LastFmService.java index 4dae35f2a..c3603c0c1 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/LastFmService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/LastFmService.java @@ -29,7 +29,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import java.nio.file.Path; import java.util.*; @@ -44,7 +43,6 @@ * @version $Id$ */ @Service -@Transactional(readOnly = true) public class LastFmService { private static final String LAST_FM_KEY = "ece4499898a9440896dfdce5dab26bbf"; 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 e6f2f14b5..f66a72a59 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 @@ -28,25 +28,20 @@ import org.airsonic.player.domain.MediaFile.MediaType; import org.airsonic.player.domain.MusicFolder.Type; import org.airsonic.player.domain.entity.StarredMediaFile; -import org.airsonic.player.domain.entity.UserRating; import org.airsonic.player.i18n.LocaleResolver; import org.airsonic.player.repository.AlbumRepository; -import org.airsonic.player.repository.ArtistRepository; import org.airsonic.player.repository.GenreRepository; import org.airsonic.player.repository.MediaFileRepository; +import org.airsonic.player.repository.MediaFileSpecifications; import org.airsonic.player.repository.MusicFileInfoRepository; import org.airsonic.player.repository.OffsetBasedPageRequest; -import org.airsonic.player.repository.RandomMediaFileRepository; import org.airsonic.player.repository.StarredMediaFileRepository; -import org.airsonic.player.repository.UserRatingRepository; import org.airsonic.player.service.metadata.JaudiotaggerParser; import org.airsonic.player.service.metadata.MetaData; import org.airsonic.player.service.metadata.MetaDataParser; import org.airsonic.player.service.metadata.MetaDataParserFactory; -import org.airsonic.player.service.search.IndexManager; import org.airsonic.player.util.FileUtil; import org.apache.commons.io.FilenameUtils; -import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.digitalmediaserver.cuelib.CueParser; @@ -60,7 +55,7 @@ import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Caching; -import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.stereotype.Service; @@ -79,8 +74,6 @@ import java.time.temporal.ChronoUnit; import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -92,7 +85,6 @@ * @author Sindre Mehus */ @Service -@Transactional public class MediaFileService { private static final Logger LOG = LoggerFactory.getLogger(MediaFileService.class); @@ -104,10 +96,6 @@ public class MediaFileService { @Autowired private MediaFolderService mediaFolderService; @Autowired - private RandomMediaFileRepository randomMediaFileRepository; - @Autowired - private UserRatingRepository userRatingRepository; - @Autowired private AlbumRepository albumRepository; @Autowired private JaudiotaggerParser parser; @@ -125,10 +113,6 @@ public class MediaFileService { private MusicFileInfoRepository musicFileInfoRepository; @Autowired private GenreRepository genreRepository; - @Autowired - private IndexManager indexManager; - @Autowired - private ArtistRepository artistRepository; private boolean memoryCacheEnabled = true; @@ -142,19 +126,16 @@ public MediaFile getMediaFile(Path fullPath) { // This may be an expensive op public MediaFile getMediaFile(Path fullPath, boolean minimizeDiskAccess) { - if (Objects.isNull(fullPath)) return null; - MusicFolder folder = mediaFolderService.getMusicFolderForFile(fullPath, true, true); - if (folder == null) { - // can't look outside folders and not present in folder - return null; - } - try { - Path relativePath = folder.getPath().relativize(fullPath); - return getMediaFile(relativePath, folder, minimizeDiskAccess); - } catch (Exception e) { - // ignore - return null; - } + return mediaFolderService.getMusicFolderForFile(fullPath, true, true) + .map(folder -> { + try { + Path relativePath = folder.getPath().relativize(fullPath); + return getMediaFile(relativePath, folder, minimizeDiskAccess); + } catch (Exception e) { + // ignore + return null; + } + }).orElse(null); } public MediaFile getMediaFile(String relativePath, Integer folderId) { @@ -416,6 +397,7 @@ public MediaFile getSongByArtistAndTitle(String artist, String title, List getStarredSongs(int offset, int count, String username, List musicFolders) { if (CollectionUtils.isEmpty(musicFolders)) { return Collections.emptyList(); @@ -458,6 +440,7 @@ public MediaFile getArtistByName(String artist, List folders) { * @param musicFolders Only return artists from these folders. * @return The most recently starred artists for this user. */ + @Transactional public List getStarredArtists(int offset, int count, String username, List musicFolders) { if (CollectionUtils.isEmpty(musicFolders)) { return Collections.emptyList(); @@ -514,6 +497,7 @@ public List getGenres(boolean sortByAlbum) { * @param genres The genres to update. * @return The updated genres. */ + @Transactional public List updateGenres(List genres) { return genreRepository.saveAll(genres); } @@ -526,6 +510,7 @@ public List updateGenres(List genres) { * @param musicFolders Only return albums in these folders. * @return The most frequently played albums. */ + @Transactional(isolation = Isolation.READ_COMMITTED) public List getMostFrequentlyPlayedAlbums(int offset, int count, List musicFolders) { if (CollectionUtils.isEmpty(musicFolders)) { return Collections.emptyList(); @@ -541,6 +526,7 @@ public List getMostFrequentlyPlayedAlbums(int offset, int count, List * @param musicFolders Only return albums in these folders. * @return The most recently played albums. */ + @Transactional(isolation = Isolation.READ_COMMITTED) public List getMostRecentlyPlayedAlbums(int offset, int count, List musicFolders) { if (CollectionUtils.isEmpty(musicFolders)) { return Collections.emptyList(); @@ -556,6 +542,7 @@ public List getMostRecentlyPlayedAlbums(int offset, int count, List getNewestAlbums(int offset, int count, List musicFolders) { if (CollectionUtils.isEmpty(musicFolders)) { return Collections.emptyList(); @@ -572,6 +559,7 @@ public List getNewestAlbums(int offset, int count, List * @param musicFolders Only return albums from these folders. * @return The most recently starred albums for this user. */ + @Transactional(isolation = Isolation.READ_COMMITTED) public List getStarredAlbums(int offset, int count, String username, List musicFolders) { if (CollectionUtils.isEmpty(musicFolders)) { return Collections.emptyList(); @@ -592,6 +580,7 @@ public List getStarredAlbums(int offset, int count, String username, * @param musicFolders Only return albums in these folders. * @return Albums in alphabetical order. */ + @Transactional(isolation = Isolation.READ_COMMITTED) public List getAlphabeticalAlbums(int offset, int count, boolean byArtist, List musicFolders) { if (CollectionUtils.isEmpty(musicFolders)) { return Collections.emptyList(); @@ -610,6 +599,7 @@ public List getAlphabeticalAlbums(int offset, int count, boolean byAr * @param musicFolders Only return albums in these folders. * @return Albums in the year range. */ + @Transactional(isolation = Isolation.READ_COMMITTED) public List getAlbumsByYear(int offset, int count, int fromYear, int toYear, List musicFolders) { if (CollectionUtils.isEmpty(musicFolders)) { @@ -632,6 +622,7 @@ public List getAlbumsByYear(int offset, int count, int fromYear, int * @param musicFolders Only return albums in these folders. * @return Albums in the genre. */ + @Transactional(isolation = Isolation.READ_COMMITTED) public List getAlbumsByGenre(int offset, int count, String genre, List musicFolders) { if (CollectionUtils.isEmpty(musicFolders)) { return Collections.emptyList(); @@ -647,6 +638,7 @@ public List getAlbumsByGenre(int offset, int count, String genre, Lis * @param count Max number of songs to return. * @return Random songs. */ + @Transactional(isolation = Isolation.READ_COMMITTED) public List getRandomSongsForParent(MediaFile parent, int count) { List children = getDescendantsOf(parent, false); removeVideoFiles(children); @@ -662,42 +654,12 @@ public List getRandomSongsForParent(MediaFile parent, int count) { * Returns random songs matching search criteria. * */ + @Transactional(isolation = Isolation.READ_COMMITTED) public List getRandomSongs(RandomSearchCriteria criteria, String username) { if (criteria == null || CollectionUtils.isEmpty(criteria.getMusicFolders())) { return Collections.emptyList(); } - boolean joinAlbumRating = criteria.getMinAlbumRating() != null || criteria.getMaxAlbumRating() != null; - boolean joinStarred = criteria.isShowStarredSongs() ^ criteria.isShowUnstarredSongs(); - - List starredFileIds = new ArrayList<>(); - List fileIds = new ArrayList<>(); - - if (joinAlbumRating) { - Integer minAlbumRating = criteria.getMinAlbumRating() == null ? 0 : criteria.getMinAlbumRating(); - Integer maxAlbumRating = criteria.getMaxAlbumRating() == null ? 5 : criteria.getMaxAlbumRating(); - List ratedIds = userRatingRepository.findByUsernameAndRatingBetween(username, minAlbumRating, maxAlbumRating).stream().map(UserRating::getMediaFileId).collect(Collectors.toList()); - List ratedAlbums = mediaFileRepository.findByIdInAndFolderInAndMediaTypeAndPresentTrue(ratedIds, criteria.getMusicFolders(), MediaType.ALBUM); - fileIds = ratedAlbums.stream().flatMap(ra -> { - return mediaFileRepository.findByAlbumArtistAndAlbumNameAndMediaTypeInAndPresentTrue(ra.getArtist(), ra.getAlbumName(), List.of(MediaType.MUSIC), Sort.by("id")).stream().map(MediaFile::getId); - }).collect(Collectors.toList()); - } else { - fileIds = mediaFileRepository.findByFolderInAndMediaTypeAndPresentTrue(criteria.getMusicFolders(), MediaType.MUSIC, PageRequest.of(0, Integer.MAX_VALUE)).stream().map(MediaFile::getId).collect(Collectors.toList()); - } - if (joinStarred) { - starredFileIds = starredMediaFileRepository.findByUsername(username).stream().map(StarredMediaFile::getMediaFile).filter(Objects::nonNull).map(MediaFile::getId).collect(Collectors.toList()); - if (criteria.isShowStarredSongs()) { - fileIds.retainAll(starredFileIds); - } else { - fileIds.removeAll(starredFileIds); - } - } - List files = randomMediaFileRepository.getRandomMediaFiles(username, criteria, fileIds); - Collections.shuffle(files); - - if (files.size() <= criteria.getCount()) { - return files; - } - return files.subList(0, criteria.getCount()); + return mediaFileRepository.findAll(MediaFileSpecifications.matchCriteria(criteria, username, settingsService.getDatabaseType()), Pageable.ofSize(criteria.getCount())); } /** @@ -742,8 +704,7 @@ private List updateChildren(MediaFile parent) { try (Stream children = Files.list(parent.getFullPath())) { children.parallel() .filter(x -> { - String ext = FilenameUtils.getExtension(x.toString()); - return "cue".equalsIgnoreCase(ext) || "flac".equalsIgnoreCase(ext); + return "cue".equalsIgnoreCase(FilenameUtils.getExtension(x.toString())) || "flac".equalsIgnoreCase(FilenameUtils.getExtension(x.toString())); }) .forEach(x -> { CueSheet cueSheet = getCueSheet(x); @@ -761,7 +722,7 @@ private List updateChildren(MediaFile parent) { try (Stream children = Files.list(parent.getFullPath())) { Map bareFiles = children.parallel() .filter(this::includeMediaFileByPath) - .filter(x -> mediaFolderService.getMusicFolderForFile(x, true, true).getId().equals(folder.getId())) + .filter(x -> mediaFolderService.getMusicFolderForFile(x, true, true).map(f -> f.equals(folder)).orElse(false)) .map(x -> folder.getPath().relativize(x)) .map(x -> { MediaFile media = storedChildrenMap.remove(Pair.of(x.toString(), MediaFile.NOT_INDEXED)); @@ -772,7 +733,9 @@ private List updateChildren(MediaFile parent) { updateMediaFile(media); } } else { - media = checkLastModified(media, false); // has to be false, only time it's called + if (!media.hasIndex()) { + media = checkLastModified(media, false); // has to be false, only time it's called + } } return media; }) @@ -845,11 +808,11 @@ public boolean showMediaFile(MediaFile media) { !(settingsService.getHideIndexedFiles() && media.hasIndex()); } - public boolean includeMediaFile(MediaFile candidate) { + private boolean includeMediaFile(MediaFile candidate) { return includeMediaFileByPath(candidate.getFullPath()); } - public boolean includeMediaFileByPath(Path candidate) { + private boolean includeMediaFileByPath(Path candidate) { String suffix = FilenameUtils.getExtension(candidate.toString()).toLowerCase(); return (!isExcluded(candidate) && (Files.isDirectory(candidate) || isAudioFile(suffix) || isVideoFile(suffix))); } @@ -904,7 +867,7 @@ private MediaFile createMediaFileByFile(Path relativePath, MusicFolder folder) { * @param mediaFile media file to reflect. Must not be null. path must be set. * @return media file reflected from file system */ - public MediaFile updateMediaFileByFile(MediaFile mediaFile) { + private MediaFile updateMediaFileByFile(MediaFile mediaFile) { return updateMediaFileByFile(mediaFile, false); } @@ -932,7 +895,7 @@ private MediaFile updateMediaFileByFile(MediaFile mediaFile, boolean isCheckedEx } //sanity check - MusicFolder folderActual = mediaFolderService.getMusicFolderForFile(file, true, true); + MusicFolder folderActual = mediaFolderService.getMusicFolderForFile(file, true, true).orElse(mediaFile.getFolder()); if (!folderActual.getId().equals(mediaFile.getFolder().getId())) { LOG.warn("Inconsistent Mediafile folder for media file with path: {}, folder id should be {} and is instead {}", file, folderActual.getId(), mediaFile.getFolder().getId()); } @@ -1186,6 +1149,7 @@ private MediaFile.MediaType getMediaType(MediaFile mediaFile) { return MediaFile.MediaType.MUSIC; } + @Transactional public void refreshMediaFile(MediaFile mediaFile) { mediaFile = updateMediaFileByFile(mediaFile); updateMediaFile(mediaFile); @@ -1339,11 +1303,11 @@ public void updateMediaFile(MediaFile mediaFile) { if (mediaFile == null) { throw new IllegalArgumentException("mediaFile must not be null"); } else if (mediaFile.getId() != null && mediaFileRepository.existsById(mediaFile.getId())) { - mediaFileRepository.saveAndFlush(mediaFile); + mediaFileRepository.save(mediaFile); } else { mediaFileRepository.findByPathAndFolderAndStartPosition(mediaFile.getPath(), mediaFile.getFolder(), mediaFile.getStartPosition()).ifPresentOrElse(m -> { mediaFile.setId(m.getId()); - mediaFileRepository.saveAndFlush(mediaFile); + mediaFileRepository.save(mediaFile); }, () -> { MusicFolder folder = mediaFile.getFolder(); if (folder != null) { @@ -1353,7 +1317,7 @@ public void updateMediaFile(MediaFile mediaFile) { mediaFile.setPlayCount(musicFileInfo.getPlayCount()); }); } - mediaFileRepository.saveAndFlush(mediaFile); + mediaFileRepository.save(mediaFile); }); } @@ -1365,6 +1329,7 @@ public void updateMediaFile(MediaFile mediaFile) { * Increments the play count and last played date for the given media file and its * directory and album. */ + @Transactional public void incrementPlayCount(MediaFile file) { Instant now = Instant.now(); file.setLastPlayed(now); @@ -1431,6 +1396,7 @@ public int getStarredAlbumCount(String username, List musicFolders) * @param ids media file ids to star * @param username username who stars the media files */ + @Transactional public void starMediaFiles(List ids, String username) { if (CollectionUtils.isEmpty(ids) || StringUtils.isEmpty(username)) { return; @@ -1453,6 +1419,7 @@ public void starMediaFiles(List ids, String username) { * @param ids media file ids to unstar * @param username username who unstars the media files */ + @Transactional public void unstarMediaFiles(List ids, String username) { if (CollectionUtils.isEmpty(ids) || StringUtils.isEmpty(username)) { return; @@ -1467,6 +1434,7 @@ public void unstarMediaFiles(List ids, String username) { * @param lastScanned last scanned time * @return true if success, false otherwise */ + @Transactional public boolean markPresent(Map> paths, Instant lastScanned) { final int BATCH_SIZE = 30000; @@ -1486,14 +1454,7 @@ public boolean markPresent(Map> paths, Instant lastScanned) Integer savedCount = IntStream.rangeClosed(0, batches).parallel().map(b -> { try { List subList = pathsInFolderList.subList(b * BATCH_SIZE, Math.min((b + 1) * BATCH_SIZE, pathsInFolderList.size())); - List files = mediaFileRepository.findByFolderAndPathIn(folder, subList); - files.parallelStream().forEach(m -> { - m.setPresent(true); - m.setLastScanned(lastScanned); - } - ); - mediaFileRepository.saveAll(files); - return subList.size(); + return mediaFileRepository.markPresent(folder, subList, lastScanned); } catch (Exception ex) { LOG.warn("Error marking media files present", ex); return 0; @@ -1512,12 +1473,9 @@ public boolean markPresent(Map> paths, Instant lastScanned) * mark media files non present * @param lastScanned last scanned time before which media files are marked non present */ + @Transactional public void markNonPresent(Instant lastScanned) { - mediaFileRepository.findByLastScannedBeforeAndPresentTrue(lastScanned).forEach(m -> { - m.setPresent(false); - m.setChildrenLastUpdated(Instant.ofEpochMilli(1)); - mediaFileRepository.save(m); - }); + mediaFileRepository.markNonPresent(Instant.ofEpochMilli(1), lastScanned); } /** @@ -1529,6 +1487,7 @@ public void markNonPresent(Instant lastScanned) { @Caching(evict = { @CacheEvict(cacheNames = "mediaFilePathCache", key = "#mediaFile.path.concat('-').concat(#mediaFile.folder.id).concat('-').concat(#mediaFile.startPosition == null ? '' : #mediaFile.startPosition.toString())", condition = "#mediaFile != null"), @CacheEvict(cacheNames = "mediaFileIdCache", key = "#mediaFile.id", condition = "#mediaFile != null && #mediaFile.id != null") }) + @Transactional public MediaFile delete(MediaFile file) { if (file == null) { return null; @@ -1542,154 +1501,9 @@ public MediaFile delete(MediaFile file) { /** * delete all media files that are not present on disk */ + @Transactional public void expunge() { mediaFileRepository.deleteAllByPresentFalse(); } - /** - * update album stats - * - * @param file media file - * @param musicFolder music folder - * @param lastScanned last scanned time - * @param albumCount album count - * @param albums albums - * @param albumsInDb albums in db - */ - public void updateAlbum(MediaFile file, MusicFolder musicFolder, - Instant lastScanned, Map albumCount, Map albums, - Map albumsInDb) { - - String artist = file.getAlbumArtist() != null ? file.getAlbumArtist() : file.getArtist(); - if (file.getAlbumName() == null || artist == null || file.getParentPath() == null || !file.isAudio()) { - return; - } - - final AtomicBoolean firstEncounter = new AtomicBoolean(false); - Album album = albums.compute(file.getAlbumName() + "|" + artist, (k, v) -> { - Album a = v; - - if (a == null) { - a = albumRepository.findByArtistAndName(artist, file.getAlbumName()) - .map(dbAlbum -> { - albumsInDb.computeIfAbsent(dbAlbum.getId(), aid -> { - // reset stats when first retrieve from the db for new scan - dbAlbum.setDuration(0); - dbAlbum.setSongCount(0); - return dbAlbum; - }); - return dbAlbum; - }).orElse(null); - } - - if (a == null) { - a = new Album(); - a.setPath(file.getParentPath()); - a.setName(file.getAlbumName()); - a.setArtist(artist); - a.setCreated(file.getChanged()); - } - - firstEncounter.set(!lastScanned.equals(a.getLastScanned())); - - if (file.getDuration() != null) { - a.incrementDuration(file.getDuration()); - } - if (file.isAudio()) { - a.incrementSongCount(); - } - - a.setLastScanned(lastScanned); - a.setPresent(true); - - return a; - }); - - if (file.getMusicBrainzReleaseId() != null) { - album.setMusicBrainzReleaseId(file.getMusicBrainzReleaseId()); - } - if (file.getYear() != null) { - album.setYear(file.getYear()); - } - if (file.getGenre() != null) { - album.setGenre(file.getGenre()); - } - - if (album.getArt() == null) { - MediaFile parent = getParentOf(file, true); // true because the parent has recently already been scanned - if (parent != null) { - CoverArt art = coverArtService.get(EntityType.MEDIA_FILE, parent.getId()); - if (!CoverArt.NULL_ART.equals(art)) { - album.setArt(new CoverArt(-1, EntityType.ALBUM, art.getPath(), art.getFolder(), false)); - } - } - } - - if (firstEncounter.get()) { - album.setFolder(musicFolder); - - albumRepository.saveAndFlush(album); - albumCount.computeIfAbsent(artist, k -> new AtomicInteger(0)).incrementAndGet(); - indexManager.index(album); - } - - // Update the file's album artist, if necessary. - if (!ObjectUtils.equals(album.getArtist(), file.getAlbumArtist())) { - file.setAlbumArtist(album.getArtist()); - updateMediaFile(file); - } - } - - /** - * update artist stats - * - * @param file media file - * @param musicFolder music folder - * @param lastScanned last scanned time - * @param albumCount album count - * @param artists artists - */ - public void updateArtist(MediaFile file, MusicFolder musicFolder, Instant lastScanned, - Map albumCount, Map artists) { - if (file.getAlbumArtist() == null || !file.isAudio()) { - return; - } - - final AtomicBoolean firstEncounter = new AtomicBoolean(false); - - Artist artist = artists.compute(file.getAlbumArtist(), (k, v) -> { - Artist a = v; - - if (a == null) { - a = artistRepository.findByName(k).orElse(new Artist(k)); - } - - int n = Math.max(Optional.ofNullable(albumCount.get(a.getName())).map(x -> x.get()).orElse(0), - Optional.ofNullable(a.getAlbumCount()).orElse(0)); - a.setAlbumCount(n); - - firstEncounter.set(!lastScanned.equals(a.getLastScanned())); - - a.setLastScanned(lastScanned); - a.setPresent(true); - - return a; - }); - - if (artist.getArt() == null) { - MediaFile parent = getParentOf(file, true); // true because the parent has recently already been scanned - if (parent != null) { - CoverArt art = coverArtService.get(EntityType.MEDIA_FILE, parent.getId()); - if (!CoverArt.NULL_ART.equals(art)) { - artist.setArt(new CoverArt(-1, EntityType.ARTIST, art.getPath(), art.getFolder(), false)); - } - } - } - - if (firstEncounter.get()) { - artist.setFolder(musicFolder); - artistRepository.saveAndFlush(artist); - indexManager.index(artist, musicFolder); - } - } } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/MediaFolderService.java b/airsonic-main/src/main/java/org/airsonic/player/service/MediaFolderService.java index c197b2559..63f7c70b5 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/MediaFolderService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/MediaFolderService.java @@ -27,6 +27,7 @@ import java.util.Collection; import java.util.Comparator; import java.util.List; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.stream.Collectors; @@ -36,7 +37,6 @@ import static java.util.stream.Collectors.toList; @Service -@Transactional public class MediaFolderService { private static final Logger LOG = LoggerFactory.getLogger(MediaFolderService.class); @@ -89,6 +89,7 @@ public List getAllMusicFolders(boolean includeDisabled, boolean inc * @param username Username to get music folders for. * @return Possibly empty list of music folders. */ + @Transactional public List getMusicFoldersForUser(String username) { return cachedMusicFoldersPerUser.computeIfAbsent(username, u -> { return userRepository.findByUsername(u) @@ -114,6 +115,7 @@ public List getMusicFoldersForUser(String username, Integer selecte .collect(toList()); } + @Transactional public void setMusicFoldersForUser(String username, Collection musicFolderIds) { List folders = musicFolderRepository.findAllById(musicFolderIds); userRepository.findByUsername(username).ifPresent(u -> { @@ -131,6 +133,7 @@ public MusicFolder getMusicFolderById(Integer id, boolean includeDisabled, boole return getAllMusicFolders(includeDisabled, includeNonExisting).stream().filter(folder -> id.equals(folder.getId())).findAny().orElse(null); } + @Transactional public void createMusicFolder(MusicFolder musicFolder) { List registeredMusicFolders = musicFolderRepository.findAll(); Triple, List, List> overlaps = getMusicFolderPathOverlaps(musicFolder, registeredMusicFolders); @@ -240,6 +243,7 @@ private void reassignChildren(MusicFolder oldFolder, MusicFolder newFolder) { * * @param id Music folder id. */ + @Transactional public void deleteMusicFolder(Integer id) { musicFolderRepository.findByIdAndDeletedFalse(id).ifPresentOrElse(folder -> { @@ -279,6 +283,7 @@ public void deleteMusicFolder(Integer id) { * @param id Music folder id. Must be a podcast folder. * @return True if the music folder was enabled, false otherwise. */ + @Transactional public boolean enablePodcastFolder(int id) { return musicFolderRepository.findByIdAndTypeAndDeletedFalse(id, Type.PODCAST).map(podcastFolder -> { try { @@ -301,6 +306,7 @@ public boolean enablePodcastFolder(int id) { /** * Deletes all music folders that are marked as deleted. */ + @Transactional public void expunge() { musicFolderRepository.deleteAllByDeletedTrue(); } @@ -310,6 +316,7 @@ public void expunge() { * * @param info Music folder info. */ + @Transactional public void updateMusicFolderByInfo(MusicFolderInfo info) { if (info.getId() == null) { throw new IllegalArgumentException("Music folder id must be set."); @@ -408,11 +415,11 @@ public void clearMediaFileCache() { * @param includeNonExisting Whether to include non-existing folders. * @return Music folder that contains the file, or null if no music folder contains the file. */ - public MusicFolder getMusicFolderForFile(Path file, boolean includeDisabled, boolean includeNonExisting) { + public Optional getMusicFolderForFile(Path file, boolean includeDisabled, boolean includeNonExisting) { return getAllMusicFolders(includeDisabled, includeNonExisting).stream() .filter(folder -> FileUtil.isFileInFolder(file, folder.getPath())) .sorted(Comparator.comparing(folder -> folder.getPath().getNameCount(), Comparator.reverseOrder())) - .findFirst().orElse(null); + .findFirst(); } /** @@ -422,6 +429,6 @@ public MusicFolder getMusicFolderForFile(Path file, boolean includeDisabled, boo * @return Music folder that contains the file, or null if no music folder contains the file. */ public MusicFolder getMusicFolderForFile(Path file) { - return getMusicFolderForFile(file, false, true); + return getMusicFolderForFile(file, false, true).orElse(null); } } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/MediaScannerService.java b/airsonic-main/src/main/java/org/airsonic/player/service/MediaScannerService.java index a5c7f6640..276333287 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/MediaScannerService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/MediaScannerService.java @@ -14,6 +14,7 @@ You should have received a copy of the GNU General Public License along with Airsonic. If not, see . + Copyright 2024 (C) Y.Tory Copyright 2016 (C) Airsonic Authors Based upon Subsonic, Copyright 2009 (C) Sindre Mehus */ @@ -21,6 +22,7 @@ import org.airsonic.player.config.AirsonicScanConfig; import org.airsonic.player.domain.*; +import org.airsonic.player.domain.CoverArt.EntityType; import org.airsonic.player.service.search.IndexManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,7 +44,9 @@ import java.util.concurrent.ForkJoinWorkerThread; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; /** * Provides services for scanning the music library. @@ -54,7 +58,8 @@ public class MediaScannerService { private static final Logger LOG = LoggerFactory.getLogger(MediaScannerService.class); - private volatile boolean scanning; + private final AtomicBoolean scanning = new AtomicBoolean(false); + private final AtomicBoolean mediaScaninng = new AtomicBoolean(false); public MediaScannerService( SettingsService settingsService, @@ -148,19 +153,30 @@ boolean neverScanned() { * Returns whether the media library is currently being scanned. */ public boolean isScanning() { - return scanning; + return scanning.get(); + } + + /** + * Returns whether the media library is currently being scanned. + */ + public boolean isMediaScanning() { + return mediaScaninng.get(); } private void setScanning(boolean scanning) { - this.scanning = scanning; + this.scanning.set(scanning); broadcastScanStatus(); } + private void setMediaScanning(boolean mediaScaninng) { + this.mediaScaninng.set(mediaScaninng); + } + private void broadcastScanStatus() { CompletableFuture.runAsync(() -> { ScanStatus status = new ScanStatus(); status.setCount(scanCount.longValue()); - status.setScanning(scanning); + status.setScanning(scanning.get()); messagingTemplate.convertAndSend("/topic/scanStatus", status); }); } @@ -191,13 +207,17 @@ public synchronized void scanLibrary() { return; } setScanning(true); + setMediaScanning(true); ForkJoinPool pool = new ForkJoinPool(scannerParallelism, mediaScannerThreadFactory, null, true); boolean isFullScan = settingsService.getFullScan(); long timeoutSeconds = isFullScan ? scanConfig.getFullTimeout() : scanConfig.getTimeout(); + MediaLibraryStatistics statistics = new MediaLibraryStatistics(); LOG.info("Starting media library scan with timeout {} seconds.", timeoutSeconds); - CompletableFuture.runAsync(() -> doScanLibrary(pool), pool) + CompletableFuture.runAsync(() -> { + doScanLibrary(pool, statistics); + }, pool) .orTimeout(timeoutSeconds, TimeUnit.SECONDS) .whenComplete((r,e) -> { if (e instanceof TimeoutException) { @@ -207,48 +227,56 @@ public synchronized void scanLibrary() { } else { LOG.info("Media library scan completed."); } + setMediaScanning(false); }) .thenRunAsync(() -> playlistFileService.importPlaylists(), pool) .whenComplete((r,e) -> { pool.shutdown(); }) - .whenComplete((r,e) -> setScanning(false)); + .whenComplete((r,e) -> { + indexManager.stopIndexing(statistics); + setScanning(false); + }); } - private void doScanLibrary(ForkJoinPool pool) { + private void doScanLibrary(ForkJoinPool pool, MediaLibraryStatistics statistics) { LOG.info("Starting to scan media library."); - MediaLibraryStatistics statistics = new MediaLibraryStatistics(); LOG.debug("New last scan date is {}", statistics.getScanDate()); + Map albumCount = new ConcurrentHashMap<>(); + Map artists = new ConcurrentHashMap<>(); + Map albums = new ConcurrentHashMap<>(); + Set albumsInDb = Collections.synchronizedSet(new HashSet<>()); try { // Maps from artist name to album count. - Map albumCount = new ConcurrentHashMap<>(); - Map artists = new ConcurrentHashMap<>(); - Map albums = new ConcurrentHashMap<>(); - Map albumsInDb = new ConcurrentHashMap<>(); - Map> encountered = new ConcurrentHashMap<>(); Genres genres = new Genres(); scanCount.set(0); - mediaFileService.setMemoryCacheEnabled(false); indexManager.startIndexing(); + mediaFileService.setMemoryCacheEnabled(false); // Recurse through all files on disk. - mediaFolderService.getAllMusicFolders() - .parallelStream() - .forEach(musicFolder -> scanFile(mediaFileService.getMediaFile(Paths.get(""), musicFolder, false), - musicFolder, statistics, albumCount, artists, albums, albumsInDb, genres, encountered)); + pool.submit(() -> { + mediaFolderService.getAllMusicFolders() + .parallelStream() + .forEach(musicFolder -> scanFile(pool, null, mediaFileService.getMediaFile(Paths.get(""), musicFolder, false), + musicFolder, statistics, albumCount, artists, albums, albumsInDb, genres)); + // Update statistics + statistics.incrementArtists(albumCount.size()); + statistics.incrementAlbums(albumCount.values().parallelStream().mapToInt(x -> x.get()).sum()); + }).join(); LOG.info("Scanned media library with {} entries.", scanCount.get()); - // Update statistics - statistics.incrementArtists(albumCount.size()); - statistics.incrementAlbums(albumCount.values().parallelStream().mapToInt(x -> x.get()).sum()); + if (!isMediaScanning()) { + LOG.info("Scan cancelled."); + return; + } LOG.info("Persisting albums"); CompletableFuture albumPersistence = CompletableFuture - .allOf(albums.values().parallelStream() + .allOf(albums.values().stream() .distinct() .map(a -> CompletableFuture.supplyAsync(() -> { return albumService.save(a); @@ -262,7 +290,8 @@ private void doScanLibrary(ForkJoinPool pool) { LOG.info("Persisting artists"); CompletableFuture artistPersistence = CompletableFuture - .allOf(artists.values().parallelStream() + .allOf(artists.values().stream() + .distinct() .map(a -> CompletableFuture.supplyAsync(() -> { return artistService.save(a); }, pool).thenAcceptAsync(coverArtService::persistIfNeeded)) @@ -273,15 +302,6 @@ private void doScanLibrary(ForkJoinPool pool) { }, pool) .thenRunAsync(() -> LOG.info("Artist persistence complete"), pool); - LOG.info("Marking present files"); - CompletableFuture mediaFilePersistence = CompletableFuture - .runAsync(() -> mediaFileService.markPresent(encountered, statistics.getScanDate()), pool) - .thenRunAsync(() -> { - LOG.info("Marking non-present files."); - mediaFileService.markNonPresent(statistics.getScanDate()); - }, pool) - .thenRunAsync(() -> LOG.info("File marking complete"), pool); - LOG.info("Persisting genres"); CompletableFuture genrePersistence = CompletableFuture .runAsync(() -> { @@ -291,28 +311,36 @@ private void doScanLibrary(ForkJoinPool pool) { LOG.info("Genre persistence successfully complete: {}", genresSuccessful); }, pool); - CompletableFuture.allOf(albumPersistence, artistPersistence, mediaFilePersistence, genrePersistence).join(); - - if (settingsService.getClearFullScanSettingAfterScan()) { - settingsService.setClearFullScanSettingAfterScan(null); - settingsService.setFullScan(null); - settingsService.save(); - } - + CompletableFuture.allOf(albumPersistence, artistPersistence, genrePersistence).join(); LOG.info("Completed media library scan."); } catch (Throwable x) { LOG.error("Failed to scan media library.", x); } finally { mediaFileService.setMemoryCacheEnabled(true); - indexManager.stopIndexing(statistics); + if (settingsService.getClearFullScanSettingAfterScan()) { + settingsService.setClearFullScanSettingAfterScan(null); + settingsService.setFullScan(null); + settingsService.save(); + } + //clearing cache + albumCount.clear(); + artists.clear(); + albumsInDb.clear(); + albums.clear(); LOG.info("Media library scan took {}s", ChronoUnit.SECONDS.between(statistics.getScanDate(), Instant.now())); } } - private void scanFile(MediaFile file, MusicFolder musicFolder, MediaLibraryStatistics statistics, + private void scanFile(ForkJoinPool pool, MediaFile parent, MediaFile file, MusicFolder musicFolder, MediaLibraryStatistics statistics, Map albumCount, Map artists, Map albums, - Map albumsInDb, Genres genres, Map> encountered) { + Set albumsInDb, Genres genres) { + + if (!isMediaScanning()) { + LOG.debug("Scan cancelled."); + return; + } + if (scanCount.incrementAndGet() % 250 == 0) { broadcastScanStatus(); LOG.info("Scanned media library with {} entries.", scanCount.get()); @@ -327,28 +355,36 @@ private void scanFile(MediaFile file, MusicFolder musicFolder, MediaLibraryStati indexManager.index(file, musicFolder); try { - if (file.isDirectory()) { - mediaFileService.getChildrenOf(file, true, true, false, false).parallelStream() - .forEach(child -> scanFile(child, musicFolder, statistics, albumCount, artists, albums, albumsInDb, genres, encountered)); - } else { - if (musicFolder.getType() == MusicFolder.Type.MEDIA) { - mediaFileService.updateAlbum(file, musicFolder, statistics.getScanDate(), albumCount, albums, albumsInDb); - mediaFileService.updateArtist(file, musicFolder, statistics.getScanDate(), albumCount, artists); + pool.submit(() -> { + if (file.isDirectory()) { + try (Stream children = mediaFileService.getChildrenOf(file, true, true, false, false) + .parallelStream()) { + children.forEach(child -> scanFile(pool, file, child, musicFolder, statistics, albumCount, + artists, albums, albumsInDb, genres)); + } + } else { + if (musicFolder.getType() == MusicFolder.Type.MEDIA) { + updateAlbum(parent, file, musicFolder, statistics.getScanDate(), albumCount, albums, albumsInDb); + updateArtist(parent, file, musicFolder, statistics.getScanDate(), albumCount, artists); + } + statistics.incrementSongs(1); } - statistics.incrementSongs(1); - } - updateGenres(file, genres); - encountered.computeIfAbsent(file.getFolder().getId(), k -> ConcurrentHashMap.newKeySet()).add(file.getPath()); + if (file.isPresent() && (file.getLastScanned() == null || file.getLastScanned().isBefore(statistics.getScanDate()))) { + file.setLastScanned(statistics.getScanDate()); + mediaFileService.updateMediaFile(file); + } + updateGenres(file, genres); - // don't add indexed tracks to the total duration to avoid double-counting - if ((file.getDuration() != null) && (!file.isIndexedTrack())) { - statistics.incrementTotalDurationInSeconds(file.getDuration()); - } - // don't add indexed tracks to the total size to avoid double-counting - if ((file.getFileSize() != null) && (!file.isIndexedTrack())) { - statistics.incrementTotalLengthInBytes(file.getFileSize()); - } + // don't add indexed tracks to the total duration to avoid double-counting + if ((file.getDuration() != null) && (!file.isIndexedTrack())) { + statistics.incrementTotalDurationInSeconds(file.getDuration()); + } + // don't add indexed tracks to the total size to avoid double-counting + if ((file.getFileSize() != null) && (!file.isIndexedTrack())) { + statistics.incrementTotalLengthInBytes(file.getFileSize()); + } + }).join(); } catch (Exception e) { LOG.warn("scan file failed : {} in {}", file.getPath(), musicFolder.getPath(), e); } @@ -365,4 +401,145 @@ private void updateGenres(MediaFile file, Genres genres) { genres.incrementSongCount(genre, settingsService.getGenreSeparators()); } } + + /** + * update album stats + * + * @param file media file + * @param musicFolder music folder + * @param lastScanned last scanned time + * @param albumCount album count + * @param albums albums + * @param albumsInDb albums in db + */ + private void updateAlbum(MediaFile parent, MediaFile file, MusicFolder musicFolder, + Instant lastScanned, Map albumCount, Map albums, + Set albumsInDb) { + + String artist = file.getAlbumArtist() != null ? file.getAlbumArtist() : file.getArtist(); + if (file.getAlbumName() == null || artist == null || file.getParentPath() == null || !file.isAudio()) { + return; + } + + final AtomicBoolean firstEncounter = new AtomicBoolean(false); + Album album = albums.compute(file.getAlbumName() + "|" + artist, (k, v) -> { + Album a = v; + + if (a == null) { + a = albumService.getAlbumByArtistAndName(artist, file.getAlbumName()).map(dbAlbum -> { + if (!albumsInDb.contains(dbAlbum.getId())) { + albumsInDb.add(dbAlbum.getId()); + dbAlbum.setDuration(0); + dbAlbum.setSongCount(0); + } + return dbAlbum; + }).orElse(null); + } + + if (a == null) { + a = new Album(); + a.setPath(file.getParentPath()); + a.setName(file.getAlbumName()); + a.setArtist(artist); + a.setCreated(file.getChanged()); + } + + firstEncounter.set(!lastScanned.equals(a.getLastScanned())); + + if (file.getDuration() != null) { + a.incrementDuration(file.getDuration()); + } + if (file.isAudio()) { + a.incrementSongCount(); + } + + a.setLastScanned(lastScanned); + a.setPresent(true); + + return a; + }); + + if (file.getMusicBrainzReleaseId() != null) { + album.setMusicBrainzReleaseId(file.getMusicBrainzReleaseId()); + } + if (file.getYear() != null) { + album.setYear(file.getYear()); + } + if (file.getGenre() != null) { + album.setGenre(file.getGenre()); + } + + if (album.getArt() == null && parent != null) { + CoverArt art = coverArtService.get(EntityType.MEDIA_FILE, parent.getId()); + if (!CoverArt.NULL_ART.equals(art)) { + album.setArt(new CoverArt(-1, EntityType.ALBUM, art.getPath(), art.getFolder(), false)); + } + } + + if (firstEncounter.get()) { + album.setFolder(musicFolder); + albumService.save(album); + albumCount.computeIfAbsent(artist, k -> new AtomicInteger(0)).incrementAndGet(); + indexManager.index(album); + } + + // Update the file's album artist, if necessary. + if (!Objects.equals(album.getArtist(), file.getAlbumArtist())) { + file.setAlbumArtist(album.getArtist()); + mediaFileService.updateMediaFile(file); + } + } + + /** + * update artist stats + * + * @param file media file + * @param musicFolder music folder + * @param lastScanned last scanned time + * @param albumCount album count + * @param artists artists + */ + private void updateArtist(MediaFile parent, MediaFile file, MusicFolder musicFolder, Instant lastScanned, + Map albumCount, Map artists) { + if (file.getAlbumArtist() == null || !file.isAudio()) { + return; + } + + final AtomicBoolean firstEncounter = new AtomicBoolean(false); + + Artist artist = artists.compute(file.getAlbumArtist(), (k, v) -> { + Artist a = v; + + if (a == null) { + a = artistService.getArtist(k); + if (a == null) { + a = new Artist(k); + } + } + + int n = Math.max(Optional.ofNullable(albumCount.get(a.getName())).map(x -> x.get()).orElse(0), + Optional.ofNullable(a.getAlbumCount()).orElse(0)); + a.setAlbumCount(n); + + firstEncounter.set(!lastScanned.equals(a.getLastScanned())); + + a.setLastScanned(lastScanned); + a.setPresent(true); + + return a; + }); + + if (firstEncounter.get()) { + artist.setFolder(musicFolder); + artistService.save(artist); + indexManager.index(artist, musicFolder); + } + + if (artist.getArt() == null && parent != null) { + CoverArt art = coverArtService.get(EntityType.MEDIA_FILE, parent.getId()); + if (!CoverArt.NULL_ART.equals(art)) { + artist.setArt(new CoverArt(-1, EntityType.ARTIST, art.getPath(), art.getFolder(), false)); + } + } + } } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/PersonalSettingsService.java b/airsonic-main/src/main/java/org/airsonic/player/service/PersonalSettingsService.java index 8a6f3b3dd..62a2a1724 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/PersonalSettingsService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/PersonalSettingsService.java @@ -62,7 +62,6 @@ import java.util.Map; @Service -@Transactional public class PersonalSettingsService { private static final Logger LOG = LoggerFactory.getLogger(PersonalSettingsService.class); @@ -242,6 +241,7 @@ private UserSettingDetail createDefaultUserSetting() { return detail; } + @Transactional @CacheEvict(cacheNames = "userSettingsCache", key = "#username") public void updateByCommand(String username, Locale locale, String themeId, PersonalSettingsCommand command) { @@ -311,6 +311,7 @@ private Integer getSystemAvatarId(PersonalSettingsCommand command) { * @param username The username. * @param scheme The transcode scheme. */ + @Transactional @CacheEvict(cacheNames = "userSettingsCache", key = "#username") public void updateTranscodeScheme(String username, TranscodeScheme scheme) { @@ -330,6 +331,7 @@ public void updateTranscodeScheme(String username, TranscodeScheme scheme) { * @param username The username. * @param musicFolderId The music folder id. */ + @Transactional @CacheEvict(cacheNames = "userSettingsCache", key = "#username") public void updateSelectedMusicFolderId(String username, Integer musicFolderId) { @@ -347,6 +349,7 @@ public void updateSelectedMusicFolderId(String username, Integer musicFolderId) * @param username The username. * @param showSideBar The show side bar status. */ + @Transactional @CacheEvict(cacheNames = "userSettingsCache", key = "#username") public void updateShowSideBarStatus(String username, boolean showSideBar) { @@ -366,6 +369,7 @@ public void updateShowSideBarStatus(String username, boolean showSideBar) { * @param username The username. * @parama viewAsList The view as list status. */ + @Transactional @CacheEvict(cacheNames = "userSettingsCache", key = "#username") public void updateViewAsListStatus(String username, boolean viewAsList) { diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/PlayerService.java b/airsonic-main/src/main/java/org/airsonic/player/service/PlayerService.java index aca12d649..7de767cf0 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/PlayerService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/PlayerService.java @@ -62,7 +62,6 @@ * @see Player */ @Service -@Transactional @DependsOn("liquibase") public class PlayerService { @@ -81,6 +80,7 @@ public class PlayerService { private AsyncWebSocketClient asyncWebSocketClient; @EventListener + @Transactional public void onApplicationEvent(ApplicationReadyEvent event) { deleteOldPlayers(60); } @@ -227,6 +227,7 @@ private boolean populatePlayer(Player player, String username, HttpServletReques * * @param player The player to update. */ + @Transactional public void updatePlayer(Player player) { playerRepository.save(player); if (player.getUsername() != null) { @@ -360,6 +361,7 @@ public List getAllPlayers() { * * @param id The unique player ID. */ + @Transactional public void removePlayerById(int id) { playerRepository.findById(id).ifPresentOrElse(player -> { playlists.remove(id); @@ -405,6 +407,7 @@ public Player clonePlayer(int playerId) { * * @param player The player to create. */ + @Transactional public Player createPlayer(Player player) { // Set default transcodings. @@ -469,6 +472,7 @@ public Player getGuestPlayer(String remoteAddress) { * @param command The command to update the player with. * @return The updated player. */ + @Transactional public Player updateByCommand(PlayerSettingsCommand command) { return playerRepository.findById(command.getPlayerId()).map(player -> { String name = StringUtils.trimToNull(command.getName()); diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/PlaylistService.java b/airsonic-main/src/main/java/org/airsonic/player/service/PlaylistService.java index 3073234bb..f8900009a 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/PlaylistService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/PlaylistService.java @@ -48,7 +48,6 @@ * @see PlayQueue */ @Service -@Transactional public class PlaylistService { private static final Logger LOG = LoggerFactory.getLogger(PlaylistService.class); @@ -128,6 +127,7 @@ public Playlist getPlaylist(int id) { } @Cacheable(cacheNames = "playlistUsersCache", unless = "#result == null") + @Transactional public List getPlaylistUsers(int playlistId) { List users = playlistRepository.findById(playlistId).map(Playlist::getSharedUsers).orElse(Collections.emptyList()); return users.stream().map(User::getUsername).filter(Objects::nonNull).toList(); @@ -148,8 +148,13 @@ public List getFilesInPlaylist(int id, boolean includeNotPresent) { ).stream().filter(x -> includeNotPresent || x.isPresent()).collect(Collectors.toList()); } + @Transactional public Playlist setFilesInPlaylist(int id, List files) { - return playlistRepository.findById(id).map(p -> setFilesInPlaylist(p, files)).orElseGet( + return playlistRepository.findById(id).map(p -> { + Playlist playlist = setFilesInPlaylist(p, files); + playlistRepository.saveAndFlush(playlist); + return playlist; + }).orElseGet( () -> { LOG.warn("Playlist {} not found", id); return null; @@ -161,11 +166,11 @@ private Playlist setFilesInPlaylist(Playlist playlist, List files) { playlist.setFileCount(files.size()); playlist.setDuration(files.stream().mapToDouble(MediaFile::getDuration).sum()); playlist.setChanged(Instant.now()); - playlistRepository.saveAndFlush(playlist); return playlist; } @CacheEvict(cacheNames = "playlistCache", key = "#id") + @Transactional public void removeFilesInPlaylistByIndices(int id, List indices) { playlistRepository.findById(id).ifPresentOrElse(p -> { List files = p.getMediaFiles(); @@ -175,7 +180,8 @@ public void removeFilesInPlaylistByIndices(int id, List indices) { newFiles.add(files.get(i)); } } - setFilesInPlaylist(p, newFiles); + Playlist playlist = setFilesInPlaylist(p, newFiles); + playlistRepository.save(playlist); }, () -> { LOG.warn("Playlist {} not found", id); } @@ -185,6 +191,7 @@ public void removeFilesInPlaylistByIndices(int id, List indices) { /** * Refreshes the file count and duration of all playlists. */ + @Transactional public List refreshPlaylistsStats() { return playlistRepository.findAll().stream().map(p -> { p.setFileCount(p.getMediaFiles().size()); @@ -203,6 +210,7 @@ public List refreshPlaylistsStats() { * @param username the username of the user that created the playlist * @return the created playlist */ + @Transactional public Playlist createPlaylist(String name, boolean shared, String username) { Instant now = Instant.now(); Playlist playlist = new Playlist(); @@ -220,6 +228,7 @@ public Playlist createPlaylist(String name, boolean shared, String username) { * Creates a new playlist. * @param playlist */ + @Transactional public Playlist createPlaylist(Playlist playlist) { Instant now = Instant.now(); playlist.setCreated(now); @@ -234,6 +243,7 @@ public Playlist createPlaylist(Playlist playlist) { } @CacheEvict(cacheNames = "playlistUsersCache", key = "#playlist.id") + @Transactional public void addPlaylistUser(Playlist playlist, String username) { userRepository.findByUsername(username).ifPresentOrElse(user -> { playlistRepository.findById(playlist.getId()).ifPresentOrElse(p -> { @@ -256,6 +266,7 @@ public void addPlaylistUser(Playlist playlist, String username) { } @CacheEvict(cacheNames = "playlistUsersCache", key = "#playlist.id") + @Transactional public void deletePlaylistUser(Playlist playlist, String username) { playlistRepository.findByIdAndSharedUsersUsername(playlist.getId(), username).ifPresentOrElse(p -> { p.removeSharedUserByUsername(username); @@ -299,6 +310,7 @@ public boolean isWriteAllowed(Playlist playlist, String username) { } @CacheEvict(cacheNames = "playlistCache") + @Transactional public void deletePlaylist(int id) { playlistRepository.deleteById(id); asyncWebSocketClient.send("/topic/playlists/deleted", id); @@ -319,6 +331,7 @@ public void updatePlaylist(Integer id, String name) { } @CacheEvict(cacheNames = "playlistCache", key = "#id") + @Transactional public void updatePlaylist(Integer id, String name, String comment, Boolean shared) { playlistRepository.findById(id).ifPresentOrElse(p -> { p.setName(name); @@ -353,6 +366,7 @@ public void broadcast(Playlist playlist) { * @param isShared if true, the playlist was shared with other users * @param filesChangedBroadcastContext if true, the client will know that the files in the playlist have changed */ + @Transactional(readOnly = true) public void broadcastFileChange(Integer id, boolean isShared, boolean filesChangedBroadcastContext) { playlistRepository.findById(id).ifPresent(playlist -> { BroadcastedPlaylist bp = new BroadcastedPlaylist(playlist, filesChangedBroadcastContext); diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/PodcastPersistenceService.java b/airsonic-main/src/main/java/org/airsonic/player/service/PodcastPersistenceService.java index d3862d00d..5cc970804 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/PodcastPersistenceService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/PodcastPersistenceService.java @@ -62,7 +62,6 @@ * @author Sindre Mehus */ @Service -@Transactional public class PodcastPersistenceService { private static final Logger LOG = LoggerFactory.getLogger(PodcastPersistenceService.class); @@ -106,11 +105,9 @@ public PodcastPersistenceService( */ public void cleanDownloadingEpisodes() { podcastChannelRepository.findAll() - .parallelStream() - .map(PodcastChannel::getId) - .map(this::getEpisodes) - .flatMap(List::parallelStream) - .filter(e -> e.getStatus() == PodcastStatus.DOWNLOADING) + .stream() + .flatMap(c -> podcastEpisodeRepository.findByChannelAndStatus(c, PodcastStatus.DOWNLOADING).stream()) + .filter(filterAllowed) .forEach(e -> { deleteEpisode(e, false); LOG.info("Deleted Podcast episode '{}' since download was interrupted.", e.getTitle()); @@ -124,6 +121,7 @@ public void cleanDownloadingEpisodes() { * @param status status to reset * @return list of channel ids that were reset */ + @Transactional public List resetChannelStatus(PodcastStatus status) { return podcastChannelRepository.findByStatus(status).stream() .map(c -> { @@ -140,6 +138,7 @@ public List resetChannelStatus(PodcastStatus status) { * @param channelId channel id to refresh * @return channel if refresh is prepared, null if channel is already refreshing or not found */ + @Transactional public PodcastChannel prepareRefreshChannel(Integer channelId) { return podcastChannelRepository.findById(channelId).map(channel -> { if (channel.getStatus() == PodcastStatus.DOWNLOADING) { @@ -164,6 +163,7 @@ public PodcastChannel prepareRefreshChannel(Integer channelId) { * @param element element to update from * @return updated channel or original channel if element is null */ + @Transactional public PodcastChannel updateChannelByElement(PodcastChannel channel, Element element) { if (element == null) { return channel; @@ -184,6 +184,7 @@ public PodcastChannel updateChannelByElement(PodcastChannel channel, Element ele * @param channel channel to set error * @param errorMessage error message */ + @Transactional public void setChannelError(PodcastChannel channel, String errorMessage) { podcastChannelRepository.findById(channel.getId()).ifPresent( c -> { @@ -199,6 +200,7 @@ public void setChannelError(PodcastChannel channel, String errorMessage) { * * @param channel channel to set completed */ + @Transactional public void setChannelCompleted(PodcastChannel channel) { podcastChannelRepository.findById(channel.getId()).ifPresent( c -> { @@ -224,6 +226,7 @@ public List getChannelsWithoutRule() { * @param command command * @return PodcastChannelRule */ + @Transactional public PodcastChannelRule createOrUpdateChannelRuleByCommand(PodcastRule command) { if (command == null || !command.isValid()) { @@ -306,6 +309,7 @@ private String getITunesAttribute(Element element, String childName, String attr * @param id id of podcast channel rule * @param deleteChannel delete channel or not */ + @Transactional public boolean deleteChannelRule(Integer id) { return podcastRuleRepository.findById(id).map(rule -> { podcastRuleRepository.delete(rule); @@ -326,6 +330,7 @@ public List getAllChannelRules() { return podcastRuleRepository.findAll(); } + @Transactional public PodcastChannel createChannel(String url) { if (StringUtils.isBlank(url)) { return null; @@ -359,6 +364,7 @@ public List getAllChannels() { * @return Possibly empty list of all Podcast episodes for the given channel, sorted in * reverse chronological order (newest episode first). */ + @Transactional public List getEpisodes(Integer channelId) { if (Objects.isNull(channelId)) return new ArrayList<>(); return podcastChannelRepository.findById(channelId).map(channel -> { @@ -408,6 +414,7 @@ public List getNewestEpisodes(int count) { * @param includeDeleted include deleted episodes * @return The Podcast episode, or null if not found. */ + @Transactional public PodcastEpisode getEpisode(int episodeId, boolean includeDeleted) { return podcastEpisodeRepository.findById(episodeId) .map(ep -> { @@ -416,10 +423,7 @@ public PodcastEpisode getEpisode(int episodeId, boolean includeDeleted) { // Refresh media file to check if it still exists mediaFileService.refreshMediaFile(mediaFile); if (!mediaFile.isPresent() && ep.getStatus() != PodcastStatus.DELETED) { - // If media file is not present anymore, set episode status to deleted - ep.setStatus(PodcastStatus.DELETED); - ep.setErrorMessage(null); - podcastEpisodeRepository.save(ep); + deleteEpisode(ep, true); } } return ep; @@ -451,6 +455,7 @@ public PodcastExportOPML exportAllChannels() { * @param episodeId episode id to prepare * @return episode if download is prepared, null if episode is already downloading or not found */ + @Transactional public PodcastEpisode prepareDownloadEpisode(Integer episodeId) { return podcastEpisodeRepository.findById(episodeId).map(episode -> { if (episode.getStatus() == PodcastStatus.DELETED) { @@ -491,6 +496,7 @@ public boolean isEpisodeDeleted(Integer episodeId) { * @param length episode length * @return created episode */ + @Transactional public PodcastEpisode createEpisode(PodcastChannel channel, String guid, String url, String title, String description, Instant date, String duration, Long length) { PodcastEpisode episode = new PodcastEpisode(null, channel, guid, url, null, title, description, date, @@ -505,6 +511,7 @@ public PodcastEpisode createEpisode(PodcastChannel channel, String guid, String * @param episode episode to update * @return updated episode or original episode if element is null */ + @Transactional public PodcastEpisode updateEpisode(PodcastEpisode episode) { return podcastEpisodeRepository.findById(episode.getId()).map(ep -> { ep.setTitle(episode.getTitle()); @@ -528,6 +535,7 @@ public PodcastEpisode updateEpisode(PodcastEpisode episode) { * @param channelId The Podcast channel ID. * @return Whether the channel was deleted. */ + @Transactional public boolean deleteChannel(int channelId) { // Delete all associated episodes (in case they have files that need to be deleted). return podcastChannelRepository.findById(channelId).map(channel -> { @@ -556,6 +564,7 @@ public boolean deleteChannel(int channelId) { * @param logicalDelete Whether to perform a logical delete by setting the * episode status to {@link PodcastStatus#DELETED}. */ + @Transactional public void deleteEpisode(int episodeId, boolean logicalDelete) { podcastEpisodeRepository.findById(episodeId).ifPresentOrElse(e -> { deleteEpisode(e, logicalDelete); diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/SecurityService.java b/airsonic-main/src/main/java/org/airsonic/player/service/SecurityService.java index 6cb637caf..171552c59 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/SecurityService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/SecurityService.java @@ -80,7 +80,6 @@ */ @Service @CacheConfig(cacheNames = "userCache") -@Transactional public class SecurityService implements UserDetailsService { private static final Logger LOG = LoggerFactory.getLogger(SecurityService.class); @@ -136,6 +135,7 @@ public UserDetails loadUserByUsername(String username, boolean caseSensitive) * @param comment The comment to add to the credential * @return true if the credential was created successfully */ + @Transactional public boolean createCredential(String username, CredentialsCommand command, String comment) { Optional user = userRepository.findByUsername(username); @@ -171,6 +171,7 @@ public boolean createCredential(String username, CredentialsCommand command, Str * the new encoder * @return true if the credentials were updated successfully */ + @Transactional public boolean updateCredentials(String username, CredentialsManagementCommand command, String comment, boolean reencodePlaintextNewCreds) { @@ -221,6 +222,7 @@ public boolean updateCredentials(String username, CredentialsManagementCommand c * @param comment The comment to add to the credential */ @CacheEvict(key = "#username", condition = "#username != null") + @Transactional public void recoverCredential(String username, String password, String comment) { if (StringUtils.isBlank(username)) { LOG.warn("Can't recover credential for a blank username"); @@ -259,6 +261,7 @@ public boolean createAirsonicCredential(String username, String password, String * password to create the credential for * @param comment The comment to add to * the credential * @return true if the credential was created successfully */ + @Transactional private boolean createAirsonicCredentialToUser(User user, String password, String comment) { String encoder = getPreferredPasswordEncoder(true); try { @@ -277,6 +280,7 @@ private boolean createAirsonicCredentialToUser(User user, String password, Strin * * * @param creds The credential to delete * @return true if the credential * was deleted successfully */ + @Transactional public boolean deleteCredential(UserCredential creds) { if (creds == null || creds.getUser() == null) { LOG.warn("Can't delete a null credential"); @@ -336,6 +340,7 @@ public boolean checkLegacyCredsPresent() { * @param useDecodableOnly if true, only migrate to decodable encoders * @return true if all credentials were migrated successfully */ + @Transactional public boolean migrateLegacyCredsToNonLegacy(boolean useDecodableOnly) { String decodableEncoder = settingsService.getDecodablePasswordEncoder(); String nonDecodableEncoder = useDecodableOnly ? decodableEncoder @@ -447,6 +452,7 @@ public User getUserByName(String username, boolean caseSensitive) { */ // TODO: This is not security related. Move to a different service. @Cacheable(key = "#username", unless = "#result == null") + @Transactional public User incrementBytesStreamed(String username, long deltaBytesStreamed) { User user = getUserByName(username); if (Objects.nonNull(user)) { @@ -465,6 +471,7 @@ public User incrementBytesStreamed(String username, long deltaBytesStreamed) { */ // TODO: This is not security related. Move to a different service. @Cacheable(key = "#username", unless = "#result == null") + @Transactional public User incrementBytesDownloaded(String username, long deltaBytesDownloaded) { User user = getUserByName(username); if (Objects.nonNull(user)) { @@ -483,6 +490,7 @@ public User incrementBytesDownloaded(String username, long deltaBytesDownloaded) */ // TODO: This is not security related. Move to a different service. @Cacheable(key = "#username", unless = "#result == null") + @Transactional public User incrementBytesUploaded(String username, long deltaBytesUploaded) { User user = getUserByName(username); if (Objects.nonNull(user)) { @@ -499,6 +507,7 @@ public User incrementBytesUploaded(String username, long deltaBytesUploaded) { * @param currentUsername current user name. */ @CacheEvict(key = "#username", condition = "#username != null") + @Transactional public void deleteUser(String username, String currentUsername) { if (StringUtils.isNotBlank(username) && username.equals(currentUsername)) { throw new SelfDeletionException(); @@ -538,6 +547,7 @@ public boolean isAdmin(String username) { * @param user The user to create. * @param credential The raw credential (will be encoded) */ + @Transactional public void createUser(User user, String credential, String comment) { String defaultEncoder = getPreferredPasswordEncoder(true); UserCredential uc = new UserCredential( @@ -570,6 +580,7 @@ public void createGuestUserIfNotExists() { * */ @Cacheable(key = "#command.username", unless = "#result == null", condition = "#command != null") + @Transactional public User updateUserByUserSettingsCommand(UserSettingsCommand command) { // check if (Objects.isNull(command)) { diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/SettingsService.java b/airsonic-main/src/main/java/org/airsonic/player/service/SettingsService.java index 12ad60032..e2c8798fa 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/SettingsService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/SettingsService.java @@ -1401,6 +1401,21 @@ public String getDatabaseUrl() { return getString(KEY_DATABASE_URL, DEFAULT_DATABASE_URL); } + public String getDatabaseType() { + String url = getDatabaseUrl(); + if (url.contains("mysql")) { + return "mysql"; + } else if (url.contains("postgresql")) { + return "postgresql"; + } else if (url.contains("hsqldb")) { + return "hsqldb"; + } else if (url.contains("mariadb")) { + return "mariadb"; + } else { + return "unknown"; + } + } + public void setDatabaseUrl(String url) { setString(KEY_DATABASE_URL, url); } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/ShareService.java b/airsonic-main/src/main/java/org/airsonic/player/service/ShareService.java index 8d12eff8d..cc8cc582c 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/ShareService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/ShareService.java @@ -31,11 +31,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import org.springframework.web.util.UriComponentsBuilder; import javax.servlet.http.HttpServletRequest; -import javax.transaction.Transactional; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -52,7 +52,6 @@ * @see Share */ @Service -@Transactional public class ShareService { private static final Logger LOG = LoggerFactory.getLogger(ShareService.class); @@ -87,6 +86,7 @@ public Share getShareByName(String name) { return shareRepository.findByName(name).orElse(null); } + @Transactional public List getSharedFiles(int id, List musicFolders) { if (CollectionUtils.isEmpty(musicFolders)) { @@ -98,6 +98,7 @@ public List getSharedFiles(int id, List musicFolders) { .collect(toList()); } + @Transactional public Share createShare(String username, List files) { Instant now = Instant.now(); @@ -119,10 +120,12 @@ public Share createShare(String username, List files) { return shareRepository.save(share); } + @Transactional public void updateShare(Share share) { shareRepository.save(share); } + @Transactional public void deleteShare(int id) { shareRepository.deleteById(id); } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/TranscodingService.java b/airsonic-main/src/main/java/org/airsonic/player/service/TranscodingService.java index dc8f74500..22299c36e 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/TranscodingService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/TranscodingService.java @@ -61,7 +61,6 @@ * @see TranscodeInputStream */ @Service -@Transactional public class TranscodingService { private static final Logger LOG = LoggerFactory.getLogger(TranscodingService.class); @@ -101,6 +100,7 @@ public List getTranscodingsForPlayer(Player player) { * @param player The player. * @param transcodingIds ID's of the active transcodings. */ + @Transactional public void setTranscodingsForPlayerByIds(Player player, List transcodingIds) { List transcodings = transcodingRepository.findByIdIn(transcodingIds); setTranscodingsForPlayer(player, transcodings); @@ -112,6 +112,7 @@ public void setTranscodingsForPlayerByIds(Player player, List transcodi * @param player The player. * @param transcodings The active transcodings. */ + @Transactional public void setTranscodingsForPlayer(Player player, List transcodings) { player.setTranscodings(transcodings); playerRepository.save(player); @@ -122,6 +123,7 @@ public void setTranscodingsForPlayer(Player player, List transcodin * * @param transcoding The transcoding to create. */ + @Transactional public void createTranscoding(Transcoding transcoding) { // Activate this transcoding for all players? transcodingRepository.save(transcoding); @@ -137,6 +139,7 @@ public void createTranscoding(Transcoding transcoding) { * * @param id The transcoding ID. */ + @Transactional public void deleteTranscoding(Integer id) { transcodingRepository.deleteById(id); } @@ -146,6 +149,7 @@ public void deleteTranscoding(Integer id) { * * @param transcoding The transcoding to update. */ + @Transactional public void updateTranscoding(Transcoding transcoding) { transcodingRepository.save(transcoding); } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/search/SearchServiceUtilities.java b/airsonic-main/src/main/java/org/airsonic/player/service/search/SearchServiceUtilities.java index 9d72b782f..2c12e0c46 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/search/SearchServiceUtilities.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/search/SearchServiceUtilities.java @@ -32,7 +32,6 @@ import org.apache.lucene.document.Document; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; import java.util.Collection; import java.util.List; @@ -50,7 +49,6 @@ * so do not include exception handling in this class. */ @Component -@Transactional public class SearchServiceUtilities { /* Search by id only. */ diff --git a/airsonic-main/src/main/resources/application.properties b/airsonic-main/src/main/resources/application.properties index 35c1a8c25..d9d3870ea 100644 --- a/airsonic-main/src/main/resources/application.properties +++ b/airsonic-main/src/main/resources/application.properties @@ -8,6 +8,7 @@ spring.jpa.hibernate.ddl-auto=none spring.jpa.open-in-view=false spring.servlet.multipart.max-file-size=512MB spring.servlet.multipart.max-request-size=512MB +spring.datasource.hikari.maximum-pool-size=20 server.error.includeStacktrace=ALWAYS logging.level.root=WARN logging.level.org.airsonic=INFO diff --git a/airsonic-main/src/main/resources/liquibase/11.1/change-hsqldb-table-type.xml b/airsonic-main/src/main/resources/liquibase/11.1/change-hsqldb-table-type.xml new file mode 100644 index 000000000..73873f729 --- /dev/null +++ b/airsonic-main/src/main/resources/liquibase/11.1/change-hsqldb-table-type.xml @@ -0,0 +1,85 @@ + + + + + SET DATABASE DEFAULT TABLE TYPE CACHED + SET TABLE PUBLIC.ALBUM TYPE CACHED; + SET TABLE PUBLIC.ARTIST TYPE CACHED; + SET TABLE PUBLIC.BOOKMARK TYPE CACHED; + SET TABLE PUBLIC.COVER_ART TYPE CACHED; + SET TABLE PUBLIC.CUSTOM_AVATAR TYPE CACHED; + SET TABLE PUBLIC.DATABASECHANGELOG TYPE CACHED; + SET TABLE PUBLIC.DATABASECHANGELOGLOCK TYPE CACHED; + SET TABLE PUBLIC.GENRE TYPE CACHED; + SET TABLE PUBLIC.INTERNET_RADIO TYPE CACHED; + SET TABLE PUBLIC.MEDIA_FILE TYPE CACHED; + SET TABLE PUBLIC.MUSIC_FILE_INFO TYPE CACHED; + SET TABLE PUBLIC.MUSIC_FOLDER TYPE CACHED; + SET TABLE PUBLIC.MUSIC_FOLDER_USER TYPE CACHED; + SET TABLE PUBLIC.PLAYER TYPE CACHED; + SET TABLE PUBLIC.PLAYER_TRANSCODING TYPE CACHED; + SET TABLE PUBLIC.PLAYLIST TYPE CACHED; + SET TABLE PUBLIC.PLAYLIST_FILE TYPE CACHED; + SET TABLE PUBLIC.PLAYLIST_USER TYPE CACHED; + SET TABLE PUBLIC.PLAY_QUEUE TYPE CACHED; + SET TABLE PUBLIC.PLAY_QUEUE_FILE TYPE CACHED; + SET TABLE PUBLIC.PODCAST_CHANNEL TYPE CACHED; + SET TABLE PUBLIC.PODCAST_CHANNEL_RULES TYPE CACHED; + SET TABLE PUBLIC.PODCAST_EPISODE TYPE CACHED; + SET TABLE PUBLIC.SHARE TYPE CACHED; + SET TABLE PUBLIC.SHARE_FILE TYPE CACHED; + SET TABLE PUBLIC.SONOSLINK TYPE CACHED; + SET TABLE PUBLIC.STARRED_ALBUM TYPE CACHED; + SET TABLE PUBLIC.STARRED_ARTIST TYPE CACHED; + SET TABLE PUBLIC.STARRED_MEDIA_FILE TYPE CACHED; + SET TABLE PUBLIC.SYSTEM_AVATAR TYPE CACHED; + SET TABLE PUBLIC.TRANSCODING TYPE CACHED; + SET TABLE PUBLIC.USERS TYPE CACHED; + SET TABLE PUBLIC.USER_CREDENTIALS TYPE CACHED; + SET TABLE PUBLIC.USER_RATING TYPE CACHED; + SET TABLE PUBLIC.USER_SETTINGS TYPE CACHED; + SET TABLE PUBLIC.VERSION TYPE CACHED; + + + SET DATABASE DEFAULT TABLE TYPE MEMORY; + SET TABLE PUBLIC.ALBUM TYPE MEMORY; + SET TABLE PUBLIC.ARTIST TYPE MEMORY; + SET TABLE PUBLIC.BOOKMARK TYPE MEMORY; + SET TABLE PUBLIC.COVER_ART TYPE MEMORY; + SET TABLE PUBLIC.CUSTOM_AVATAR TYPE MEMORY; + SET TABLE PUBLIC.DATABASECHANGELOG TYPE MEMORY; + SET TABLE PUBLIC.DATABASECHANGELOGLOCK TYPE MEMORY; + SET TABLE PUBLIC.GENRE TYPE MEMORY; + SET TABLE PUBLIC.INTERNET_RADIO TYPE MEMORY; + SET TABLE PUBLIC.MEDIA_FILE TYPE MEMORY; + SET TABLE PUBLIC.MUSIC_FILE_INFO TYPE MEMORY; + SET TABLE PUBLIC.MUSIC_FOLDER TYPE MEMORY; + SET TABLE PUBLIC.MUSIC_FOLDER_USER TYPE MEMORY; + SET TABLE PUBLIC.PLAYER TYPE MEMORY; + SET TABLE PUBLIC.PLAYER_TRANSCODING TYPE MEMORY; + SET TABLE PUBLIC.PLAYLIST TYPE MEMORY; + SET TABLE PUBLIC.PLAYLIST_FILE TYPE MEMORY; + SET TABLE PUBLIC.PLAYLIST_USER TYPE MEMORY; + SET TABLE PUBLIC.PLAY_QUEUE TYPE MEMORY; + SET TABLE PUBLIC.PLAY_QUEUE_FILE TYPE MEMORY; + SET TABLE PUBLIC.PODCAST_CHANNEL TYPE MEMORY; + SET TABLE PUBLIC.PODCAST_CHANNEL_RULES TYPE MEMORY; + SET TABLE PUBLIC.PODCAST_EPISODE TYPE MEMORY; + SET TABLE PUBLIC.SHARE TYPE MEMORY; + SET TABLE PUBLIC.SHARE_FILE TYPE MEMORY; + SET TABLE PUBLIC.SONOSLINK TYPE MEMORY; + SET TABLE PUBLIC.STARRED_ALBUM TYPE MEMORY; + SET TABLE PUBLIC.STARRED_ARTIST TYPE MEMORY; + SET TABLE PUBLIC.STARRED_MEDIA_FILE TYPE MEMORY; + SET TABLE PUBLIC.SYSTEM_AVATAR TYPE MEMORY; + SET TABLE PUBLIC.TRANSCODING TYPE MEMORY; + SET TABLE PUBLIC.USERS TYPE MEMORY; + SET TABLE PUBLIC.USER_CREDENTIALS TYPE MEMORY; + SET TABLE PUBLIC.USER_RATING TYPE MEMORY; + SET TABLE PUBLIC.USER_SETTINGS TYPE MEMORY; + SET TABLE PUBLIC.VERSION TYPE MEMORY; + + + \ No newline at end of file diff --git a/airsonic-main/src/main/resources/liquibase/11.1/changelog.xml b/airsonic-main/src/main/resources/liquibase/11.1/changelog.xml index 41d36a129..a480b2907 100644 --- a/airsonic-main/src/main/resources/liquibase/11.1/changelog.xml +++ b/airsonic-main/src/main/resources/liquibase/11.1/changelog.xml @@ -6,4 +6,5 @@ + diff --git a/airsonic-main/src/test/java/org/airsonic/player/config/AirsonicHomeConfigTest.java b/airsonic-main/src/test/java/org/airsonic/player/config/AirsonicHomeConfigTest.java index 58dad3ac6..790f93542 100644 --- a/airsonic-main/src/test/java/org/airsonic/player/config/AirsonicHomeConfigTest.java +++ b/airsonic-main/src/test/java/org/airsonic/player/config/AirsonicHomeConfigTest.java @@ -41,8 +41,8 @@ import java.nio.file.Path; import java.util.stream.Stream; -import static org.junit.Assert.assertFalse; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -91,7 +91,7 @@ public void testGetters() { assertEquals(tempAirsonicDir.resolve("airsonic.properties").toString(), homeConfig.getPropertyFile().toString()); assertEquals(tempAirsonicDir.resolve("airsonic.log").toString(), homeConfig.getDefaultLogFile().toString()); assertEquals( - "jdbc:hsqldb:file:" + tempAirsonicDir.resolve("db").resolve("airsonic").toString() + ";hsqldb.tx=mvcc;sql.enforce_size=false;sql.char_literal=false;sql.nulls_first=false;sql.pad_space=false;hsqldb.defrag_limit=50;shutdown=true", + "jdbc:hsqldb:file:" + tempAirsonicDir.resolve("db").resolve("airsonic").toString() + ";hsqldb.tx=mvcc;sql.enforce_size=false;sql.char_literal=false;sql.nulls_first=false;sql.pad_space=false;hsqldb.defrag_limit=50;hsqldb.default_table_type=CACHED;shutdown=true", homeConfig.getDefaultJDBCUrl()); } } diff --git a/airsonic-main/src/test/java/org/airsonic/player/config/AirsonicScanConfigTest.java b/airsonic-main/src/test/java/org/airsonic/player/config/AirsonicScanConfigTest.java index a5a3aed7d..6d55b18cc 100644 --- a/airsonic-main/src/test/java/org/airsonic/player/config/AirsonicScanConfigTest.java +++ b/airsonic-main/src/test/java/org/airsonic/player/config/AirsonicScanConfigTest.java @@ -54,13 +54,13 @@ public class AirsonicScanConfigTestWithDefaultValue { @Test public void testFullTimeoutProperty() { - Integer expectedFullTimeout = 3600; + Integer expectedFullTimeout = 14400; assertEquals(expectedFullTimeout, scanConfig.getFullTimeout()); } @Test public void testTimeoutProperty() { - Integer expectedTimeout = 600; + Integer expectedTimeout = 3600; assertEquals(expectedTimeout, scanConfig.getTimeout()); } diff --git a/airsonic-main/src/test/java/org/airsonic/player/service/DatabaseServiceTest.java b/airsonic-main/src/test/java/org/airsonic/player/service/DatabaseServiceTest.java index 635cf0121..a00c3622d 100644 --- a/airsonic-main/src/test/java/org/airsonic/player/service/DatabaseServiceTest.java +++ b/airsonic-main/src/test/java/org/airsonic/player/service/DatabaseServiceTest.java @@ -25,10 +25,10 @@ import java.time.LocalDateTime; import java.util.stream.Stream; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.*; diff --git a/airsonic-main/src/test/java/org/airsonic/player/service/MediaFileServiceTest.java b/airsonic-main/src/test/java/org/airsonic/player/service/MediaFileServiceTest.java index fac27df20..a016f152f 100644 --- a/airsonic-main/src/test/java/org/airsonic/player/service/MediaFileServiceTest.java +++ b/airsonic-main/src/test/java/org/airsonic/player/service/MediaFileServiceTest.java @@ -92,7 +92,7 @@ public void createIndexedTracksFailedByNoIndexTracksReturnEmptyList() { assertTrue(actual.isEmpty()); // verify updateMedia does not called verify(mediaFileRepository).findByFolderAndPath(any(), eq("valid/airsonic-test.wav")); - verify(mediaFileRepository).saveAndFlush(base); + verify(mediaFileRepository).save(base); verify(coverArtService).persistIfNeeded(eq(base)); } } diff --git a/airsonic-main/src/test/java/org/airsonic/player/service/MediaScannerServiceTest.java b/airsonic-main/src/test/java/org/airsonic/player/service/MediaScannerServiceTest.java index d45357c98..c1ff08989 100644 --- a/airsonic-main/src/test/java/org/airsonic/player/service/MediaScannerServiceTest.java +++ b/airsonic-main/src/test/java/org/airsonic/player/service/MediaScannerServiceTest.java @@ -7,12 +7,14 @@ import org.airsonic.player.domain.MusicFolder; import org.airsonic.player.domain.MusicFolder.Type; import org.airsonic.player.repository.MusicFolderRepository; +import org.airsonic.player.service.search.IndexManager; import org.airsonic.player.util.MusicFolderTestData; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.SpyBean; @@ -26,8 +28,10 @@ import java.util.List; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @TestPropertySource(properties = { @@ -46,6 +50,9 @@ public class MediaScannerServiceTest { @SpyBean private SettingsService settingsService; + @SpyBean + private IndexManager indexManager; + @SpyBean private AirsonicScanConfig scanConfig; @@ -76,6 +83,7 @@ public void setup() { TestCaseUtils.waitForScanFinish(mediaScannerService); mediaFolderService.clearMediaFileCache(); mediaFolderService.clearMusicFolderCache(); + Mockito.reset(indexManager); } @AfterEach @@ -90,6 +98,7 @@ public void testMusicFullScanTimeOut() { when(settingsService.getIgnoreSymLinks()).thenReturn(false); when(scanConfig.getFullTimeout()).thenReturn(1); doAnswer(invocation -> { + invocation.callRealMethod(); Thread.sleep(10000); return null; }).when(mediaFileService).setMemoryCacheEnabled(anyBoolean()); @@ -104,6 +113,7 @@ public void testMusicFullScanTimeOut() { long end = System.currentTimeMillis(); // Test that the scan time out is respected assertTrue(end - start < 10000); + verify(indexManager).stopIndexing(any()); } @Test @@ -112,6 +122,7 @@ public void testMusicScanTimeOut() { when(settingsService.getIgnoreSymLinks()).thenReturn(false); when(scanConfig.getTimeout()).thenReturn(1); doAnswer(invocation -> { + invocation.callRealMethod(); Thread.sleep(10000); return null; }).when(mediaFileService).setMemoryCacheEnabled(anyBoolean()); @@ -126,5 +137,6 @@ public void testMusicScanTimeOut() { long end = System.currentTimeMillis(); // Test that the scan time out is respected assertTrue(end - start < 10000); + verify(indexManager).stopIndexing(any()); } } diff --git a/airsonic-main/src/test/java/org/airsonic/player/service/MediaScannerServiceUnitTest.java b/airsonic-main/src/test/java/org/airsonic/player/service/MediaScannerServiceUnitTest.java index 87870b33a..470757d2b 100644 --- a/airsonic-main/src/test/java/org/airsonic/player/service/MediaScannerServiceUnitTest.java +++ b/airsonic-main/src/test/java/org/airsonic/player/service/MediaScannerServiceUnitTest.java @@ -9,7 +9,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.messaging.simp.SimpMessagingTemplate; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.when; diff --git a/airsonic-main/src/test/java/org/airsonic/player/service/PlayQueueServiceTest.java b/airsonic-main/src/test/java/org/airsonic/player/service/PlayQueueServiceTest.java index 3f9ab690b..2366aeb78 100644 --- a/airsonic-main/src/test/java/org/airsonic/player/service/PlayQueueServiceTest.java +++ b/airsonic-main/src/test/java/org/airsonic/player/service/PlayQueueServiceTest.java @@ -43,9 +43,9 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; diff --git a/airsonic-main/src/test/java/org/airsonic/player/service/VersionServiceTest.java b/airsonic-main/src/test/java/org/airsonic/player/service/VersionServiceTest.java index b738f70cd..b95aa9d0b 100644 --- a/airsonic-main/src/test/java/org/airsonic/player/service/VersionServiceTest.java +++ b/airsonic-main/src/test/java/org/airsonic/player/service/VersionServiceTest.java @@ -31,8 +31,8 @@ import java.util.Properties; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @ExtendWith(MockitoExtension.class) diff --git a/docs/README.md b/docs/README.md index 3b5fe57b4..e47385956 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,7 +3,7 @@ ## Contents - [Configures](./configures/README.md) - - [Detail Configuration](./detail.md) + - [Detail Configuration](./configures/detail.md) - Media - [Jukebox](./media/jukebox.md) - [TroubleShooting](./troubleshooting.md) diff --git a/docs/configures/detail.md b/docs/configures/detail.md index d609ceb66..7b2dad9d3 100644 --- a/docs/configures/detail.md +++ b/docs/configures/detail.md @@ -97,7 +97,7 @@ The maximum time in seconds that Airsonic will spend scanning media folders whe | item | description | | --- | --- | | type | integer | -| default | 3600 | +| default | 14400 | | example | airsonic.scan.full-timeout=3600 | | configurable by | Java options, environment variables | | environment variable | AIRSONIC_SCAN_FULLTIMEOUT | @@ -110,7 +110,7 @@ The maximum time in seconds that Airsonic will spend scanning media folders whe | item | description | | --- | --- | | type | integer | -| default | 600 | +| default | 3600 | | example | airsonic.scan.timeout=600 | | configurable by | Java options, environment variables | | environment variable | AIRSONIC_SCAN_TIMEOUT | diff --git a/integration-test/src/test/java/org/airsonic/test/Scanner.java b/integration-test/src/test/java/org/airsonic/test/Scanner.java index 743b3de4c..0d77c655e 100644 --- a/integration-test/src/test/java/org/airsonic/test/Scanner.java +++ b/integration-test/src/test/java/org/airsonic/test/Scanner.java @@ -11,7 +11,6 @@ import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.io.FileUtils; -import org.junit.Assert; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -31,6 +30,7 @@ import java.util.Objects; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; public class Scanner { public static final String SERVER = System.getProperty("DockerTestingHost", "http://localhost:4040"); @@ -58,7 +58,7 @@ public static UriComponentsBuilder addRestParameters(UriComponentsBuilder builde } public static void doScan() throws Exception { - Assert.assertFalse(isScanning()); + assertFalse(isScanning()); String startScan = rest.getForObject( addRestParameters(UriComponentsBuilder.fromHttpUrl(SERVER + "/rest/startScan")).toUriString(), @@ -73,7 +73,7 @@ public static void doScan() throws Exception { Thread.sleep(sleepTime); } - Assert.assertFalse(isScanning()); + assertFalse(isScanning()); } private static boolean isScanning() { diff --git a/integration-test/src/test/java/org/airsonic/test/StreamIT.java b/integration-test/src/test/java/org/airsonic/test/StreamIT.java index ce2c1bc53..a32c2191f 100644 --- a/integration-test/src/test/java/org/airsonic/test/StreamIT.java +++ b/integration-test/src/test/java/org/airsonic/test/StreamIT.java @@ -2,13 +2,18 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.nio.file.Paths; import static org.assertj.core.api.Assertions.assertThat; public class StreamIT { + + private static final Logger LOG = LoggerFactory.getLogger(StreamIT.class); + @Test public void testStreamFlacAsMp3() throws Exception { testFileStreaming("dead"); @@ -30,7 +35,10 @@ private void testFileStreaming(String file) throws Exception { ""); Scanner.doScan(); String mediaFileId = Scanner.getMediaFilesInMusicFolder().parallelStream() - .filter(x -> StringUtils.containsIgnoreCase(x.getTitle(), file)) + .filter(x -> { + LOG.info("media file: {}", x.getTitle()); + return StringUtils.containsIgnoreCase(x.getTitle(), file); + }) .findAny() .map(x -> x.getId()) .orElseThrow(() -> new RuntimeException("no media file id matched"));